Exploring Jetpack Compose Canvas: the power of drawing
In this article, I will share my experience of using Canvas with Jetpack Compose, which is the new UI toolkit made by Google. The Android Dev Challenge #2 gave me the opportunity to learn tons of things about Canvas and how to take advantage of it to draw and animate shapes or texts in a very nice way.
Most of the code samples are based on the project below:
TimePack CountPose is a timer application to showcase Jetpack Compose animations with Canvas Composable.
Disclaimer: the code samples are based on Compose 1.0.0-beta02. The API methods might change in a near future.
First steps with Canvas
If you are familiar with the Android View canvas methods you won’t be lost with the one from Jetpack Compose. All function names are same and some of them are even more explicit when dealing the Path API for example: relativeQuadraticBezierTo() instead of rQuadto() to curve a segment of the path.
If you are not familiar with the native Android Canvas, I highly recommend to take a look at this article by Rebecca Franks to have a great introduction to the Android Canvas.
Getting Started with Drawing on the Android Canvas 🖼
Learn how to use the Android Canvas class
With Jetpack Compose, there is a Composable that is part of the UI component library called Canvas to unleash the power of drawing in your application. We are going to draw a smiley face with simple shapes (circle, arc and rectangles) to show its capabilities.
The onDraw lambda on the Canvas give us access to the DrawScope. This scope is allowing us to draw everything we want in the Canvas. Remember that the origin (x=0, y=0) of the Canvas is located at the top left.
To draw the head of the smiley face, we are going to draw a circle with a stroke style. If we let the style empty, it will be filled by default. All draw methods accept a Color or a Brush (used for adding gradients with a list of colors). To set the radius, we have access to the dimension of the current drawing environment with size provided by the the DrawScope in order to compute a scalable radius depending of the size of the component. The center attribute accepts an Offset to set the position of the shape in the canvas.
Afterwards, we draw the arc for the mouth with a single color and we do the same with the rectangles for the eyes. Now, our smiley face is ready to be display on screen:
There are many methods available in the DrawScope to draw on a Canvas, here is a sample of the current functions:
- drawCircle() // draws a circle at given coordinates
- drawArc() // draws an arc scaled to fit inside a given rectangle
- drawImage() // draws an ImageBitmap in the canvas
- drawPoints() // draws a sequence of points
- drawPath() // draws a path with a given color
and many more… Now let’s see how we can animate drawings we added to our Canvas.
Animations with Canvas
Now that we have the basics, let’s see how we can go further and implement and animate more complex drawings. In the Android Dev Challenge #2, I decided to develop a wave animation that translates slowly to the bottom with a dynamic wave width that goes flat at the end the time.
The DrawScope offers a really nice API to directly animate the drawings of the Canvas. You can apply translations, rotations or scale transformations. For the timer wave animation, we are going to the translate transformation in action.
First, we define two types of AnimationState to achieve the targeted animation. First, we set an infinite animation on a float from 0 to 1 in order have an infinite wave effect thanks to rememberInfiniteTransition(). Then we expose the value of the animation using animateFloat() and the associated specification.
For all finite animations, we will work directly with the AnimationState functions like animateFloatAsState(), animateColorAsState()… where can set the target value and define the animation specifications.
Now that we defined the animation states, we can implement the wave animation. To draw wave itself, we are going to use a Path that will allow us to add Bezier curve segments to our path like a sinusoid function. Then, once the drawing is done, we need to wrap it with the translate() lambda provided by the DrawScope and pass the value of the AnimateState to animate the top pixels.
Here is the full animation in action!
Use native Canvas to draw text
At the moment, you cannot draw text directly on the Jetpack Compose canvas. To do so, you have to access the native canvas from the Android framework to draw some text on it. On the onDraw lambda of the Canvas component, you can call the function drawIntoCanvas to access the underlying Canvas with nativeCanvas (quite helpful if you can to reuse some draw logic you implemented in previous Android apps). Then, you can call all the methods related to the native Canvas like drawText() or drawVertices() for example.
To apply a style to the text, a Paint object must be used. As we are using the native canvas, we cannot use Paint from Jetpack Compose directly with the drawText() function. To obtain a native Paint instance we can call the method asFrameworkPaint() in order to deal with an android.graphics.Paint.
Here is a code snippet that shows how to draw a simple text on the native canvas:
And here is what it looks like on the sample app:
You can use all the Jetpack Compose Canvas transformations (translate, rotate, scale…) and wrap drawIntoCanvas so you can add animations on what you drawn.
Drawing on the canvas may unlock many design possibilities! You can add simple very easily but you may end up implementing complex mathematics algorithms if you want to implement complex path drawings. The Jetpack Compose Canvas has a lot to offer so use it wisely.
Big thanks to Annyce Davis for the review and the great feedbacks. 👏
Do not hesitate to ping me on Twitter if you have any question 🤓