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.