Texto En Compose

En este tutorial verás el uso de texto en Compose a partir de la función componible Text. Estudiaremos el propósito de cada uno de sus parámetros, con el fin de cambiar características como tamaño, color, alineación de escritura, fuente, etc.


Ejemplo De Texto En Compose

La idea es explorar el cambio del texto cuando modificamos los argumentos con que es creado en la recomposición. Por lo que crearemos un archivo llamado Text.kt, donde añadiremos múltiples funciones componibles con previsualizaciones para experimentar estos cambios.

Esta serie de ejemplos puedes encontrarlo en el módulo p7_components del proyecto Android Studio de la guía de Jetpack Compose:


La Función Text

A lo largo de los tutoriales anteriores, hemos invocado a Text() gran cantidad de veces para mostrar texto en pantalla de forma básica.

En esta ocasión entraremos en detalle en su firma para conseguir resultados que puedan ser útiles en situaciones que requieran proyectar texto más estilizado.

La siguiente es su definición formal en la documentación de Jetpack Compose:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) 

Veamos como funciona cada uno.


Mostrar Texto En Pantalla

Como ya sabes, el parámetro text recibe un String que será desplegado en la pantalla. Debido a que los demás parámetros tienen valores por defecto, es posible llamar a Text pasando un texto básico como el siguiente:

@Composable
fun ShowText(){
    Text(text = "Texto En Compose")
}
Ejemplo de texto en Compose

También puedes usar un recurso de texto para mostrar texto en pantalla. Para ello cárgalo con la función stringResource() y pasa como argumento el indicador de la clase R:

@Composable
fun ShowTextFromResources() {
    Column {
        Text(stringResource(R.string.string_res))
        Text(stringResource(R.string.format_string_res, 2))
        for (item in stringArrayResource(R.array.string_array_res)) {
            Text(item)
        }
    }
}

La función stringResource() también te permite cargar strings con formato. E incluso tenemos a stringArrayResource() para cargar arrays de strings. El ejemplo anterior usa los siguientes elementos <string> y <array-string>:

<resources>
    <string name="app_name">Texto En Compose</string>

    <string name="string_res">Texto desde strings.xml</string>
    <string name="format_string_res">Cantidad de ítems: %1$s</string>
    <string-array name="string_array_res">
        <item>String 1</item>
        <item>String 2</item>
        <item>String 3</item>
    </string-array>
</resources>
Ejemplo stringResource() en Compose

Cambiar Color De Texto

Pasa una instancia Color al atributo color para cambiar el color de todo el texto:

@Composable
fun TextColor() {
    Text("Color Cyan", color = Color.Cyan)
}

Cambiar Tamaño De Texto

El tamaño de texto en Compose es representado por la clase TextUnit. Las unidades de medida disponibles son: sp, em y Unspecified (toma valor por defecto o hereda del tema aplicado).

Por ejemplo: Mostrar dos textos con tamaños de 20sp y 10em.

@Composable
fun TextSize() {
    Column {
        Text("Texto con 20sp", fontSize = 20.sp)
        Text("Texto con 10em", fontSize = 10.em)
    }
}

Mostrar Texto En Cursiva

Los estilos de fuente son generados por el argumento que le pases a fontStyle. La clase FontStyle define si un texto está en cursiva o normal.

Ejemplo:

@Composable
fun ItalicText() {
    Text("Texto en cursiva", fontStyle = FontStyle.Italic)
}

Mostrar Texto En Negrilla

Controla el grosor de todo el texto con el parámetro fontWeight. Los valores que recibe se encuentran en la clase FontWeight. Puedes usar las propiedades W100 hasta W900 o sus alias Thin hasta Black.

Por ejemplo: Mostrar un texto con el peso numérico 500 y otro con Extra Bold.

@Composable
fun FontWeightText() {
    Column {
        Text("Texto con grosor W500", fontWeight = FontWeight.W500)
        Text("Texto con grosor Extra Bold", fontWeight = FontWeight.ExtraBold)
    }
}
Cambiar grosor de texto en Compose FontWeight

Cambiar Familia Tipográfica

Modifica la familia tipográfica con el parámetro fontFamily a través de la creación de un objeto FontFamily con múltiples componentes TextStyle.

Por ejemplo: Renderizar un texto con la familia tipográfica Besley y varios de sus grosores.

En primer lugar debes descargar los archivos de la fuente en el sitio del creador (Google Fonts para este ejemplo) y luego incluirlos en src/font:

Esto te habilita acceder a la referencias, como R.font.besley_regular, para la creación de la familia:

@Composable
fun FontFamilyText() {
    val besleyFontFamily = FontFamily(
        Font(R.font.besley_regular, FontWeight.Normal),
        Font(R.font.besley_medium, FontWeight.Medium),
        Font(R.font.besley_semibold, FontWeight.SemiBold),
        Font(R.font.besley_bold, FontWeight.Bold),
        Font(R.font.besley_extrabold, FontWeight.ExtraBold),
        Font(R.font.besley_black, FontWeight.Black)
    )
    Column {
        Text(
            "Besley Normal",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.Normal
        )
        Text(
            "Besley Medium",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.Medium
        )
        Text(
            "Besley Semi-bold",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.SemiBold
        )
        Text(
            "Besley Bold",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.Bold
        )
        Text(
            "Besley Extra-bold",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.ExtraBold
        )
        Text(
            "Besley Black",
            fontFamily = besleyFontFamily,
            fontWeight = FontWeight.Black
        )
    }
}

Modificar Espaciado Entre Caracteres

Otra característica que puedes alterar es el espacio que existe entre los caracteres del texto con letterSpacing. Asigna valores de la clase TextUnit con la cantidad que deseas aplicar.

Ejemplo: Contrastar el espacio entre letras de un texto cuando se aplican las cantidades 0.15em y 0.4em:

@Composable
fun LetterSpacingText() {
    Column {
        Text("Texto", letterSpacing = 0.15.em)
        Text("Texto", letterSpacing = 0.4.em)
    }
}

Subrayar/Tachar Un Texto

El parámetro textDecoration te posibilita subrayar o tachar un texto a partir de las propiedades de la clase TextDecoration. Esta define el dibujo de una línea horizontal sobre el texto.

Por ejemplo: Crear un texto subrayado, otro tachado y otro con la combinación de ambas líneas:

@Composable
fun TextDecorationExample() {
    Column {
        Text("Texto", textDecoration = TextDecoration.Underline)
        Text("Texto", textDecoration = TextDecoration.LineThrough)
        Text(
            "Texto",
            textDecoration = TextDecoration.Underline + TextDecoration.LineThrough
        )
    }
}
Ejemplo de TextDecoration en Compose

Justificar Texto O Alinear A Izquierda, Centro O Derecha

Si deseas cambiar la alineación del texto dentro de las líneas de un párrafo usa el parámetro textAlign. Este recibe valores de la clase TextAlign, la cual posee las siguientes propiedades para representar la alienación:

  • Center
  • End
  • Justify
  • Left
  • Right
  • Start

El propósito de Start y End sigue siendo el mismo que hemos visto en el sistema de views. Representan el lado inicial y final sin importar cual sea la dirección de escritura del lenguaje que está siendo usado en la App.

Center alinea el texto en el centro del contenedor y Justify estira las líneas de texto para llenar el ancho del contenedor a través de saltos de líneas automáticos.

Ejemplo: Comparar las diferencias entre un párrafo alineado a la izquierda, al centro, a la derecha y con justificación:

@Composable
fun TextAlignExample() {
    Column {
        val paragraph = stringResource(R.string.paragraph_res)
        val width = Modifier.width(300.dp)
        Text(text = paragraph, textAlign = TextAlign.Start, modifier = width)
        Space()
        Text(text = paragraph, textAlign = TextAlign.Center, modifier = width)
        Space()
        Text(text = paragraph, textAlign = TextAlign.End, modifier = width)
        Space()
        Text(text = paragraph, textAlign = TextAlign.Justify, modifier = width)
    }
}

@Composable
private fun Space() {
    Spacer(modifier = Modifier.size(16.dp))
}
Ejemplo de alineación de texto en Compose

Modificar Altura De La Línea Del Texto

La altura de línea es el área vertical que recubre la línea de un texto. Es decir, la suma entre el área del contenido del texto, más su espacio de salto con respecto a la siguiente:

Ejemplo de lineHeight en Text Compose

Para aumentar o disminuir este valor pasa una instancia TextUnit al parámetro lineHeight de Text.

Ejemplo: Dados tres párrafos, reflejar su presentación cuando con alturas de línea de 20dp y 30dp.

@Composable
fun LineHeightExample() {
    Row {
        Text("LineHeight = 20sp\nPárrafo\nPárrafo", lineHeight = 20.sp)
        Space()
        Text("LineHeight = 30sp\nPárrafo\nPárrafo", lineHeight = 30.sp)
    }
}

Truncar Texto Desbordado

Si el tamaño del texto es tan largo que excede las restricciones impuestas por su contenedor, se producirán desbordamientos que puede que afecten el espacio de otros componentes aledaños.

Para evitar esto, usa el parámetro overflow. Este recibe los siguientes valores de la clase TextOverflow:

  • Clip (valor por defecto): Corta el texto de forma precisa para ajustarlo a su contenedor
  • Ellipsis: Suprime los últimos caracteres del texto con una elipsis para indicar el desbordamiento
  • Visible: Despliega todo el texto incluso si no hay espacio para albergarlo en su contenedor

Ejemplo: Mostrar las diferentes formas de manejar el desbordamiento de un texto con tres líneas.

@Composable
fun OverflowExample() {
    Box(modifier = Modifier.width(300.dp)) {
        Column(Modifier.width(200.dp)) {
            TitleExample("Clip")
            Text(
                "Este texto excede el tamaño de 200dp",
                maxLines = 1
            )
            Space()
            TitleExample("Ellipsis")
            Text(
                "Este texto excede el tamaño de 200dp",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
            Space()
            TitleExample("Visible")
            Text(
                "Este texto excede el tamaño de 200dp",
                overflow = TextOverflow.Visible,
                softWrap = false
            )
        }
    }
}
Ejemplo de overflow para desbordamiento de texto en Compose
Parámetro overflow de Text

Saltos De Línea Automáticos

Cuando Compose renderiza un componente Text cuyo contenido de texto es más largo que el ancho de su contenedor, este es envuelto (soft wrap) a fin de dividir su continuación en la siguiente línea.

Este aspecto está habilitado por defecto con el parámetro booleano softWrap, quien recibe true en la definición de la función. No obstante, si quieres que las líneas de texto se desplieguen como si no existiese límite horizontal, entonces pasa false.

Ejemplo: Evitar que un texto tenga saltos de línea automáticos cuando es presentado en pantalla:

@Composable
fun SoftWrapExample() {
    Row(Modifier.width(200.dp)) {
        Text(
            "Texto largo sin saltos de línea",
            softWrap = false,
            modifier = Modifier.weight(1f)
        )
        Space()
        Text(
            "Otro texto",
            modifier = Modifier.weight(1f)
        )
    }
}
Parámetro softWrap de Text en Compose

Limitar Cantidad De Líneas De Un Texto

El número de líneas de texto que aceptará un componente Text es definido por el valor del atributo maxLines. Pasa valores enteros mayores que cero para limitar el máximo de líneas que requieres en tu texto.

Ejemplo: Permitir máximo tres líneas en un texto.

@Composable
fun MaxLinesExample() {
    Column {
        TitleExample("maxLines = 3")
        Text("Párrafo\n".repeat(4), maxLines = 3)
    }
}
Ejemplo de parámetro maxLines de Text en Compose

Realizar Acciones Cuando El Texto Se Muestra

Existe otro parámetro de tipo función (TextLayoutResult) -> Unit llamado onTextLayout, que te permite especificar sentencias una vez el layout del texto ha sido calculado.

La clase de datos TextLayoutResult almacena todos los aspectos relevantes del texto creado como párrafos, tamaño y la configuración de su estilo.

Por lo que te da la oportunidad de pasar una lambda para agregar nuevos detalles o funcionalidades al texto.

Ejemplo: Imprimir el número de líneas resultantes que tiene un texto en 150dp.

@Composable
fun OnTextLayoutExample() {
    var lines: String by remember {
        mutableStateOf("X")
    }
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(Modifier.width(150.dp)) {
            TitleExample("onTextLayout")
            Text(
                "¿Cuántas líneas se generan para este texto a 150dp?",
                onTextLayout = {
                    lines = "R:/${it.lineCount}"
                })
            Text(lines)
        }
    }
}

Lectura: El ejemplo anterior usó un estado mutable para sostener el número de líneas creado. Aprende más leyendo Estado En Compose.


Aplicar Un Tema A Un Texto

