Exploring text on Canvas using drawText API in Jetpack Compose

Exploring the new DrawScope.drawText() API
Nov 9 2022 · 4 min read

Background

Before Compose 1.3.0, there was no drawText(). We couldn’t draw text directly on the Jetpack Compose canvas, we had to use android native canvas canvas.nativeCanvas.drawTextto draw text.

Recent release of Jetpack Compose 1.3.0 introduces many new APIs, and Text on Canvas is one of them.

In this article, we’ll explore the new DrawScope.drawText() API. Note that this API is still in the experimental state and it is likely to change in the future.

Here’s the full source code if you need it.

Let’s get started!

Old Approach

Before we dive deep into the new API let’s see the old approach to drawing the text.

Here’s the code snippet

@Composable
fun NativeDrawText() {

    val paint = Paint().asFrameworkPaint().apply {
        // paint configuration
    }

    Canvas(modifier = Modifier
        .fillMaxWidth()
        .height(100.dp), onDraw = {
        drawRect(color = Color.Black)

        drawIntoCanvas {
            it.nativeCanvas.drawText("Text on Canvas!",  20f, 200f, paint)
        }
    })
}

And the result

New DrawScope.drawText API

Now it’s time to explore DrawScope.drawText

drawText function has 4 overloads, Let’s see them one by one with examples.

The First two overload takes TextMeasure in an argument, while the other two variant takes TextLayoutResult with other configuration parameters.

Draw text using a TextMeasurer

fun DrawScope.drawText(
    textMeasurer: TextMeasurer,
    text: AnnotatedString,
    // other configuration...
)

TextMeasure is a new Experimental API. TextMeasure is responsible for measuring text layout. To learn more about TextMeasure, check out the official API doc.

With this overload function, we can draw styled text on canvas. With rememberTextMeasurer() we can create an instance of TextMeasurer

rememberTextMeasurer() takes cacheSize as parameters. cacheSize defines the capacity of the internal cache inside TextMeasurer, which means the number of unique text layout inputs that are measured.

Let’s see the example now

@OptIn(ExperimentalTextApi::class)
@Composable
fun ExampleTextAnnotatedString() {

    val textMeasure = rememberTextMeasurer()

    val text = buildAnnotatedString {
        withStyle(
            style = SpanStyle(
                color = Color.White,
                fontSize = 22.sp,
                fontStyle = FontStyle.Italic
            )
        ) {
            append("Hello,")
        }
        withStyle(
            style = SpanStyle(
                brush = Brush.horizontalGradient(colors = RainbowColors),
                fontSize = 28.sp,
                fontWeight = FontWeight.Bold
            )
        ) {
            append("\nText on Canvas️")
        }
    }
    Canvas(modifier = Modifier
        .fillMaxWidth()
        .height(100.dp), onDraw = {
        drawRect(color = Color.Black)

        drawText(
            textMeasurer = textMeasure,
            text = text,
            topLeft = Offset(10.dp.toPx(), 10.dp.toPx())
        )
    })
}

Here we have created textMeasure instance by rememberTextMeasurer() and annotated string with style also set up the topLeft point. Also to style our text we’ve used Brush API to apply gradient coloring.

Nice colorful text on canvas 🤩.


Now let’s see the second overload function 

This draw function supports only one text style and async font loading.

@ExperimentalTextApi
fun DrawScope.drawText(
    textMeasurer: TextMeasurer,
    text: String,
    // Other configuration
)

The signature is the same as the previous overload function, except for the text parameters which take the string instead of Annotated string.

@Composable
fun ExampleTextString() {

    val textMeasure = rememberTextMeasurer()

    Canvas(modifier = Modifier
        .fillMaxWidth()
        .height(100.dp), onDraw = {
        drawRect(color = Color.Black)

        drawText(
            textMeasurer = textMeasure, text = "Text on Canvas!",
            style = TextStyle(
                fontSize = 35.sp,
                brush = Brush.linearGradient(
                    colors = RainbowColors
                )
            ),
            topLeft = Offset(20.dp.toPx(), 20.dp.toPx())
        )
    })
}

And the output

Now let’s see the remaining two overloads which draw an existing text layout on canvas

Draw text using a TextLayoutResult

TextLayoutResult can be generated by textMeasurer.measure()

@ExperimentalTextApi
fun DrawScope.drawText(
    textLayoutResult: TextLayoutResult,
    color: Color = Color.Unspecified,
    topLeft: Offset = Offset.Zero,
    alpha: Float = Float.NaN,
    shadow: Shadow? = null,
    textDecoration: TextDecoration? = null
){}

@ExperimentalTextApi
fun DrawScope.drawText(
    textLayoutResult: TextLayoutResult,
    brush: Brush,
    topLeft: Offset = Offset.Zero,
    alpha: Float = Float.NaN,
    shadow: Shadow? = null,
    textDecoration: TextDecoration? = null
){}

Both overload functions are the same, except for color and brush parameters to color the text.

Let’s first see how to create TextLayoutResult by TextMeasure.measure() Here we’ll use LayoutModifier to create TextLayoutResult.

val textMeasure = rememberTextMeasurer()
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }

Canvas(
    modifier = Modifier
        .layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)

            textLayoutResult = textMeasure.measure(
                AnnotatedString("Text on Canvas!"),
                style = TextStyle(
                    // text styling
                )
            )
            layout(placeable.width, placeable.height) {
                placeable.place(0, 0)
            }
        }
) { //draw }

here, textMeasure.measure takes an annotated string and other configurations as an argument. Currently, there’s no way to pass a simple text string.

Now let’s use the above created textLayoutResult to draw text

@OptIn(ExperimentalTextApi::class)
@Composable
fun ExampleTextLayoutResult() {
    val textMeasure = rememberTextMeasurer()
    var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }

    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
            .layout { measurable, constraints ->
                ...
            }
    ) {
        drawRect(color = Color.Black)

        textLayoutResult?.let {
            drawText(
                textLayoutResult = it,
                alpha = 1f,
                shadow = Shadow(color = Color.Red, offset = Offset(5f, 8f)),
                textDecoration = TextDecoration.Underline
            )
        }

    }
}

Along with textLayoutResult we have also customized our text. Let’s see the result

Easy. isn’t it?

Like Text composable, you can decorate and customize text according to your requirements

For example, you can set overflow when text is too long to fit

@OptIn(ExperimentalTextApi::class)
@Composable
fun ExampleTextOverFlow() {

    val textMeasure = rememberTextMeasurer()

    Canvas(
       onDraw = {
        drawRect(color = Color.Black)

        drawText(
            textMeasurer = textMeasure,
            text = //...some long string,
            overflow = TextOverflow.Ellipsis,
            maxLines = 3
         )
    })
}

Here text is ellipsed to fit in the container

To Conclude

That’s it for today, hope you have a basic idea of drawText API. It’s effortless to use. You can do lots of customization to decorate your text on canvas. Let’s wait for Experimental Tag to be removed from API, till that keep exploring it 🍻.

The complete source code of the above examples is available on Github.

Hope you found this blog post useful.

Thank you!!


radhika-s image
Radhika saliya
Android developer | Sharing knowledge of Jetpack Compose & android development


radhika-s image
Radhika saliya
Android developer | Sharing knowledge of Jetpack Compose & android development


Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.