What You Will Build
A retro-style flip clock that displays hours, minutes, and seconds as individual digit cards. Each digit subtly rotates on the X axis when it changes, simulating the mechanical flip of a split-flap display.
Why This Pattern Matters
Flip clocks are a staple of dashboard and kiosk UIs. This project teaches you real-time state updates with LaunchedEffect and delay, per-digit animation with animateFloatAsState, and 3D transforms via graphicsLayer.
Step 1: Track Time with LaunchedEffect
@Composable
fun FlipClockScreen() {
var hours by remember { mutableIntStateOf(
Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
) }
var minutes by remember { mutableIntStateOf(
Calendar.getInstance().get(Calendar.MINUTE)
) }
var seconds by remember { mutableIntStateOf(
Calendar.getInstance().get(Calendar.SECOND)
) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
val cal = Calendar.getInstance()
hours = cal.get(Calendar.HOUR_OF_DAY)
minutes = cal.get(Calendar.MINUTE)
seconds = cal.get(Calendar.SECOND)
}
}
// ... UI next
}
Step 2: Create the FlipDigit Composable
Each digit gets a spring-based rotation that triggers whenever the digit value changes. The graphicsLayer applies a subtle rotationX to simulate the flip:
@Composable
fun FlipDigit(digit: Int) {
val rotation by animateFloatAsState(
targetValue = digit * 36f,
animationSpec = spring(dampingRatio = 0.6f),
label = "flip"
)
Box(
Modifier
.width(50.dp).height(70.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF2D2D2D))
.graphicsLayer(rotationX = (rotation % 360f) * 0.05f),
contentAlignment = Alignment.Center
) {
Text(
"$digit",
color = Color.White,
fontSize = 40.sp,
fontWeight = FontWeight.Bold
)
}
}
Step 3: Arrange the Clock Layout
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
FlipDigit(hours / 10); FlipDigit(hours % 10)
Text(":", color = Color.White, fontSize = 48.sp,
fontWeight = FontWeight.Bold)
FlipDigit(minutes / 10); FlipDigit(minutes % 10)
Text(":", color = Color.White, fontSize = 48.sp,
fontWeight = FontWeight.Bold)
FlipDigit(seconds / 10); FlipDigit(seconds % 10)
}
Tips and Pitfalls
- dampingRatio = 0.6f gives a satisfying bounce. Lower values (0.3) make it bouncier; 1.0 removes bounce entirely.
- Why multiply digit * 36f? We need a different target for each digit value so the animation triggers. 36 degrees per digit spreads 0-9 across a full circle.
- cameraDistance — if you increase rotationX, also increase cameraDistance in graphicsLayer to prevent distortion.
- Battery impact: A 1-second timer is fine, but avoid sub-100ms timers for always-on displays.