Esta característica puedes verla en mi tutorial Aplicar Temas En Compose. Ahí verás como el parámetro style de Text puede recibir un tipo de escala basado en el sistema de Material Design.

Por ejemplo: Aplicar la escala Headline 6 de la tipografía base de Material Design:

@Composable
private fun TitleExample(text: String) {
    Text(text = text, style = MaterialTheme.typography.h6)
}

Usar Múltiples Estilos A Un Texto

El componente Text tiene otra variante donde el parámetro text es de tipo AnnotatedString. Esta clase representa la estructura de un texto con múltiples estilos aplicados.

¿De que se compone esta definición?

  • Un String que representa el texto
  • Una lista de elementos SpanStyle que definen el estilo para uno o más caracteres
  • Una lista de elementos ParagraphStyle para especificar el estilo de un párrafo completo

Aplicar Estilo A Diferentes Caracteres

Hay tres formas para construir las instancias AnnotatedString.

La primera de ellas es usar su constructor público:

AnnotatedString(
    text: String,
    spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
    paragraphStyles: List<AnnotatedString.Range<ParagraphStyle>> = listOf()
)

Como ves, spanStyles y paragraphStyles son listas del tipo Range. Este representa la información de inicio y final asociada al estilo. Por lo que para crear rangos pasamos el estilo y las posiciones a las que se aplicará:

<T : Any?> Range(item: T, start: Int, end: Int)

Por ejemplo:

Tomar la palabra «Develou» y aplicar los siguientes estilos:

  • Caracteres [1,2] -> Color amarillo + tamaño 24sp
  • Caracteres [3,4] -> Color azul + tamaño 16sp
  • Caracteres [5-7] -> Color rojo + tamaño 12sp

Usando SpanStyle tendrás:

@Composable
fun SpanStyleExample() {
    val styles = listOf(
        AnnotatedString.Range(SpanStyle(Color.Yellow, fontSize = 24.sp), 0, 2),
        AnnotatedString.Range(SpanStyle(Color.Blue, fontSize = 16.sp), 2, 4),
        AnnotatedString.Range(SpanStyle(Color.Red, fontSize = 12.sp), 4, 7)
    )
    Text(AnnotatedString("Develou", styles))
}

El AnnotatedString.Builder

Otra forma es usar AnnotatedString.Builder y construir paso a paso la cadena. El ejemplo anterior es posible representarlo así:

@Composable
fun SpanStyleExample() {

    Text(
        with(AnnotatedString.Builder("Develou")) {
            addStyle(SpanStyle(Color.Yellow, fontSize = 24.sp), 0, 2)
            addStyle(SpanStyle(Color.Blue, fontSize = 16.sp), 2, 4)
            addStyle(SpanStyle(Color.Red, fontSize = 12.sp), 4, 7)
            toAnnotatedString()
        }
    )
}

La instancia del builder se construye con un string adicional sobre el cual puedes invocar los siguientes métodos:

  • addStyle(): Aplica un SpanStyle o ParagraphStyle a un segmento del string actual
  • append(): Concatena texto al valor inicial
  • pushStyle(): Aplica un SpanStyle o ParagraphStyle a todos los elementos que añadas hasta que llames a pop()
  • pop(): Finaliza el último estilo o anotación iniciado

La Función buildAnnotatedString()

Adicionalmente, existe la función buildAnnotatedString() que usa al Builder anterior para proporcionarte un DSL más intuitivo a la hora de crear los bloques de construcción.

Al invocar esta función las sentencias se verían así:

@Composable
fun SpanStyleExample() {
    
    Text(
        buildAnnotatedString {
            withStyle(SpanStyle(Color.Yellow, fontSize = 24.sp)) {
                append("De")
            }
            withStyle(SpanStyle(Color.Blue, fontSize = 16.sp)) {
                append("ve")
            }

            withStyle(SpanStyle(Color.Red, fontSize = 12.sp)) {
                append("lou")
            }
        }
    )
}

Donde withStyle() recibe una instancia de SpanStyle o ParagraphStyle para aplicarla sobre los bloques internos. Y append() concatena el texto con el estilo al resultado final.

Aplicar Estilos A Un Párrafo

En el caso en que quieras aplicarle el estilo a todas las líneas de un párrafo usa ParagraphStyle en cualquier modo de construcción.

Por ejemplo:

Probar los cuatro parámetros de ParagraphStyle: lineHeight, textDirection, textAlign y textIndent.

