TextMeasurer es una API de Jetpack Compose diseñada para medir las dimensiones del texto antes de que se renderice realmente en pantalla. A diferencia del enfoque clásico mediante Paint.measureText en Android View, TextMeasurer proporciona una forma nativa de Compose para obtener el ancho, la altura y las métricas de las líneas teniendo en cuenta el estilo y las restricciones. Según Android Developers Documentation (2025), TextMeasurer se utiliza activamente en componentes Layout personalizados, etiquetas animadas y elementos de UI adaptativos donde el tamaño del texto afecta el posicionamiento de otros elementos.
Puntos Clave
TextMeasurer es una interfaz funcional del paquete androidx.compose.ui.text que permite medir texto en Compose sin renderizarlo realmente en pantalla. Es parte del motor de texto de Compose y opera a nivel de Paragraph API, que utiliza Skia para el renderizado.
El propósito principal de TextMeasurer es la pre-medición. En Compose, el tamaño del texto generalmente se determina después de que el Composable ya está colocado en el Layout. Sin embargo, hay escenarios donde se necesita conocer el ancho del texto antes de la composición — por ejemplo, para decidir si una cadena cabe en un contenedor, o para calcular el tamaño de un botón según la longitud del texto.
TextMeasurer resuelve esto proporcionando el método measure(), que toma un TextMeasureRequest (texto, estilo, restricciones) y devuelve TextLayoutResult (métricas, líneas, caracteres). Esto permite al desarrollador obtener todos los datos del texto de forma síncrona, sin un retraso de un fotograma.
Usa TextMeasurer cuando el tamaño del texto afecte el tamaño del componente padre, al crear transiciones de texto animadas o al implementar Layouts personalizados donde el texto coexista con otros elementos.
La API de TextMeasurer consta de dos entidades clave: rememberTextMeasurer() (creación de una instancia) y measure() (ejecución de la medición). La instancia se crea una vez para todo el ciclo de vida del componente y se reutiliza para todas las mediciones.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// result.size.width, result.size.height, result.lineCount
El parámetro constraints define el ancho y alto máximo dentro del cual debe caber el texto. Si el texto no cabe en el ancho, se envuelve a la siguiente línea. El resultado de la medición contiene un TextLayoutResult, que proporciona el número de líneas, la posición de cada carácter, el ancho de cada línea y el tamaño total del texto.
TextMeasurer funciona de forma síncrona en el hilo de UI y no provoca recomposición. Sin embargo, la medición frecuente de texto con diferentes estilos puede generar sobrecarga — por lo tanto, la instancia se crea mediante remember y se reutiliza.
El escenario más simple es medir el ancho de texto de una sola línea para determinar el tamaño del contenedor padre. Por ejemplo, para un botón cuyo ancho depende de la longitud de la etiqueta.
val textMeasurer = rememberTextMeasurer()
var label by remember { mutableStateOf("Submit") }
val textResult = remember(label) {
textMeasurer.measure(
text = label,
style = MaterialTheme.typography.labelLarge,
constraints = Constraints(maxWidth = 1000, maxHeight = 100)
)
}
Box(
modifier = Modifier
.width(textResult.size.width.dp)
.height(40.dp)
.background(Color.Blue)
.clickable { onClick() }
) {
Text(text = label, style = MaterialTheme.typography.labelLarge)
}
En este ejemplo, remember(label) asegura que la medición se reinicie solo cuando cambie el texto. Las restricciones se establecen con un maxWidth grande (1000) para evitar el ajuste de línea — la restricción real será igual al ancho del texto. El ancho del Box se establece al ancho medido del texto, lo que produce un efecto de “botón ajustado al texto”.
Nota: el ancho del resultado debe convertirse a dp usando .dp, ya que TextMeasurer devuelve dimensiones en píxeles, mientras que Modifier.width espera Dp. Para mayor precisión, usa density y LocalDensity.current.
Para texto multilínea, TextMeasurer permite determinar no solo la altura total, sino también el número de líneas y la posición de cada carácter dentro del párrafo. Esto es necesario al crear campos de texto personalizados, chats o editores.
Las principales propiedades de TextLayoutResult para texto multilínea: lineCount (número de líneas), getLineTop(index) (coordenada Y del inicio de la línea), getLineBottom(index) (coordenada Y del final), getLineWidth(index) (ancho de línea). Combinándolas, puedes posicionar con precisión los elementos de la interfaz en relación con las líneas de texto.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Long multiline text that needs to be measured before layout",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 150, maxHeight = 300)
)
val linesCount = result.lineCount
val firstLineWidth = result.getLineWidth(0)
val totalHeight = result.size.height
El método getLineWidth(lineIndex) devuelve el ancho de la línea especificada en píxeles. Puede ser menor que maxWidth si la línea no alcanza el borde del contenedor. Conociendo el ancho de cada línea, puedes implementar un efecto de justificación manualmente o colocar elementos inline junto a líneas cortas.
Para obtener la posición de un carácter específico, usa getBoundingBox(offset), que devuelve un Rect con las coordenadas left, top, right, bottom. Esto es útil para posicionar el cursor en campos de texto personalizados o para resaltar un rango de texto con animación.
La aplicación más potente de TextMeasurer son los componentes Layout personalizados, donde el texto coexiste con otros elementos y sus tamaños son interdependientes. Por ejemplo, un widget “Icono + texto” donde el icono debe centrarse con respecto a la primera línea de texto, no a todo el bloque.
En un Row estándar, el icono se centra por toda la altura del Row, lo que parece poco natural cuando el texto ocupa 3 líneas y el icono solo una. Con TextMeasurer, puedes medir la primera línea y alinear el icono precisamente con ella.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Multi-line text here")
},
measurePolicy = { measurables, constraints ->
val textMeasurable = measurables[1]
val textPlaceable = textMeasurable.measure(constraints)
val iconPlaceable = measurables[0].measure(constraints)
val firstLineHeight = textMeasurer.measure(
text = "Sample",
style = MaterialTheme.typography.bodyMedium,
constraints = Constraints(maxWidth = 2000, maxHeight = 100)
).size.height
layout(width = textPlaceable.width + iconPlaceable.width,
height = maxOf(textPlaceable.height, iconPlaceable.height)) {
iconPlaceable.placeRelative(0, (firstLineHeight - iconPlaceable.height) / 2)
textPlaceable.placeRelative(iconPlaceable.width, 0)
}
}
)
En este ejemplo, medir la primera línea mediante TextMeasurer determina la posición del icono. Sin TextMeasurer, el icono se centraría por toda la altura del texto multilínea, lo que es visualmente incorrecto. Este patrón se usa ampliamente en chats, listas de contactos y tarjetas de información.
TextMeasurer es una API síncrona que se ejecuta en el hilo de UI. Un uso excesivo puede provocar caídas de fotogramas, especialmente al medir grandes volúmenes de texto en dispositivos débiles.
La primera regla — almacena en caché el resultado. Siempre envuelve la llamada a measure() en remember con dependencias de texto y estilo. Nunca llames a measure() dentro de CompositionLocal o en rutas de recomposición calientes.
La segunda regla — limita maxWidth y maxHeight. Si estableces maxWidth = Constraints.Infinity, TextMeasurer no podrá ajustar las líneas y el texto se saldrá de la pantalla. Siempre pasa restricciones realistas, especialmente para texto multilínea.
La tercera regla — evita mediciones repetidas con los mismos parámetros. Si el texto y el estilo no han cambiado, el resultado de measure() será el mismo. Combina la medición con Deferred o LaunchedEffect para escenarios asíncronos.
Preguntas Frecuentes
TextMeasurer mide el texto antes de renderizar, mientras que onSizeChanged mide después. TextMeasurer es necesario cuando el tamaño del texto afecta al layout padre; onSizeChanged funciona para lógica reactiva después de la composición.
Sí, TextMeasurer.measure() acepta AnnotatedString, que incluye SpannableString con estilos inline, enlaces y colores. Todos los spans se tienen en cuenta durante la medición.
TextMeasurer opera a nivel de renderizado Skia, donde todas las dimensiones están en píxeles. Convierte mediante LocalDensity.current: density.run { width.toDp() }.
TextMeasurer usa FontFamily.Default hasta que se carga una fuente personalizada. Para una medición precisa, espera a que la fuente se cargue mediante FontResource y usa async/await.
Medir texto en cada elemento de LazyColumn puede reducir los FPS en dispositivos débiles. Optimización: almacena en caché el resultado mediante remember y limita el tamaño del texto.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también