TextMeasurer è un’API di Jetpack Compose progettata per misurare le dimensioni del testo prima del suo effettivo rendering sullo schermo. A differenza dell’approccio classico tramite Paint.measureText in Android View, TextMeasurer fornisce un modo nativo di Compose per ottenere larghezza, altezza e metriche delle righe tenendo conto dello stile e dei vincoli. Secondo Android Developers Documentation (2025), TextMeasurer è attivamente utilizzato in componenti Layout personalizzati, etichette animate ed elementi UI adattivi dove la dimensione del testo influisce sul posizionamento di altri elementi.
Punti Chiave
TextMeasurer è un’interfaccia funzionale del pacchetto androidx.compose.ui.text che consente di misurare il testo in Compose senza effettivamente renderizzarlo sullo schermo. Fa parte del motore di testo di Compose e opera a livello di Paragraph API, che utilizza Skia per il rendering.
Lo scopo principale di TextMeasurer è la pre-misurazione. In Compose, la dimensione del testo viene solitamente determinata dopo che il Composable è già stato posizionato nel Layout. Tuttavia, esistono scenari in cui è necessario conoscere la larghezza del testo prima del layout — ad esempio, per decidere se una stringa si adatta a un contenitore, o per calcolare la dimensione di un pulsante in base alla lunghezza del testo.
TextMeasurer risolve questo problema fornendo il metodo measure(), che accetta un TextMeasureRequest (testo, stile, vincoli) e restituisce TextLayoutResult (metriche, righe, caratteri). Ciò consente allo sviluppatore di ottenere tutti i dati del testo in modo sincrono, senza un ritardo di un fotogramma.
Utilizza TextMeasurer quando la dimensione del testo influisce sulla dimensione del componente padre, quando crei transizioni di testo animate o quando implementi Layout personalizzati in cui il testo coesiste con altri elementi.
L’API TextMeasurer è composta da due entità chiave: rememberTextMeasurer() (creazione di un’istanza) e measure() (esecuzione della misurazione). L’istanza viene creata una volta per l’intero ciclo di vita del componente e viene riutilizzata per tutte le misurazioni.
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
Il parametro constraints definisce la larghezza e l’altezza massime entro cui il testo deve adattarsi. Se il testo non si adatta in larghezza, va a capo alla riga successiva. Il risultato della misurazione contiene un TextLayoutResult, che fornisce il numero di righe, la posizione di ogni carattere, la larghezza di ogni riga e la dimensione complessiva del testo.
TextMeasurer funziona in modo sincrono sul thread UI e non innesca la ricomposizione. Tuttavia, misurare frequentemente il testo con stili diversi può creare overhead — pertanto l’istanza viene creata tramite remember e riutilizzata.
Lo scenario più semplice è misurare la larghezza di testo su una singola riga per determinare la dimensione del contenitore padre. Ad esempio, per un pulsante la cui larghezza dipende dalla lunghezza dell’etichetta.
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)
}
In questo esempio, remember(label) garantisce che la misurazione si riavvii solo quando il testo cambia. I vincoli sono impostati con un maxWidth grande (1000) per evitare l’andata a capo — il vincolo reale sarà uguale alla larghezza del testo. La larghezza del Box viene impostata sulla larghezza misurata del testo, ottenendo un effetto di “pulsante adattato al testo”.
Nota: la larghezza dal risultato deve essere convertita in dp usando .dp, poiché TextMeasurer restituisce dimensioni in pixel, mentre Modifier.width si aspetta Dp. Per precisione, usa density e LocalDensity.current.
Per il testo multilinea, TextMeasurer consente di determinare non solo l’altezza totale, ma anche il numero di righe e la posizione di ogni carattere all’interno del paragrafo. Ciò è necessario quando si creano campi di testo personalizzati, chat o editor.
Le principali proprietà di TextLayoutResult per il testo multilinea: lineCount (numero di righe), getLineTop(index) (coordinata Y dell’inizio riga), getLineBottom(index) (coordinata Y di fine riga), getLineWidth(index) (larghezza riga). Combinandole, puoi posizionare con precisione gli elementi dell’interfaccia rispetto alle righe di testo.
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
Il metodo getLineWidth(lineIndex) restituisce la larghezza della riga specificata in pixel. Può essere inferiore a maxWidth se la riga non raggiunge il limite del contenitore. Conoscendo la larghezza di ogni riga, puoi implementare manualmente un effetto di giustificazione o posizionare elementi inline accanto a righe corte.
Per ottenere la posizione di un carattere specifico, usa getBoundingBox(offset), che restituisce un Rect con coordinate left, top, right, bottom. Ciò è utile per posizionare il cursore in campi di testo personalizzati o per evidenziare un intervallo di testo con animazione.
L’applicazione più potente di TextMeasurer sono i componenti Layout personalizzati, dove il testo coesiste con altri elementi e le loro dimensioni sono interdipendenti. Ad esempio, un widget “Icona + testo” in cui l’icona deve essere centrata rispetto alla prima riga di testo, non all’intero blocco.
In un Row standard, l’icona è centrata su tutta l’altezza del Row, il che appare innaturale quando il testo occupa 3 righe mentre l’icona è una sola. Con TextMeasurer, puoi misurare la prima riga e allineare l’icona precisamente ad essa.
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)
}
}
)
In questo esempio, la misurazione della prima riga tramite TextMeasurer determina la posizione dell’icona. Senza TextMeasurer, l’icona verrebbe centrata sull’intera altezza del testo multilinea, il che è visivamente scorretto. Questo pattern è ampiamente utilizzato in chat, elenchi di contatti e schede informative.
TextMeasurer è un’API sincrona che opera sul thread UI. Un uso eccessivo può causare cali di fotogrammi, specialmente quando si misurano grandi volumi di testo su dispositivi deboli.
Prima regola — memorizza nella cache il risultato. Avvolgi sempre la chiamata a measure() in remember con dipendenze di testo e stile. Non chiamare mai measure() all’interno di CompositionLocal o in percorsi di ricomposizione caldi.
Seconda regola — limita maxWidth e maxHeight. Se imposti maxWidth = Constraints.Infinity, TextMeasurer non potrà andare a capo e il testo uscirà dallo schermo. Passa sempre vincoli realistici, specialmente per il testo multilinea.
Terza regola — evita misurazioni ripetute con gli stessi parametri. Se il testo e lo stile non sono cambiati, il risultato di measure() sarà lo stesso. Combina la misurazione con Deferred o LaunchedEffect per scenari asincroni.
Domande Frequenti
TextMeasurer misura il testo prima del rendering, mentre onSizeChanged misura dopo. TextMeasurer è necessario quando la dimensione del testo influenza il layout padre; onSizeChanged è adatto per logica reattiva dopo la composizione.
Sì, TextMeasurer.measure() accetta AnnotatedString, che include SpannableString con stili inline, link e colori. Tutti gli span vengono considerati durante la misurazione.
TextMeasurer opera a livello di rendering Skia, dove tutte le dimensioni sono in pixel. Converti tramite LocalDensity.current: density.run { width.toDp() }.
TextMeasurer utilizza FontFamily.Default fino al caricamento di un font personalizzato. Per una misurazione accurata, attendi il caricamento del font tramite FontResource e usa async/await.
Misurare il testo in ogni elemento di LazyColumn può ridurre gli FPS su dispositivi deboli. Ottimizzazione: memorizza nella cache il risultato tramite remember e limita la dimensione del testo.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche