Animated Clock

A simple clock

Let's build a simple analog clock display in Compose.

First, let's look at our API for the clock:

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...
import kotlin.math.min

@Composable
fun AnalogClock(
    hour: Int,
    minute: Int,
    modifier: Modifier = Modifier,
    outlineColor: Color = Color.Black,
    outlineWidth: Dp = 8.dp,
    fillColor: Color = Color.Gray,
    hourHandColor: Color = Color.DarkGray,
    minuteHandColor: Color = Color.LightGray,
    hourTickColor: Color = Color.Black,
    hourTickLength: Dp = 16.dp,
) {
    with(LocalDensity.current) {
        val outlineWidthPx = remember { outlineWidth.toPx() }
        // ...
}

We're passing in parameters for

  • hour and minute - the current time to display
  • modifier - our normal Modifier (note that modifier should be the first optional parameter)
  • outlineColor, outlineWidth - the border of the clock. Note that the width is in Dp so we'll need to convert it to pixels
  • fillColor - the color of the inside of the clock
  • hourHandColor, minuteHandColor - the colors of the clock hands
  • hourTickColor, hourTickLength - the color and length of the markings on the circle indicating the hours. (Again, length is in Dp)

As before, we convert dp to px using the LocalDensity composition local's toPx() function:

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...

@Composable
fun AnalogClock(
    // ...
    hourTickLength: Dp = 16.dp,
) {
    with(LocalDensity.current) {
        val outlineWidthPx = remember { outlineWidth.toPx() }
        val hourTickLengthPx = remember { hourTickLength.toPx() }

        Canvas(modifier = modifier) { // this == DrawScope
            // ...
    }
}

Then, let's draw the filled circle followed by the outline. We draw the outline last so it isn't overlaid by the fill:

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...

@Composable
fun AnalogClock(
    // ...
) {
    with(LocalDensity.current) {
        // ...

        Canvas(modifier = modifier) { // this == DrawScope
            val diameter = min(size.width, size.height) * 0.8f
            val radius = diameter/2
            drawCircle(
                color = fillColor,
                radius = radius,
                // center = this.center -- default uses center of DrawScope
                style = Fill,
            )

            drawCircle(
                color = outlineColor,
                radius = radius,
                // center = this.center -- default uses center of DrawScope
                style = Stroke(outlineWidthPx),
            )

            repeat(12) { hourTick ->
                // ...
        }
    }
}

Call the clock the same way we call other Composables

show in full file app/src/main/java/com/androidbyexample/composeclock/MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        setContent {
            ComposeClockTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    // ...
                    }

//@Preview(showBackground = true)
//@Composable
//fun GreetingPreview() {
//  ComposeClockTheme {
//      Greeting("Android")
                    AnalogClock(
                        hour = hour,
                        minute = minute,
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding),
                    )
                }
            }
        }
    }
}

And we see

Clock circles

Next, let's draw the hour tick marks. This is what they'll look like:

Clock hour ticks

To create each of these, we will draw vertical lines and rotate the canvas.

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...

@Composable
fun AnalogClock(
    // ...
) {
    with(LocalDensity.current) {
        // ...
        Canvas(modifier = modifier) { // this == DrawScope
            // ...
            )

            repeat(12) { hourTick ->
                rotate(hourTick * 30f) {
                    val start = center - Offset(0f, radius)
                    val end = start + Offset(0f, hourTickLengthPx)
                    drawLine(
                        color = hourTickColor,
                        start = start,
                        end = end,
                        strokeWidth = outlineWidthPx,
                    )
                }
            }

            val minuteRatio = minute/60f
            // ...
        }
    }
}

We use the repeat function to loop from 0-11, each time calling rotate to turn the canvas 30 degrees (1/12 of the 360-degree circle).

The start and end properties are Offsets which contain both an x and y value. We calcuate them as offsets from the center of the DrawScope. To get to the start, we subtract the radius for y to get to the circle border. Then we add back the hourTickLength to move down.

To draw the hands, we'll do something very similar. We rotate the canvas to the hand positions, then draw from the center.

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...

@Composable
fun AnalogClock(
    // ...
) {
    with(LocalDensity.current) {
        // ...
        Canvas(modifier = modifier) { // this == DrawScope
            // ...
            }

            val minuteRatio = minute/60f
            val hourRatio = (hour + minuteRatio) / 12f

            // draw minute hand first in case it overlaps the smaller hour hand
            rotate(minuteRatio * 360) {
                drawLine(
                    color = minuteHandColor,
                    start = center - Offset(0f, radius*0.9f),
                    end = center,
                    strokeWidth = outlineWidthPx,
                )
            }
            rotate(hourRatio * 360) {
                drawLine(
                    color = hourHandColor,
                    start = center - Offset(0f, radius*0.6f),
                    end = center,
                    strokeWidth = outlineWidthPx,
                )
            }

            // give the hour hand a pin to look nicer
            // ...
        }
    }
}

Clock hands

The hands don't look great where they meet. Let's give the hour hand a little pin at the center, a small filled circle.

show in full file app/src/main/java/com/androidbyexample/composeclock/Clock.kt
// ...

@Composable
fun AnalogClock(
    // ...
) {
    with(LocalDensity.current) {
        // ...
        Canvas(modifier = modifier) { // this == DrawScope
            // ...
            }

            // give the hour hand a pin to look nicer
            drawCircle(
                color = hourHandColor,
                radius = outlineWidthPx,
                style = Fill,
            )
        }
    }
}

Clock hour hand pin

That's better looking!

We can animate the clock by setting up state for the hour and minute. We could do this in a view model, but for this simple example, we'll use MutableState in the MainActivity Composable:

show in full file app/src/main/java/com/androidbyexample/composeclock/MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        setContent {
            ComposeClockTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
//                  Greeting(
//                      name = "Android",
//                      modifier = Modifier.padding(innerPadding)
//                  )
//              }
//          }
//      }
//  }
//}
                    var hour by remember { mutableIntStateOf(0) }
                    var minute by remember { mutableIntStateOf(0) }

//@Composable
//fun Greeting(name: String, modifier: Modifier = Modifier) {
//  Text(
//      text = "Hello $name!",
//      modifier = modifier
//  )
//}
                    LaunchedEffect(true) {
                        // because true doesn't change, we never restart this coroutine
                        while(true) {
                            minute++
                            if (minute > 59) {
                                minute = 0
                                hour++
                            }
                            delay(10)
                        }
                    }

// ...
                }
            }
        }
    }
}

Clock animated


All code changes

ADDED: app/src/main/java/com/androidbyexample/composeclock/Clock.kt
package com.androidbyexample.composeclock

import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.min

@Composable fun AnalogClock( hour: Int, minute: Int, modifier: Modifier = Modifier, outlineColor: Color = Color.Black, outlineWidth: Dp = 8.dp, fillColor: Color = Color.Gray, hourHandColor: Color = Color.DarkGray, minuteHandColor: Color = Color.LightGray, hourTickColor: Color = Color.Black, hourTickLength: Dp = 16.dp, ) {
with(LocalDensity.current) { val outlineWidthPx = remember { outlineWidth.toPx() } val hourTickLengthPx = remember { hourTickLength.toPx() }
Canvas(modifier = modifier) { // this == DrawScope
val diameter = min(size.width, size.height) * 0.8f val radius = diameter/2 drawCircle( color = fillColor, radius = radius, // center = this.center -- default uses center of DrawScope style = Fill, ) drawCircle( color = outlineColor, radius = radius, // center = this.center -- default uses center of DrawScope style = Stroke(outlineWidthPx), )
repeat(12) { hourTick -> rotate(hourTick * 30f) { val start = center - Offset(0f, radius) val end = start + Offset(0f, hourTickLengthPx) drawLine( color = hourTickColor, start = start, end = end, strokeWidth = outlineWidthPx, ) } }
val minuteRatio = minute/60f val hourRatio = (hour + minuteRatio) / 12f // draw minute hand first in case it overlaps the smaller hour hand rotate(minuteRatio * 360) { drawLine( color = minuteHandColor, start = center - Offset(0f, radius*0.9f), end = center, strokeWidth = outlineWidthPx, ) } rotate(hourRatio * 360) { drawLine( color = hourHandColor, start = center - Offset(0f, radius*0.6f), end = center, strokeWidth = outlineWidthPx, ) }
// give the hour hand a pin to look nicer drawCircle( color = hourHandColor, radius = outlineWidthPx, style = Fill, )
} } }
CHANGED: app/src/main/java/com/androidbyexample/composeclock/MainActivity.kt
package com.androidbyexample.composeclock

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.androidbyexample.composeclock.ui.theme.ComposeClockTheme
import kotlinx.coroutines.delay
import kotlin.math.min

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeClockTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
// Greeting( // name = "Android", // modifier = Modifier.padding(innerPadding) // ) // } // } // } // } //} var hour by remember { mutableIntStateOf(0) } var minute by remember { mutableIntStateOf(0) } //@Composable //fun Greeting(name: String, modifier: Modifier = Modifier) { // Text( // text = "Hello $name!", // modifier = modifier // ) //} LaunchedEffect(true) { // because true doesn't change, we never restart this coroutine while(true) { minute++ if (minute > 59) { minute = 0 hour++ } delay(10) } }
//@Preview(showBackground = true) //@Composable //fun GreetingPreview() { // ComposeClockTheme { // Greeting("Android") AnalogClock( hour = hour, minute = minute, modifier = Modifier .fillMaxSize() .padding(innerPadding), )
} } } } }