Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import androidx.compose.foundation.Canvas
- import androidx.compose.foundation.background
- import androidx.compose.foundation.layout.Box
- import androidx.compose.foundation.layout.BoxWithConstraints
- import androidx.compose.foundation.layout.aspectRatio
- import androidx.compose.foundation.layout.fillMaxSize
- import androidx.compose.foundation.layout.height
- import androidx.compose.foundation.layout.padding
- import androidx.compose.foundation.layout.size
- import androidx.compose.foundation.shape.RoundedCornerShape
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.Text
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.getValue
- import androidx.compose.runtime.mutableFloatStateOf
- import androidx.compose.runtime.remember
- import androidx.compose.runtime.setValue
- import androidx.compose.ui.Alignment
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.geometry.Offset
- import androidx.compose.ui.geometry.Size
- import androidx.compose.ui.graphics.Brush
- import androidx.compose.ui.graphics.Color
- import androidx.compose.ui.graphics.SolidColor
- import androidx.compose.ui.graphics.StrokeCap
- import androidx.compose.ui.graphics.drawscope.Stroke
- import androidx.compose.ui.graphics.graphicsLayer
- import androidx.compose.ui.layout.onSizeChanged
- import androidx.compose.ui.platform.LocalDensity
- import androidx.compose.ui.tooling.preview.Preview
- import androidx.compose.ui.unit.Dp
- import androidx.compose.ui.unit.dp
- import androidx.compose.ui.unit.sp
- import androidx.compose.ui.util.fastCoerceIn
- import kotlin.math.asin
- import kotlin.math.cos
- import kotlin.math.max
- import kotlin.math.sin
- /**
- * Circular progress indicator with an optional composable anchored to the arc endpoint.
- *
- * @param progress Current progress in [0.0, 1.0]; values outside this range are coerced.
- * @param modifier Modifier applied to this indicator.
- * @param strokeWidth Width of the progress arc.
- * @param strokeBrush Brush for the progress arc.
- * @param trackBrush Brush for the background track circle.
- * @param trackWidth Width of the background track; defaults to [strokeWidth].
- * @param strokeCap Cap style for the arc ends.
- * @param startAngle Arc start angle in degrees (0 = 12 o'clock position).
- * @param direction Sweep direction of the arc.
- * @param rotateContent If true, [content] is rotated to align with the arc tangent at the endpoint.
- * @param keepContentUpsideUp When [rotateContent] is true, prevents [content] from appearing upside-down.
- * @param contentAlignment Aligns [content]'s edge to the LEADING, TRAILING, or CENTER of the arc endpoint.
- * @param content Composable placed at the arc endpoint. Receives the arc radius and the current
- * rotation angle in degrees so callers can size or orient child elements accordingly.
- */
- @Composable
- fun AdvancedCircularProgressIndicator(
- progress: () -> Float,
- modifier: Modifier = Modifier,
- strokeWidth: Dp = 8.dp,
- strokeBrush: Brush = SolidColor(MaterialTheme.colorScheme.primary),
- trackBrush: Brush = SolidColor(MaterialTheme.colorScheme.onSurface),
- trackWidth: Dp = strokeWidth,
- strokeCap: StrokeCap = StrokeCap.Round,
- startAngle: Float = 180f,
- direction: RotationDirection = RotationDirection.CLOCKWISE,
- rotateContent: Boolean = true,
- keepContentUpsideUp: Boolean = true,
- contentAlignment: RadialAlignment = RadialAlignment.TRAILING,
- content: (@Composable (radius: Float, angle: Float) -> Unit)? = null
- ) {
- val coercedProgress = progress().fastCoerceIn(0f, 1f)
- val arcEndDeg = (startAngle + 360f * coercedProgress * direction.multiplier) % 360f
- BoxWithConstraints(
- modifier = modifier.aspectRatio(1f),
- contentAlignment = Alignment.Center
- ) {
- Canvas(modifier.fillMaxSize()) {
- val trackWidthPx = trackWidth.toPx()
- val strokeWidthPx = strokeWidth.toPx()
- val radiusOffset = max(trackWidthPx, strokeWidthPx) / 2f
- val arcSize = size.minDimension - radiusOffset * 2f
- drawCircle(
- brush = trackBrush,
- radius = size.minDimension / 2f - radiusOffset,
- style = Stroke(width = trackWidthPx, cap = strokeCap)
- )
- drawArc(
- brush = strokeBrush,
- startAngle = startAngle - 90f,
- sweepAngle = 360f * coercedProgress * direction.multiplier,
- useCenter = false,
- topLeft = Offset(radiusOffset, radiusOffset),
- size = Size(arcSize, arcSize),
- style = Stroke(width = strokeWidthPx, cap = strokeCap)
- )
- }
- if (content != null) {
- val density = LocalDensity.current
- val strokeWidthPx = remember(strokeWidth, density) { with(density) { strokeWidth.toPx() } }
- val trackWidthPx = remember(trackWidth, density) { with(density) { trackWidth.toPx() } }
- val radius = remember(constraints.maxWidth, strokeWidthPx, trackWidthPx) {
- constraints.maxWidth / 2f - max(strokeWidthPx, trackWidthPx) / 2f
- }
- val capAngleOffset = remember(strokeWidthPx, radius) {
- Math.toDegrees(asin((strokeWidthPx / 2.0) / radius)).toFloat()
- }
- var contentWidthPx by remember { mutableFloatStateOf(0f) }
- val alignmentDelta = remember(contentWidthPx, radius, contentAlignment) {
- val ratio = (contentWidthPx / 2f / radius).coerceIn(-1f, 1f)
- val halfAngle = Math.toDegrees(asin(ratio.toDouble())).toFloat()
- when (contentAlignment) {
- RadialAlignment.LEADING -> +halfAngle
- RadialAlignment.TRAILING -> -halfAngle
- RadialAlignment.CENTER -> 0f
- }
- }
- val alignedAngle = arcEndDeg + capAngleOffset * direction.multiplier + alignmentDelta
- val visualRotation = if (keepContentUpsideUp) keepUpsideRotation(alignedAngle) else alignedAngle
- val contentOffset = pointOnCircle(radius.toInt(), userAngleToCanvas(alignedAngle))
- Box(
- Modifier
- .onSizeChanged { contentWidthPx = it.width.toFloat() }
- .graphicsLayer {
- if (rotateContent) rotationZ = visualRotation
- translationX = contentOffset.x
- translationY = contentOffset.y
- }
- ) {
- content(radius, visualRotation)
- }
- }
- }
- }
- private fun userAngleToCanvas(angle: Float): Float = angle - 90f
- private fun keepUpsideRotation(degrees: Float): Float {
- val normalized = ((degrees % 360f) + 360f) % 360f
- return if (normalized in 90f..270f) normalized - 180f else normalized
- }
- private fun pointOnCircle(radius: Int, angleDegrees: Float): Offset {
- val angleRad = Math.toRadians(angleDegrees.toDouble())
- return Offset(
- x = (radius * cos(angleRad)).toFloat(),
- y = (radius * sin(angleRad)).toFloat()
- )
- }
- enum class RotationDirection(val multiplier: Int) {
- CLOCKWISE(1),
- COUNTER_CLOCKWISE(-1)
- }
- enum class RadialAlignment {
- LEADING,
- CENTER,
- TRAILING
- }
- @Preview
- @Composable
- fun AdvancedCircularProgressIndicatorPreview() {
- AdvancedCircularProgressIndicator(
- { 0.6f },
- Modifier.size(200.dp),
- strokeWidth = 28.dp,
- ) { radius, angle ->
- Box(
- modifier = Modifier
- .padding(end = 3.dp)
- .height(22.dp)
- .background(Color.White.copy(alpha = 0.2f), RoundedCornerShape(100))
- .padding(start = 8.dp, end = 4.dp),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = "$radius",
- fontSize = 12.sp
- )
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment