Jetpack Compose: How to play with Canvas?

Previously, we looked at how to make a scrollable list and how to change the scroll position programmatically. In this article, we’ll see how to play with the Canvas API inside Jetpack Compose, what we can draw with it and how to animate our custom components.

Disclaimer: All source code is developed with Compose 1.0.0-beta09. Because Jetpack Compose is now in beta, Compose api used in this article should be the same for new versions.

Compose Canvas API

Compose creates an abstract layer on top of Android SDK Canvas to create a Compose version. It allows Compose to be compatible with external platforms and open incredible possibilities for Kotlin Multiplatform Projects (KMP).

If you need the native canvas, you can get it but take care about its usage. When you create a project compatible with more platforms than Android, you need to check if there isn’t any other alternative for your use case.

For this article, we’ll focus on Compose Canvas API and its geometrical shape functions you can use to draw on the screen.

  • drawLine draw a single line between two positions.
  • drawRect draw a rectangle with a position and size.
  • drawRoundedRect draw a rounded rectangle with a position, size and radius for corners.
  • drawArc draw an arc between two degrees.
  • drawCircle draw a circle at the provided center coordinate and radius.
  • drawOval draw an oval with a position and radius.
  • drawPath draw a path with a color.
  • drawPoints draw a list of points.

We’ll use some of these methods to show the Canvas API capabilities! To illustrate this article, we’ll draw a counter component with a circular progress bar and markers around the component which is animated every second. This timer has been developed for the #AndroidDevChallenge 2021.

Time counter developed for #AndroidDevChallenge 2021

Draw the circular progress bar

When you want to draw with the canvas, you have two possibilities: a Canvas component or the Modifier drawBehind on Composable. The second solution is interesting because you can draw something behind the content of a box. We’ll use this option.

The structure of our circular progress bar is a 360 degree arc of a circle behind a second arc that depends on the current timer value. When the timer goes down, we can see the white arc hidden by the purple arc.

The specification of drawArc function is pretty simple. You give a color or a gradient, the start angle (where 0 is positioned at 3 hours), the end angle, a parameter to close the center of the bounds and the style of the arc (where you can choose a filled or a stroke style).

Information: If you want to know all parameters of this function, you can just check its official documentation.

Draw markers

The marker is a little bit touchy and need mathematics knowledge because we want to draw a line around a circular progress bar where the inclination depends on the degree of the marker.

For example, to compute the start point of the marker, first compute the theta which will be used to compute the starter position x and y of the marker.

θ = (angle - 90) * π / 180

Second, compute the radius (r) where the last parameter represent the distance (D) between the center of your component and your marker inside this component.

r = width / 2 * D

Finally, compute the position x and y where you’ll use the theta value and your radius. These positions will be used by drawLine function to draw our marker at the good distance and with the correct radius.

x = cos(θ) * r

y = sin(θ) * r

You can use these formulas to compute the starting point and the destination point of your markers and call the Canvas function to draw your line.

Animate your timer

First, we’ll create a ViewModel with start, pause and stop functions. They will be linked to our buttons under our timer. When the timer is started, the ViewModel will have the responsibility to recompose our UI at each second.

Second, between each recomposition, we’ll animate our circular progress bar and our markers to have something more smooth for our users.

Thanks to coroutines and flows, we have an elegant way to create a loop of 30 seconds, an event at each second to notify a subscriber and a mechanism to cancel our timer.

Create a coroutine context from your ViewModel scope to cancel automatically our timer and iterates as long as your coroutine scope is active or not finished. Finally, for each iteration in this loop, call delay function to wait 1 second.

The _timer variable is typed by a state flow and is used to notify our UI and job variable is typed by Job, which contains our coroutine scope, where you can cancel at any time your timer. Here is complete implementation of our ViewModel.

We have almost all our composables to draw our chrono component. Note that there is also a ChronoBackground component which is just a circular Box with a text to display the timer in seconds.

We’ll see how to combine our custom components but before, we’ll create two variables to animate our circular progress bar and our markers by their input values.

You have so many ways to animate your components. From the official documentation, you can find this useful and complete illustration to guide you in the best choice for your context.

Official decisional tree to choice the best way to animate your UI

In this article, we’ll use animate*AsState where * can be Float, Int, IntOffset, IntSize, Offset, Dp, Rect, Size, Value, Color, Vector or your custom implementation if these provided functions doesn’t suit your needs.

Find here the current signature of animateFloatAsState that will be used for our two variables:

You give the target value at the end of the animation, the specification to change the value through the time, the optional threshold value to decide when the animation value is considered close enough to the target value and a listener if you want to do something when the target value is reached.

If we assume that we have the number of markers (nbMarkers) we want to display and the current value of the timer (seconds), we can compute the angle progress of our circular progress bar and the number of active markers.

It’s time to assemble all our previous work to create one single component to draw our animated timer. Inside a square Box, we draw all our markers, the text in the center and finally, the circular progress bar.

In this post, we’ve taken a quick overview on the Canvas API usage and animations. If you have any questions related to Jetpack Compose, feel free to ping me here or on Twitter and if you want to check the complete code of this timer application, it is available on Github.

Big thanks to Jens Klingenberg and Julien Salvi for reviewing my article. 👏

Software Staff Engineer @Decathlon