Blog Infos
Author
Published
Topics
, , , ,
Published

Hello Folks,

  • Jetpack Compose is seriously taking over, and it’s only getting bigger! Today, we’re about to create something awesome — our own custom gauge speedometer using canvas in Compose. Cool, right?
  • Let’s jump in and design this whole thing from the ground up! 🔥How are we going to do that?

Alright, let’s get into it! To create a custom needle in Jetpack Compose, there are two ways you can roll:

  1. Canvas Magic: Draw the needle directly using Canvas—completely custom!
  2. Image Spin: Use an image as the needle, find its center, and rotate it based on the percentage.

In this blog, we’re sticking with the custom needle using the canvas approach, but don’t worry — I’ll also share the code for the image method, where you can lock in the needle’s center point to keep it steady during rotation.

So, how do we build the needle using canvas? We’ll create a Path for the needle right inside the canvas.

Let’s jump into the code.

Canvas(modifier = Modifier.fillMaxSize()) {
    val sweepAngle = 240f
    val height = size.height
    val width = size.width
    val startAngle = 150f

    val centerOffset = Offset(width / 2f, height / 2.09f)
  
    drawCircle(Color.White, 24f, centerOffset)

    // Calculate needle angle based on inputValue
    val needleAngle = (meterValue / 100f) * sweepAngle + startAngle
    val needleLength = 160f // Adjust this value to control needle length
    val needleBaseWidth = 10f // Adjust this value to control the base width


    val needlePath = Path().apply {
        // Calculate the top point of the needle
        val topX = centerOffset.x + needleLength * cos(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )
        val topY = centerOffset.y + needleLength * sin(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )

        // Calculate the base points of the needle
        val baseLeftX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseLeftY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseRightX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )
        val baseRightY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )

        moveTo(topX, topY)
        lineTo(baseLeftX, baseLeftY)
        lineTo(baseRightX, baseRightY)
        close()
    }

    drawPath(
        color = Color.White,
        path = needlePath
    )
}
  • If you copy and paste this code, this is how it’s going to look like

Now let’s give this needle some flair with a background and throw in a light gradient to make it pop!

Canvas(modifier = Modifier.fillMaxSize()) {
    val sweepAngle = 240f
    val height = size.height
    val width = size.width
    val startAngle = 150f

    val centerOffset = Offset(width / 2f, height / 2.09f)
    drawCircle(
        Brush.radialGradient(
            listOf(
                innerGradient.copy(alpha = 0.2f),
                Color.Transparent
            )
        ), width / 2f
    )
    drawCircle(Color.White, 24f, centerOffset)

    // Calculate needle angle based on inputValue
    val needleAngle = (meterValue / 100f) * sweepAngle + startAngle
    val needleLength = 160f // Adjust this value to control needle length
    val needleBaseWidth = 10f // Adjust this value to control the base width


    val needlePath = Path().apply {
        // Calculate the top point of the needle
        val topX = centerOffset.x + needleLength * cos(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )
        val topY = centerOffset.y + needleLength * sin(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )

        // Calculate the base points of the needle
        val baseLeftX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseLeftY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseRightX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )
        val baseRightY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )

        moveTo(topX, topY)
        lineTo(baseLeftX, baseLeftY)
        lineTo(baseRightX, baseRightY)
        close()
    }

    drawPath(
        color = Color.White,
        path = needlePath
    )
}
  • After adding a background with drawCircle, it’s going to look something like the image below.

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

Now let’s give this needle some flair with a background and throw in a light gradient to make it pop!

Canvas(modifier = Modifier.fillMaxSize()) {
    val sweepAngle = 240f
    val height = size.height
    val width = size.width
    val startAngle = 150f

    val centerOffset = Offset(width / 2f, height / 2.09f)
    drawCircle(
        Brush.radialGradient(
            listOf(
                innerGradient.copy(alpha = 0.2f),
                Color.Transparent
            )
        ), width / 2f
    )
    drawCircle(Color.White, 24f, centerOffset)

    // Calculate needle angle based on inputValue
    val needleAngle = (meterValue / 100f) * sweepAngle + startAngle
    val needleLength = 160f // Adjust this value to control needle length
    val needleBaseWidth = 10f // Adjust this value to control the base width


    val needlePath = Path().apply {
        // Calculate the top point of the needle
        val topX = centerOffset.x + needleLength * cos(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )
        val topY = centerOffset.y + needleLength * sin(
            Math.toRadians(needleAngle.toDouble()).toFloat()
        )

        // Calculate the base points of the needle
        val baseLeftX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseLeftY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle - 90).toDouble()).toFloat()
        )
        val baseRightX = centerOffset.x + needleBaseWidth * cos(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )
        val baseRightY = centerOffset.y + needleBaseWidth * sin(
            Math.toRadians((needleAngle + 90).toDouble()).toFloat()
        )

        moveTo(topX, topY)
        lineTo(baseLeftX, baseLeftY)
        lineTo(baseRightX, baseRightY)
        close()
    }

    drawPath(
        color = Color.White,
        path = needlePath
    )
}
  • After adding a background with drawCircle, it’s going to look something like the image below.

  • We have added our two arcs, 1 arc with a full swipe, and the other based on our percentage value.

This is what our final code will look like.

@Composable
fun ProtectionMeter(
    modifier: Modifier = Modifier,
    inputValue: Int,
    trackColor: Color = Color(0xFFE0E0E0),
    progressColors: List<Color>,
    innerGradient: Color,
    percentageColor: Color = Color.White
) {

    val meterValue = getMeterValue(inputValue)
    Box(modifier = modifier.size(196.dp)) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val sweepAngle = 240f
            val fillSwipeAngle = (meterValue / 100f) * sweepAngle
            val height = size.height
            val width = size.width
            val startAngle = 150f
            val arcHeight = height - 20.dp.toPx()

            drawArc(
                color = trackColor,
                startAngle = startAngle,
                sweepAngle = sweepAngle,
                useCenter = false,
                topLeft = Offset((width - height + 60f) / 2f, (height - arcHeight) / 2f),
                size = Size(arcHeight, arcHeight),
                style = Stroke(width = 50f, cap = StrokeCap.Round)
            )

            drawArc(
                brush = Brush.horizontalGradient(progressColors),
                startAngle = startAngle,
                sweepAngle = fillSwipeAngle,
                useCenter = false,
                topLeft = Offset((width - height + 60f) / 2f, (height - arcHeight) / 2),
                size = Size(arcHeight, arcHeight),
                style = Stroke(width = 50f, cap = StrokeCap.Round)
            )
            val centerOffset = Offset(width / 2f, height / 2.09f)
            drawCircle(
                Brush.radialGradient(
                    listOf(
                        innerGradient.copy(alpha = 0.2f),
                        Color.Transparent
                    )
                ), width / 2f
            )
            drawCircle(Color.White, 24f, centerOffset)

            // Calculate needle angle based on inputValue
            val needleAngle = (meterValue / 100f) * sweepAngle + startAngle
            val needleLength = 160f // Adjust this value to control needle length
            val needleBaseWidth = 10f // Adjust this value to control the base width


            val needlePath = Path().apply {
                // Calculate the top point of the needle
                val topX = centerOffset.x + needleLength * cos(
                    Math.toRadians(needleAngle.toDouble()).toFloat()
                )
                val topY = centerOffset.y + needleLength * sin(
                    Math.toRadians(needleAngle.toDouble()).toFloat()
                )

                // Calculate the base points of the needle
                val baseLeftX = centerOffset.x + needleBaseWidth * cos(
                    Math.toRadians((needleAngle - 90).toDouble()).toFloat()
                )
                val baseLeftY = centerOffset.y + needleBaseWidth * sin(
                    Math.toRadians((needleAngle - 90).toDouble()).toFloat()
                )
                val baseRightX = centerOffset.x + needleBaseWidth * cos(
                    Math.toRadians((needleAngle + 90).toDouble()).toFloat()
                )
                val baseRightY = centerOffset.y + needleBaseWidth * sin(
                    Math.toRadians((needleAngle + 90).toDouble()).toFloat()
                )

                moveTo(topX, topY)
                lineTo(baseLeftX, baseLeftY)
                lineTo(baseRightX, baseRightY)
                close()
            }

            drawPath(
                color = Color.White,
                path = needlePath
            )
        }

        Column(
            modifier = Modifier
                .padding(bottom = 5.dp)
                .align(Alignment.BottomCenter), horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "$inputValue %", fontSize = 20.sp, lineHeight = 28.sp, color = percentageColor)
            Text(text = "Percentage", fontSize = 16.sp, lineHeight = 24.sp, color = Color(0xFFB0B4CD))
        }

    }
}

private fun getMeterValue(inputPercentage: Int): Int {
    return if (inputPercentage < 0) {
        0
    } else if (inputPercentage > 100) {
        100
    } else {
        inputPercentage
    }
}
  • This is what our final product will look like.

  • We’ve wrapped up the logic in the getMeterValue function, ensuring that the swipe stays within the limits. You can easily customize the arc’s height, width, and color—everything is set up for you.

If you have any questions, just drop a comment, and I’ll get back to you ASAP. We’ll dive deeper into Jetpack Compose soon. Until then, happy coding!

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu