Analog Clock | Jetpack Compose Canvas | Android Studio | Kotlin

You have seen many types of digital clock pattern everyday in you mobile. Today we will learn how to make a conventional Analog Clock using Jetpack Compose Canvas. This clock will have 3 a hands as you see in our every day wall clock.

First we need some variable for our clock like

  1. currentTimeInMillis – it will keep current time in remember block
  2. textMeasurer – it will be used to draw text on the canvas
  3. circleCenter – define the center of our clock
  4. circleRadius – radius of our clock
var currentTimeInMs by remember {
    mutableLongStateOf(System.currentTimeMillis())
}

val textMeasurer = rememberTextMeasurer()

var circleCenter by remember {
    mutableStateOf(Offset.Zero)
}

val circleRadius = 450f

After this we need a Launched Effect block to keep updating our currentTime variable inside a while loop with delay of 1 sec.

//it will run once
LaunchedEffect(key1 = true) {
    //it will run in loop continue after every one second
    while (true) {
        delay(1000)
        currentTimeInMs = System.currentTimeMillis()
    }
}

Then use our Canvas composable inside a Box and define some variable inside it. Create the calendar instance with our currentTime and get the hour, minute and seconds based on that time. Also define a brush for the center of our clock. Then draw two circle one for out side and other for center with gradient.

//height and width of canvas
val width = size.width
val height = size.height

//exact at the centre of canvas
circleCenter = Offset(x = width / 2f, y = height / 2f)

val date = Date(currentTimeInMs)
val calendar = Calendar.getInstance()
//set the current time in the calendar
calendar.time = date

//now its easy to find hours, min & seconds from calendar
val hours = calendar.get(Calendar.HOUR_OF_DAY)
val minutes = calendar.get(Calendar.MINUTE)
val seconds = calendar.get(Calendar.SECOND)

val brush = Brush.radialGradient(
    listOf(
        Color.White.copy(0.45f),
        Color.Cyan.copy(0.35f),
    )
)

//the outer frame circle
drawCircle(
     style = Stroke(
     width = 15f ),
    brush = brush,
    radius = circleRadius + 7f,
    center = circleCenter
)

//it is the inner circle
drawCircle(
      brush = brush,
      radius = circleRadius,
      center = circleCenter
)

Now we will create a loop for 60 iteration and each time we will draw the markers around the circle perimeter and also show the time text along it. We will show the length of markers as per time like for hours we will show a longer length and size and for minute we will show a shorter bar. The idea is to draw a line with each iteration and set it in particular angle. We need to create the start and end offset so that we can show the line with particular location around the center.

//angle for each line
val angleInDegrees = i * 360f / 60  //6 degree gap b/w each

val angleInRad = Math.toRadians(angleInDegrees.toDouble()) + PI / 2f

//we want main points with long line and other with short lines
//so multiple of 5 will be long line
val lineLength = if (i % 5 == 0) largeLineLength else littleLineLength
val lineThickness = if (i % 5 == 0) 5f else 2f
val lineColor = if (i % 5 == 0) Color.Cyan else Color.Gray

val start = Offset(
    x = (circleRadius * cos(angleInRad) + circleCenter.x).toFloat(),
    y = (circleRadius * sin(angleInRad) + circleCenter.y).toFloat()
)

val end = Offset(
    x = (circleRadius * cos(angleInRad) + circleCenter.x).toFloat(),
    y = (circleRadius * sin(angleInRad) + lineLength + circleCenter.y).toFloat()
)
rotate(
   angleInDegrees + 180,
   pivot = start
) {
    drawLine(
        color = lineColor,
        start = start,
        end = end,
        strokeWidth = lineThickness.dp.toPx(),
        cap = StrokeCap.Butt
    )
}

Now we will show the time text around the clock. For this we need to find some variable like

  1. rotate angle – We need to set the text at a particular angle to all the 12 locations
  2. midOffset – find the center from the circle center and the end of the gradient circle.
  3. textWidth & textHeight – after this with the help of text measurer find the width and height of our texts. We need to find this because each text will occupy different space which shows the text out of sync.
val rotateAngle = if (angleInDegrees == 0f) 360f else angleInDegrees

drawContext.canvas.nativeCanvas.apply {
    if (i % 5 == 0) {
        val midAngle = rotateAngle - 90f
        val midOffSet = Offset(
            x = (cos(Math.toRadians(midAngle.toDouble())) * textCircleRadius + circleCenter.x).toFloat(),
            y = (sin(Math.toRadians(midAngle.toDouble())) * textCircleRadius + circleCenter.y).toFloat()
        )

        //measure text if it have any style like font, size, letter spacing
        val textLayoutResult = textMeasurer.measure(
            text = (rotateAngle / 30).toInt().toString(),
            style = TextStyle.Default.copy(fontSize = 15.sp)
        )
        val textWidth = textLayoutResult.size.width
        val textHeight = textLayoutResult.size.height

        drawText(
            textLayoutResult, color = Color.Black,
            topLeft = Offset(
                midOffSet.x - textWidth / 2,
                midOffSet.y - textHeight / 2
            )
        )
    }

after this we need to draw our 3 hands so lets create an enum class for them.

enum class ClockHands {
    Seconds,
    Minutes,
    Hours
}

Now add these 3 hands inside a list and loop through them and create 3 type of variable for the hands like angle of the hand at any particular time and also the length and width of our hands based on the type of hand

val eachDegree = 360f / 60f  // 6
// to get angle of each hand at a moment
val angleInDegrees = when (clockHand) {
    ClockHands.Seconds -> {
        seconds * eachDegree
    }

    ClockHands.Minutes -> {
        (minutes + seconds / 60f) * eachDegree
    }

    ClockHands.Hours -> {
        //If the time is 3:30:
        //Hours contribution: 60 * 3 = 180 minutes
        //Total minutes = 180 + 30 = 210 minutes
        //Angle = 0.5 * 210 = 105 degrees*/
        (60 * hours + minutes) * 30f / 60f
    }
}

val lineLength = when (clockHand) {
    ClockHands.Seconds -> {
        circleRadius * 0.8f
    }

    ClockHands.Minutes -> {
        circleRadius * 0.7f
    }

    ClockHands.Hours -> {
        circleRadius * 0.5f
    }
}
val lineThickness = when (clockHand) {
    ClockHands.Seconds -> {
        3f
    }

    ClockHands.Minutes -> {
        7f
    }

    ClockHands.Hours -> {
        9f
    }
}

Now find the start and end offset of the line we draw for the hands and show them with a angle around the center

val start = Offset(
    x = circleCenter.x,
    y = circleCenter.y
)

val end = Offset(
    x = circleCenter.x,
    y = lineLength + circleCenter.y
)
rotate(
    angleInDegrees - 180,
    pivot = start
) {
    drawLine(
        color = if (clockHand == ClockHands.Seconds) redOrange else gray,
        start = start,
        end = end,
        strokeWidth = lineThickness.dp.toPx(),
        cap = StrokeCap.Round
    )
}

Then finally draw a circle at the center to hide the hands at center

//center circle
drawCircle(
    color = Color.Cyan,
    radius = 35f,
    center = circleCenter
)

GitHub gist for this video code : here

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top