@Composable
fun ParagraphStyleExample() {
    Text(
        buildAnnotatedString {
            withStyle(SpanStyle(fontSize = 20.sp)) {
                append("lineHeight (20sp)")
            }
            withStyle(ParagraphStyle(lineHeight = 24.sp)) {
                append("En este texto aplicamos 24sp para la altura de línea\n")
            }
            withStyle(SpanStyle(fontSize = 20.sp)) {
                append("textDirection (Rtl)")
            }
            withStyle(ParagraphStyle(textDirection = TextDirection.Rtl)) {
                append("En este texto aplicamos una dirección de texto de derecha a izquierda\n")
            }
            withStyle(SpanStyle(fontSize = 20.sp)) {
                append("textAlign (Center)")
            }
            withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {

                append("Este texto está alineado al centro\n")
            }
            withStyle(SpanStyle(fontSize = 20.sp)) {
                append("textIndent (20sp)")
            }
            withStyle(ParagraphStyle(textIndent = TextIndent(20.sp, 8.sp))) {
                append("En este texto aplicamos 16sp de sangría para la primera línea ")
                append("y 8sp para las demás.")
            }
        },
        modifier = Modifier
            .width(200.dp)
            .padding(16.dp)
    )
}

Permitir Al Usuario Seleccionar Texto

Si deseas habilitar la selección de texto cuando el usuario presiona prolongadamente una línea del mismo, entonces recubre tu componente Text con SelectionContainer().

Por ejemplo:

@Composable
fun SelectionContainerExample() {
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        SelectionContainer {
            Text("Este texto puede ser seleccionado")
        }
    }
}
Seleccionar texto en Compose

Si deseas excluir un componente de la selección de texto, entonces recúbrelo con la función componible DisableSelection().

@Composable
fun SelectionContainerExample() {
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        SelectionContainer {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Text("Este texto puede ser seleccionado")
                DisableSelection {

                    Text("Esto no")
                }
                Text("Desde aquí vuelve a ser seleccionable")
            }
        }
    }
}
Evitar que un texto sea seleccionable en Compose

Obtener La Posición Cliqueada De Un Texto

Cuando desees conseguir el lugar del texto en que el usuario ha realizado clic, usa el componente ClickableText en vez Text.

Este recibe como parámetro una función (Int) -> Unit que te provee la posición seleccionada.

Por ejemplo:

@Composable
fun ClickableTextExample() {
    var clickPos by remember {
        mutableStateOf(0)
    }
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        ClickableText(
            text = AnnotatedString("Posición cliqueada -> $clickPos"),
            style = MaterialTheme.typography.h6
        ) { offset ->
            clickPos = offset
        }
    }
}
Ejemplo de ClickableText en Compose

Añadir URL A Texto

La clase AnnotatedString te permite añadir anotaciones a las secciones del texto, con el fin de describir su contenido y especificar alguna lógica adicional.

Una de las aplicaciones más comunes de anotaciones es enlazar un segmento de texto a una URL. Donde añades el estilo para el enlace y luego, junto a ClickableText, determinas si la anotación es parte de ese fragmento para abrir el navegador web.

Por ejemplo:

@Composable
fun TextUrlExample() {
    val text = buildAnnotatedString {
        append("Visita mi sitio ")
        pushStringAnnotation("URL", "https://www.develou.com")
        withStyle(
            SpanStyle(
                color = Color.Blue,
                fontWeight = FontWeight.Bold
            )
        ) {
            append("develou.com")
        }
        pop()
    }
    val uriHandler = LocalUriHandler.current
    Box(
        Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {

        ClickableText(text, style = MaterialTheme.typography.body2) { offset ->
            text.getStringAnnotations(
                tag = "URL",
                start = offset,
                end = offset
            ).firstOrNull()?.let { annotation ->
                uriHandler.openUri(annotation.item)
            }
        }
    }
}

El método pushStringAnnotation() facilita la inclusión de la anotación a partir del método Builder.appendAnnotation().

Cuando el usuario haga clic, podrás obtener la anotación con AnnotatedString.getStringAnnotations() con la etiqueta de referencia («URL» para este ejemplo) y el lugar donde se hizo click pasando a offset al límite [start, end].

Por otro lado, la propiedad de composición local LocalUriHandler te permite abrir la URL en la App del navegador. Por lo que le pasas el valor de la propiedad item al método openUri().

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!