Drawing multiline text to Canvas on Android

Leveraging the Android framework and Kotlin to make Canvas text drawing more powerful

Nick Rout
Over Engineering
Published in
7 min readApr 6, 2018

--

Featured in Android Weekly Issue #304

The Android 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 View, Drawable, Bitmap, etc.

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. color, textSize, etc.). Other variants of this function exist that allow for specification of start and end positions within the text, drawing along a Path, etc.

The limitations of Canvas text drawing 🤨

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.

How do we draw 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 measureText and 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.

Android framework to the rescue 🚀

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:

For text that will be edited, use a DynamicLayout, which will be updated as the text changes. For text that will not change, use a StaticLayout.

Considering that we are trying to draw some static (multiline) text to Canvas, StaticLayout is just what we need!

Using StaticLayout 👨‍💻

Using StaticLayout is quite simple. Firstly, instantiate one by obtaining and using a StaticLayout.Builder:

val staticLayout = StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.build()

A few parameters are required when obtaining the builder:

  • text: The text CharSequence to 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
  • textPaint: The TextPaint used 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.

Note: 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.

Then draw!

staticLayout.draw(canvas)

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).

A word on TextPaint ☝️

Before we progress, there’s one thing that needs to be discussed… What is TextPaint? While almost all Canvas functions require a Paint parameter, 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).

Exploring the StaticLayout properties 🔍

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 StaticLayout.Builder.

It is important to note that some parameters (like color) do not belong directly to StaticLayout, but rather belong to theTextPaint. Let’s take a look at the some of the options we have:

alignment

The alignment of the text, similar to gravity.

textDirection

The horizontal direction that the text follows.

lineSpacing

The spacing between lines of text (includes spacingMult and spacingAdd).

justificationMode (minSdk 26)

Options to justify the text (stretch spaces so that the lines appear “square”).

Other properties include ellipsize, maxLines, indents and more. Check the StaticLayout and StaticLayout.Builder documentation for what’s available.

Positioning multiline text 📍

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:

fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.withTranslation(x, y) {
draw(this)
}
}

Using Kotlin to make it feel familiar 🤓

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 Canvas!

fun Canvas.drawMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
x: Float,
y: Float,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
textDir: TextDirectionHeuristic = TextDirectionHeuristics.LTR,
spacingMult: Float = 1f,
spacingAdd: Float = 0f,
hyphenationFrequency: Int = Layout.HYPHENATION_FREQUENCY_NONE,
justificationMode: Int = Layout.JUSTIFICATION_MODE_NONE) {

val staticLayout = StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setTextDirection(textDir)
.setLineSpacing(spacingAdd, spacingMult)
.setBreakStrategy(breakStrategy)
.setJustificationMode(justificationMode)
.build()

staticLayout.draw(this, x, y)
}

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:

canvas.drawMultilineText(text, textPaint, width, x, y)

But there’s one issue… 😬

Our new 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.

Implementing a StaticLayout cache 💾

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):

object StaticLayoutCache {

private const val MAX_SIZE = 50 // Max number of cached items
private val cache = lruCache<String, StaticLayout>(MAX_SIZE)

operator fun set(key: String, staticLayout: StaticLayout) {
cache.put(key, staticLayout)
}

operator fun get(key: String): StaticLayout? {
return cache[key]
}
}

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:

val staticLayout = StaticLayoutCache[cacheKey] ?:
StaticLayout.Builder.obtain(text, start, end, textPaint, width)
... // Add other properties
.build().apply { StaticLayoutCache[cacheKey] = this }

The final piece of the puzzle is deciding what to use as the cacheKey. Using 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:

val cacheKey = "$text-$start-$end-$textPaint-$width-$alignment-$textDir-$spacingMult-$spacingAdd-$breakStrategy-$justificationMode"

Wrapping it up 🎁

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.

The final implementation. Note: this requires the Android KTX library as a dependency.

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!

Find me on Twitter @ricknout

--

--

Nick Rout
Over Engineering

Principal Android Engineer at GoDaddy | Ex-Google | Google Developer Expert for Android