Leveraging the Android framework and Kotlin to make Canvas text drawing more powerful
Canvas offers a variety of drawing functions for implementing custom graphics in your app. A common use of
Canvas is to draw text to a given region of a custom
Canvas has existing functions that allow you to draw text, the simplest of which can be seen below:
canvas.drawText(**text**, x, y, **paint**)
A single line of text is drawn at a given (x, y) origin, taking into account the properties of the
Paint (to describe the colors and styles for the drawing eg.
textSize, etc.). Other variants of this function exist that allow for specification of start and end positions within the text, drawing along a
The existing Canvas text drawing functions are simple and powerful but aren’t without their limitations. The major drawback (as mentioned above) is that the text is drawn on a single line. If the width of the text exceeds the width of the
Canvas, the text will be clipped. Long text will usually need to be drawn on multiple lines, and you may have wanted “paragraph” style text in the first place. From here on, we’ll refer to this as multiline text.
So how should we go about implementing this? Unfortunately you can’t just include
\n characters in your text, as all whitespace characters are interpreted and drawn as spaces within the single line.
Paint includes handy
breakText functions for splitting up text which you could use. You may even consider an existing algorithm such as the Knuth-Plass Line Wrapping Algorithm. This quickly becomes a complex problem.
Thankfully, the Android framework provides us with a class that handles all of the complexity for us:
Layout (in the
android.text package), described as “a base class that manages text layout in visual elements on the screen”. It forms the basis of how classes like
TextView fit text within given layout parameters.
The documentation stipulates:
Considering that we are trying to draw some static (multiline) text to Canvas,
StaticLayout is just what we need!
StaticLayout is quite simple. Firstly, instantiate one by obtaining and using a
A few parameters are required when obtaining the builder:
text: The text
CharSequenceto be laid out, optionally with spans
start: The index of the start of the text
end: The index + 1 of the end of the text
TextPaintused for layout
width: The bounding width (in pixels)
These parameters allow
StaticLayout to layout the text appropriately within the bounding
width. A resultant
height property becomes available once the
StaticLayout has been instantiated. Many other parameters can be appended to the builder to adjust the end appearance, but we’ll get to those later.
_StaticLayout.Builder_ was added in API Level 23. Prior to this, you need to use the
_StaticLayout_ constructors. To add to this, the constructors have been deprecated in API Level 28. Be sure to handle backwards compatibility appropriately.
Note: If you happen to be utilising this in the
_onDraw_ function of a custom
_View_, be sure to instantiate the
_StaticLayout_ separately to avoid object allocation during drawing (in a constructor or Kotlin
_init_ block, for example).
Before we progress, there’s one thing that needs to be discussed… What is
TextPaint? While almost all
Canvas functions require a
StaticLayout requires a
TextPaint. From the documentation, it is described as “an extension of Paint that leaves room for some extra data used during text measuring and drawing”. A great explanation can be found here. In short, you use it exactly as you would
Paint and don’t have to worry about the extra data it includes (for the purpose of what we are doing).
Phew! We now know the basics of what
StaticLayout is and how we can use it to draw multiline text to
Canvas. However, there are a lot of parameters that you can provide to change the appearance of the end result.
Consider a basic text editor: you usually have options to change the alignment, margins, line spacing, text size and more. The same is true for
StaticLayout; additional parameters can be appended to the
It is important to note that some parameters (like
color) do not belong directly to
StaticLayout, but rather belong to the
TextPaint. Let’s take a look at the some of the options we have:
The alignment of the text, similar to gravity.
The horizontal direction that the text follows.
The spacing between lines of text (includes
Options to justify the text (stretch spaces so that the lines appear “square”).
By default, calling
staticLayout.draw(canvas) will draw the entire block of text (from its top left corner) at position (0, 0) on the
Canvas. This is fine for simple use cases, but we may want to position the text elsewhere (as we are able to do with the default
Canvas text drawing methods).
We know that we define the bounding
width of this block, and that we get a resultant
height once we’ve instantiated the
StaticLayout. We can use these properties along with a basic
Canvas translation to position the text.
To do this, we declare a
StaticLayout extension function. It makes use of the handy
Canvas.withTranslation function found in the Android KTX library:
There may be multiple places in an app in which we need to implement multiline text drawing. Instantiating a
StaticLayout in every place would lead to unnecessary bloat. So let’s make an extension function for
This extension function includes most (not all) of the
StaticLayout properties, and provides default values for those that may not be used as commonly. It also makes use of our previously defined extension function to position the block of text.
We can now draw multiline text to
Canvas in a way that feels very familiar to the existing text drawing functions:
drawMultilineText extension function is great to use, but it’s doing something it shouldn’t: it instantiates a new
StaticLayout every time it is called. This violates our goal to avoid object allocation during drawing.
There’s most likely a variety of ways to solve the aforementioned issue. One approach is to implement a basic
LruCache to store/retrieve
StaticLayouts for drawing (again making use of the Android KTX library for the
lruCache extension function):
Now, we only instantiate a
StaticLayout on first use of the
drawMultilineText function (and put it in the cache), otherwise we get one from the cache:
The final piece of the puzzle is deciding what to use as the
StaticLayout.toString() comes to mind, but this means we would need to instantiate it first, which we don’t want to do if there’s a cached version. Given that the parameters of the
drawMultilineText function essentially describe the uniqueness of a
StaticLayout for our purposes, we can create our own key like so:
The final implementation provides us with an idiomatic (and, hopefully, performant) way of drawing multiline text to
Canvas, which feels at home amongst other
Canvas functions. It also includes full backwards compatibility and all of the available
StaticLayout properties. Using Kotlin extension functions, named parameters and operator overloading has greatly reduced the amount of code and made the end result easier to use.
I hope this post has provided some insight into
StaticLayout and how it can be used for multiline text drawing on
Canvas. If you have any questions, thoughts or suggestions then I’d love to hear from you!