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.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.