What You Will Build
Three concentric animated ring arcs (Move, Exercise, Stand) that fill to their respective percentages with spring animations, mimicking the Apple Watch Activity Rings. Each ring has a background track, a colored progress arc, and a label below showing the current percentage.
Why This Pattern Matters
Circular progress indicators are everywhere in fitness, health, and productivity apps. Drawing arcs on Canvas with animated sweep angles teaches you the drawArc API, stroke styling with StrokeCap.Round, and spring-based value animation.
The Activity Rings
@Composable
fun ActivityRingsScreen() {
var move by remember { mutableFloatStateOf(0.85f) }
var exercise by remember { mutableFloatStateOf(0.65f) }
var stand by remember { mutableFloatStateOf(0.9f) }
val animMove by animateFloatAsState(
move, spring(dampingRatio = 0.6f))
val animExercise by animateFloatAsState(
exercise, spring(dampingRatio = 0.6f))
val animStand by animateFloatAsState(
stand, spring(dampingRatio = 0.6f))
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(modifier = Modifier.size(220.dp),
contentAlignment = Alignment.Center) {
Canvas(Modifier.fillMaxSize()) {
val rings = listOf(
Triple(Color.Red, animMove, 0.45f),
Triple(Color(0xFF34C759), animExercise, 0.33f),
Triple(Color(0xFF00BCD4), animStand, 0.21f)
)
rings.forEach { (color, value, radiusFrac) ->
val stroke = Stroke(18f, cap = StrokeCap.Round)
val r = size.minDimension * radiusFrac
val tl = Offset(center.x - r, center.y - r)
val sz = Size(r * 2, r * 2)
// Background track
drawArc(color.copy(0.2f), 0f, 360f, false,
tl, sz, style = stroke)
// Progress arc
drawArc(color, -90f, 360f * value, false,
tl, sz, style = stroke)
}
}
}
Spacer(Modifier.height(24.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
RingLabel("Move", "${'$'}{(animMove * 100).toInt()}%", Color.Red)
RingLabel("Exercise",
"${'$'}{(animExercise * 100).toInt()}%", Color(0xFF34C759))
RingLabel("Stand",
"${'$'}{(animStand * 100).toInt()}%", Color(0xFF00BCD4))
}
Spacer(Modifier.height(24.dp))
Button(onClick = {
move = Random.nextFloat()
exercise = Random.nextFloat()
stand = Random.nextFloat()
}) { Text("Randomize") }
}
}
@Composable
fun RingLabel(label: String, value: String, color: Color) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(value, fontWeight = FontWeight.Bold, color = color)
Text(label, fontSize = 12.sp, color = Color.Gray)
}
}
Tips and Pitfalls
- StrokeCap.Round gives the arcs smooth rounded endpoints. Without it, the ends are flat and look unfinished.
- Start angle of -90f positions the arc start at the 12 o'clock position (top center).
- Background track at 0.2f alpha shows the full ring outline for context.
- Spring damping of 0.6f makes the rings overshoot slightly before settling, adding liveliness.