Jetpack / Kotlin / KTX

KTX: Graphics

KTX is a series of Kotlin extension functions for Android that first appeared in February 2018. They can simplify many repetitive tasks or those which require boilerplate code. However, they are not always easy to discover. I find it useful to periodically scan through the list here. I find this a good way of discovering extensions that I wasn’t aware of. So in this post, we’ll look at some that I find particularly useful.

Bitmap

In the previous article we looked at how we can render a View directly to a Bitmap using a KTX extension. There are also some extensions for Bitmap that can make life easier for us.

There are a few variants of createBitmap() which, as the name suggests, are helpers for creating Bitmap instances. The simplest form creates an ARGB_8888 Bitmap of the specified dimensions:

val bitmap = createBitmap(300, 300)

To draw directly in to a Bitmap we need to use a Canvas. Typically we need to do something like this:

val bitmap = createBitmap(300, 300)
val canvas = Canvas(bitmap)

We can now draw to the canvas, and this gets rendered to the Bitmap.

There is a KTX extension function which simplifies this:

val bitmap = createBitmap(300,300)
bitmap.applyCanvas {
}

At first glance, this may not appear to offer many benefits. However, the lambda has a receiver of Canvas meaning that within the lambda body we can call methods directly on the Canvas:

val bitmap = createBitmap(300, 300)
bitmap.applyCanvas {
    drawLine(0f, 0f, 10f, 0f, paint)
}

There are also operator functions to get and set the values of individual pixels. These are wrappers around getPixel() and setPixel(), but mean that we can go from code like this:

val bitmap = createBitmap(300, 300)
@ColorInt val colour: Int = bitmap.getPixel(50, 50)
...
bitmap.setPixel(50, 50, anotherColour)

To something more like this:

val bitmap = createBitmap(300, 300)
@ColorInt val colour: Int = bitmap[50, 50]
...
bitmap[50, 50] = anotherColour

There are also extensions to determine whether a given Point or PointF is within the bounds of the Bitmap.

Last, but certainly not least, is an extension to scale an existing Bitmap:

val bitmap = createBitmap(300, 300)
val scaledBitmap = bitmap.createScaledBitmap(150, 150)

Canvas

There are a number of Canvas extensions which all operate in roughly the same way. They are scoping blocks which surround operations which alter the Canvas state. For example, consider the following:

private fun Canvas.drawSomething() {
    val saveId = save()
    rotate(45f)
    drawLine(0f, 0f, 10f, 0f, paint)
    restoreToCount(saveId)
}

The rotate() alters the state of the Canvas for all drawing operations which follow it. If we wrap inside a save() and restore*() pair, as we have in the example, then we restore the Canvas back to its previous state after restoreToCount(). In other words, the rotate() only has an effect from the point at which it is invoked, until the restore().

KTX has a simplification of this pattern:

private fun Canvas.drawSomething() {
    withRotation(45f) {
        drawLine(0f, 0f, 10f, 0f, paint)
    }
}

The withRotation() extension wraps the lambda clock in a save() and restore() pair. It effectively scopes the rotation to just the lambda block.

I have kept this example simple, but there are some default argument values in operation here and we can also override the pivot point for the rotation though additional arguments.

There are a number of with*() extensions which apply this same pattern for applying clipping, a Matrix, a scale, a skew, and a translation. The arguments for each vary depending on the needs of each operation.

Point, PointF, Rect, RectF, Region

There are extensions for all of these basic types too numerous to document in full here. However, these generally offer some useful extensions to convert between types and also manipulate then.

They all have destructuring operators to allow things like this:

val point = Point(10, 10)
val (x, y) = point

There are also various mathematical operators defined which enable us to perform common mathematical operations in short form:

val point1 = Point(10, 10)
val point2 = Point(20, 20)
val point3 = point1 + point2          // will evaluate to Point(30, 30)

Matrix

There are some construction helpers for common kinds of matrices: translationMatrix(), scaleMatrix(), and rotationMatrix().

Rather than having to do something like this:

val matrix = Matrix().apply {
    setRotation(45f)
}

We can simplify to this by using the KTX extension:

val matrix = rotationMatrix(45f)

Another really useful extension is the times() operator. This allows us to multiple two matrices together:

val matrix3 = matrix1 * matrix2

The final KTX extension for Matrix allows us to obtain the values from the Matrix as 9 Float values in a FloatArray:

val values: FloatArray = matrix1.values()

Path

Most of the Path extensions are concerned with combining two paths in various ways. There are operators for plus() and minus() which allow us to do this:

val path3 = path1 + path2
val path4 = path1 - path2

There are also infix extension functions for logical operations and(), or(), and xor() which allow us to do this:

val path5 = path1 and path2
val path6 = path1 or path2
val path7 = path1 xor path2

Color, Int, Long

There are destructuring declarations for each of Color, Int, or Long. Assuming an ARGB colour space, we can do this:

val (a, r, g, b) = Color(...)

There are also extensions to convert colours represented each of these types to alternate colour spaces specifying the destination colour space using ColorSpace or ColorSpace.Named.

There are also extensions to convert colours between these types.

Another useful extension is to parse a String to a colour Int:

@ColorInt val col = "#8FFF0000".toColorInt()

PorterDuff.Mode

The final category of extensions that we’ll look at is when using Porter Duff alpha compositing modes. We normally need to construct a PorterDuffXfermode as follows:

val xferMode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)

The KTX extension simplifies this to:

val xferMode = PorterDuff.Mode.DST_OUT.toXfermode()

Similarly we the traditional construction of a PorterDuffColorFilter is done like this:

val colourFilter = PorterDuffColorFilter(colour, PorterDuff.Mode.DST_OUT)

With KTX the KTX extension this is simplified to:

val colourFilter = PorterDuff.Mode.DST_OUT.toColorFilter(colour)

Conclusion

This has not been an exhaustive detailing of the KTX extensions for the Android graphics APIs but there are quite a lot of them. They are mostly pretty straightforward, so I have not gone into too much detail. However, combining these can have a significant impact on the size and readability of code performing complex drawing operations.

I usually publish a GitHub repo containing the working code for my blog posts. However, in this case, the individual code snippets pretty much stand up on their own, so I haven’t published a repo in this case.

© 2020 – 2021, Mark Allison. All rights reserved.

Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.