Custom Speedometer View | Jetpack Compose Canvas | Android Studio | Kotlin

Today the modern cars have the analog style speedometer but showing in digital way. In this article we learn how to make a custom speedometer view using Jetpack Compose Canvas. You can use this view at multiple places like showing the progress of any data in your app like showing the intensity of brightness, strength of network and value of any sensor of your android device.

This speedo meter has similar design as our analog clock but instead of showing text around full circle we will only show from 90 to 240 degree angle. First create a speedometer view function which will take following args:

  1. currentSpeed : Float – this will animate the speed hand as per the value
  2. modifier : Modifier – modifying property from outside

First we will draw a progress bar like arc whose progress will moves with the hand speed. So for this we need to show two arc one above the other where secondary will show the path for the primary arc to take as you see in all conventional progress bars. Also the color of our primary arc will be depend on :\

  1. speed < 80 -> Color will be grean(Good)
  2. speed < 160 -> Color will be yellow(Caution)
  3. else -> Color will be red (Danger)
val circleRadius = size.height / 2

val mainColor = when {
    currentSpeed < 80 -> Color.Green
    currentSpeed < 160 -> Color.Yellow
    else -> Color.Red
}

//secondary arc below main arc
drawArc(
    color = Color.LightGray,
    startAngle = 30f,
    sweepAngle = -240f,
    useCenter = false,
    style = Stroke(width = 5.0.dp.toPx())
)

//main arc above secondary arc
drawArc(
   color = mainColor,
   startAngle = 150f,
   sweepAngle = currentSpeed,
   style = Stroke(
        width = 5.0.dp.toPx(),
        cap = StrokeCap.Round
   ),
   useCenter = false
)

Now we will loop from 0 to 240 with step of 2 to draw our markers. With each iteration as per the value find the width and length of the marker and also define the start and end offset of the line we gonna draw for the marker.

We also need a function to find the offset of the point on the circle perimeter.

private fun calculateOffSet(
    degrees: Double,
    radius: Float,
    center: Offset
): Offset {
    val x = (radius * cos(degrees) + center.x).toFloat()
    val y = (radius * sin(degrees) + center.x).toFloat()

    return Offset(x, y)
}

Now code for the markers

val angleInRad =
    Math.toRadians(speed + 150.0)  //to start drawing at 150 degree angle anticlockwise

val lineLength = if (speed % 20 == 0) {
    circleRadius - 50f
} else {
    circleRadius - 40f
}

val lineThickness = if (speed % 20 == 0) {
    5f
} else if (speed % 10 == 0) {
    2f
} else {
    1f
}

val startOffset = calculateOffSet(
    angleInRad, circleRadius - 20f, center
)

val endOffset = calculateOffSet(
    angleInRad, lineLength, center
)

//draw all markers
drawLine(
     color = Color.Black,
     start = startOffset,
     end = endOffset,
     strokeWidth = lineThickness.dp.toPx()
)

Then we will draw our speed text only at speed is multiple of 20. We will first measure the width and height of the text and then after finding a certain angle for the text we will draw using drawText function.

//draw texts only if speed is multiple of 20
if (speed % 20 == 0) {
    val textMarker = textMeasurer.measure(
        text = speed.toString(),
        style = TextStyle.Default.copy(fontSize = 15.sp)
    )
    val textWidth = textMarker.size.width
    val textHeight = textMarker.size.height

    val textOffset = calculateOffSet(
        angleInRad, circleRadius - 90f, center
    )

    //draw speed Text
    drawText(
        textMarker,
        color = Color.Black,
        topLeft = Offset(
            textOffset.x - textWidth / 2,
            textOffset.y - textHeight / 2
        )
    )
}

We will show the speed bellow the speed hand to show precise speed. so lets write code for that

val textBottom = textMeasurer.measure(
    text = currentSpeed.toInt().toString(),
    style = TextStyle.Default.copy(fontSize = 25.sp)
)

//draw bottom text
drawText(
    textBottom, Color.Black,
       Offset(
          size.width.times(0.45f),
          size.height.times(.6f)
       )
)

After this we need to draw two things first one is the speed hand then a circle at its starting. We can show a line for the speed hand but to learn more we will draw a custom path like triangle shape that you actually see in the vehicle speedometer. For that we need to define a path and then rotate it along center will do the work for us.

//it is the inner circle for indicator base
drawCircle(
     radius = 40f,
     center = center
)

//draw indicator
val indicatorOffset = calculateOffSet(
    0.0, circleRadius - 20f, center
)

val indicatorPath = Path().apply {
    moveTo(center.x, center.y)
    // move bottom
    lineTo(center.x, center.y - 20f)
    // move left
    lineTo(indicatorOffset.x, indicatorOffset.y)
    //move right
    lineTo(center.x, center.y + 20f)
    close()
}

rotate(currentSpeed + 150, center) {
    drawPath(indicatorPath, Color.Red.copy(0.7f))
}

GitHub gist for this video code : here

Leave a Comment

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

Scroll to Top