Explorando texto no Canvas usando a API drawText no Jetpack Compose

Tempo de leitura: 4 minutes

Antes do Compose 1.3.0, não havia drawText(). Não podíamos desenhar o texto diretamente na tela do Jetpack Compose, tínhamos que usar o canvas nativo do Android canvas.nativeCanvas.drawText para desenhar o texto.

A versão recente do Jetpack Compose 1.3.0 introduziu muitas APIs novas, e Text on Canvas é uma delas.

Neste artigo, exploraremos a nova API DrawScope.drawText(). Observe que essa API ainda está em estado experimental e é provável que seja alterada no futuro.

Antes de nos aprofundarmos na nova API, vamos ver a abordagem antiga para desenhar o texto.

Aqui está o trecho de código

@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)
        }
    })
}

E o resultado

Agora é hora de explorar o DrawScope.drawText

A função drawText tem 4 sobrecargas, vamos vê-las uma a uma com exemplos.

As duas primeiras sobrecargas recebem TextMeasure em um argumento, enquanto as outras duas variantes recebem TextLayoutResult com outros parâmetros de configuração.

 

Desenhar texto usando um TextMeasurer

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

TextMeasure é uma nova API experimental. A TextMeasure é responsável por medir o layout do texto. Para saber mais sobre o TextMeasure, consulte o documento oficial da API.

Com essa função de sobrecarga, podemos desenhar texto com estilo na tela. Com a função rememberTextMeasurer(), podemos criar uma instância do TextMeasurer

O cacheSize define a capacidade do cache interno dentro do TextMeasurer, o que significa o número de entradas de layout de texto exclusivas que são medidas.

Vamos ver o exemplo agora

@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())
        )
    })
}

Aqui, criamos a instância textMeasure por meio da função rememberTextMeasurer() e anotamos a string com o estilo, além de configurar o ponto topLeft. Além disso, para estilizar nosso texto, usamos a API Brush para aplicar uma coloração gradiente.

Belo texto colorido em tela 🤩.

 

Agora vamos ver a segunda função de sobrecarga

Essa função de desenho suporta apenas um estilo de texto e carregamento de fonte assíncrono.

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

A assinatura é a mesma da função de sobrecarga anterior, exceto pelos parâmetros de text que recebem a string em vez da string anotada.

@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())
        )
    })
}

E a saída

 

Agora vamos ver as duas sobrecargas restantes que desenham um layout de texto existente na tela

 

Desenhar texto usando um TextLayoutResult

O TextLayoutResult pode ser gerado por 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
){}

Ambas as funções de sobrecarga são as mesmas, exceto pelos parâmetros de cor e pincel para colorir o texto.

Vamos ver primeiro como criar TextLayoutResult por TextMeasure.measure() Aqui usaremos LayoutModifier para criar 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 }

Aqui, textMeasure.measure usa uma cadeia de caracteres anotada e outras configurações como argumento. Atualmente, não há como passar uma string de texto simples.

Agora vamos usar o textLayoutResult criado acima para desenhar o texto

@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
            )
        }

    }
}

Junto com textLayoutResult, também personalizamos nosso texto. Vamos ver o resultado

É fácil, não é?

 

Como o Text composable, você pode decorar e personalizar o texto de acordo com suas necessidades

Por exemplo, você pode definir o estouro quando o texto for muito longo para caber

@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
         )
    })
}

Aqui, o texto é elipsado para caber no contêiner

 

Para concluir

Por hoje é só, espero que você tenha uma ideia básica da API drawText. Ela é fácil de usar. Você pode fazer várias personalizações para decorar seu texto na tela. Vamos esperar que a tag Experimental seja removida da API e, até lá, continuar explorando-a 🍻.