What You Will Build
A number display where each digit rolls vertically to its new value when the number changes, similar to an odometer or airport departure board. Each digit animates independently using spring physics, creating a satisfying mechanical rolling effect.
Why This Pattern Matters
Rolling counters make number changes feel meaningful rather than instant. They are used in score displays, financial dashboards, follower counts, and gamified UIs. The technique teaches clipped overflow rendering and per-character animation.
The Rolling Number Composable
@Composable
fun RollingNumber(value: Int) {
val digits = value.toString()
Row(horizontalArrangement = Arrangement.Center) {
digits.forEach { char ->
val digit = char.digitToInt()
val animatedOffset by animateFloatAsState(
targetValue = digit.toFloat(),
animationSpec = spring(
dampingRatio = 0.7f,
stiffness = 100f
)
)
Box(
modifier = Modifier
.width(36.dp)
.height(60.dp)
.clipToBounds()
) {
Column(
modifier = Modifier.offset(
y = (-animatedOffset * 60).dp
)
) {
(0..9).forEach { n ->
Box(
modifier = Modifier.size(36.dp, 60.dp),
contentAlignment = Alignment.Center
) {
Text("$n", fontSize = 48.sp,
fontWeight = FontWeight.Black)
}
}
}
}
}
}
}
Using the Rolling Counter
@Composable
fun RollingCounterScreen() {
var value by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
RollingNumber(value)
Spacer(Modifier.height(32.dp))
Button(onClick = {
value = Random.nextInt(200, 1300)
}) {
Text("Random Value")
}
}
}
Tips and Pitfalls
- clipToBounds() is essential to hide the digits above and below the visible window. Without it, all 10 digits would be visible.
- Spring stiffness of 100f creates a slow, satisfying roll. Higher stiffness makes digits snap; lower makes them bounce excessively.
- Each digit animates independently because each character in the string gets its own animateFloatAsState.
- Digit count changes: When going from "99" to "100", Compose recomposes the Row with a new character, which may cause a jump. Consider padding with leading zeros if your range is fixed.