Categories
android kotlin ui

Android Canvas APIs with Kotlin and KTX

Learn how to use the Android KTX extension functions to clean up Canvas drawing code

Have you ever wanted to write a Custom View on Android but you were too afraid to deal with X, Y translations on a Canvas object? 

Well, working with them got a whole lot easier when using Kotlin and the Android KTX Extension functions provided by the Android Team at Google. 

Drawing on Canvas without KTX 🙀🙅🏽‍♀️

If you want to translate (move) an object you are drawing on a Canvas, how would you go about doing that? You would likely need to do something like the following:

canvas.save()
canvas.translate(200f, 300f)
canvas.drawCircle(...) // drawn on the translated canvas
canvas.restore()

Canvas#save() and Canvas#restore() can be used to save() the current matrix (transformations like translate, rotate etc) and restore() the state of the Canvas back to its original transformations. 

The drawCircle() method that we are calling will be drawn at a translated point on the Canvas. Once restore is called, the Canvas is no longer translated and any further operations on that Canvas after that point will not be translated.

Example of a circle being drawn to a Canvas, the first doesn’t contain a translation, whereas the second has a translation applied

If we wanted to then do something more complex, for instance, draw a path that is scaled up after we have translated, the code would look as follows:

val translateCheckpoint = canvas.save()
canvas.translate(200f, 300f)
canvas.drawCircle(...) // drawn on the translated canvas
val rotateCheckpoint = canvas.save()
canvas.rotate(45f)
canvas.drawRect(...) // drawn on the translated and rotated canvas
canvas.restoreToCount(rotateCheckpoint)
canvas.restoreToCount(translateCheckpoint)
Example of drawing a Rect — Image 1: No transformations — without doing save/restore. Image 2: Translated with same checkpoint as circle. Image 3: Translated and rotated.

To do multiple translations on a Canvas, we would then want to use restoreToCount with the specific checkpoint. This will notify the canvas of the certain checkpoint of the transformations that should be restored. 

We can see that this can easily get out of control and it becomes really difficult to follow what transformations will be applied to a certain draw... call.  

Improving Canvas API calls with Android KTX 😻

To use these extension functions, you need to make sure you are importing the core-ktx dependency in your app level build.gradle file (look here for the latest version):

implementation 'androidx.core:core-ktx:1.0.1'

If we are using KTX, we can simplify the previous draw examples to be the following:

canvas.withTranslate(200f, 300f) {
drawCircle(...)
}

This wraps up this logic in a block, that makes it easier to understand and our code is cleanly separated. We also don’t need to specify the canvas on which to draw on at this point, since the block now has the Canvas in this scope. If we wished to draw certain parts of the Canvas at different points, say we wanted to translate and scale, we can nest the withRotate function inside the first withTranslate block. 

canvas.withTranslate(200f, 300f) {
drawCircle(...) // drawn on the translated canvas
withRotate(45f) {
drawRect(...) // drawn on the translated and rotated canvas
}
}

Now our function calls are clearly separated with parentheses and we can easily see which canvas transformations will be applied to the drawRect function by using these extension functions. 

Diving into the Android KTX implementation 📝

This is one of the extension functions for Canvas defined in the KTX library (original source code can be found here):

/**
* Wrap the specified [block] in calls to [Canvas.save]/[Canvas.translate]
* and [Canvas.restoreToCount].
*/
inline fun Canvas.withTranslation(
x: Float = 0.0f,
y: Float = 0.0f,
block: Canvas.() -> Unit
) {
val checkpoint = save()
translate(x, y)
try {
block()
} finally {
restoreToCount(checkpoint)
}
}

Taking a deeper look into how this extension function works, we can see that the functions wrap up the logic of saving and restoring the canvas. The last parameter is a function and that function (block()) is a function literal with receiver. This sounds complicated but this just means that the Canvas instance then becomes the this scope for that function definition. 

This allows us in the block() function, to call the Canvas methods without having to specify the canvas object directly. For example, we don’t have to call canvas.drawCircle() anymore, we can now just call drawCircle() and the correct Canvas object will be used for that method call. 

The block param is the last parameter of the function (and it is a function itself) so we are able to extract the block function outside of the parentheses. For example, both of the following usages are acceptable:

canvas.withTranslate(200f, 300f, {
drawCircle(...)
})

Outside of parenthesis:

canvas.withTranslate(200f, 300f) {
drawCircle(...)
}

It is worth noting that in Android Studio, the first example here will produce a lint warning to tell you to rather use the second option. 

What a nifty mechanism for cleaning up our Canvas API interactions!

Finally… ✨

We can see how the AndroidX Canvas Extension functions can help improve the readability of our canvas transformation code. There are a few other extension functions for Canvas, some of the others include:

  • Canvas#withScale() 
  • Canvas#withSkew() 
  • Canvas#withMatrix()

Have you found any other useful extension functions in the KTX library? Let me know on Twitter @riggaroo 

Thanks to Nick Rout and Josh Leibstein for reviewing this post.