TextMeasurer — este o API din Jetpack Compose destinată măsurării dimensiunilor textului înainte de randarea efectivă pe ecran. Spre deosebire de abordarea clasică prin Paint.measureText în Android View, TextMeasurer oferă o modalitate nativă Compose de a obține lățimea, înălțimea și metricile rândurilor ținând cont de stil și constrângeri. Conform Android Developers Documentation (2025), TextMeasurer este utilizat activ în componentele Layout personalizate, subtitrările animate și elementele UI adaptive, unde dimensiunea textului influențează poziționarea altor elemente.
Principalele puncte
TextMeasurer — este o interfață funcțională din pachetul androidx.compose.ui.text care permite măsurarea textului în Compose fără randare directă pe ecran. Face parte din motorul text al Compose și funcționează la nivelul Paragraph API care utilizează Skia pentru randare.
Scopul principal al TextMeasurer este măsurarea preventivă. În Compose, dimensiunea textului este de obicei determinată după ce Composable a fost deja plasat în Layout. Dar există scenarii în care trebuie să cunoașteți lățimea textului înainte de compunere: de exemplu, pentru a decide dacă un rând încape în container, sau pentru a calcula dimensiunea unui buton pe baza lungimii textului.
TextMeasurer rezolvă această problemă oferind metoda measure(), care primește TextMeasureRequest (text, stil, constrângeri) și returnează TextLayoutResult (metrici, rânduri, caractere). Acest lucru permite dezvoltatorului să obțină toate datele despre text sincron, fără o întârziere de un cadru.
Folosiți TextMeasurer când dimensiunea textului influențează dimensiunea componentului părinte, la crearea tranzițiilor animate de text sau la implementarea Layout-urilor personalizate unde textul este învecinat cu alte elemente.
API-ul TextMeasurer este format din două entități cheie: rememberTextMeasurer() (crearea instanței) și measure() (pornirea măsurării). Instanța este creată o singură dată pe întreg ciclul de viață al componentului și este reutilizată pentru toate măsurătorile.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// lățimea rezultatului, înălțimea rezultatului, numărul de rânduri
Parametrul constraints definește lățimea și înălțimea maximă în care textul trebuie să se încadreze. Dacă textul nu încape pe lățime, este transferat pe următorul rând. Rezultatul măsurării conține TextLayoutResult, în care puteți obține numărul de rânduri, poziția fiecărui caracter, lățimea fiecărui rând și dimensiunea totală a textului.
TextMeasurer funcționează sincron în firul UI și nu cauzează recompoziție. Totuși, măsurarea frecventă a textului cu stiluri diferite poate crea încărcare — de aceea instanța este creată prin remember și reutilizată.
Cel mai simplu scenariu — măsurarea lățimii textului pe un singur rând pentru a determina dimensiunea containerului părinte. De exemplu, pentru un buton a cărui lățime depinde de lungimea etichetei.
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)
}
În acest exemplu, remember(label) garantează că măsurarea se repornește doar la modificarea textului. Constraints sunt setate cu un maxWidth mare (1000) pentru a preveni transferul rândului — constrângerea reală va fi egală cu lățimea textului. Lățimea Box este setată la lățimea măsurată a textului, ceea ce dă efectul de „buton după dimensiunea textului“.
Atenție: lățimea din rezultat trebuie convertită în dp prin .dp, deoarece TextMeasurer returnează dimensiuni în pixeli, iar Modifier.width așteaptă Dp. Pentru precizie, utilizați density și LocalDensity.current.
Pentru textul pe mai multe rânduri, TextMeasurer permite determinarea nu doar a înălțimii totale, ci și a numărului de rânduri și a poziției fiecărui caracter în paragraf. Acest lucru este necesar la crearea câmpurilor de text personalizate, chat-urilor sau editorilor.
Principalele proprietăți ale TextLayoutResult pentru textul pe mai multe rânduri: lineCount (numărul de rânduri), getLineTop(index) (coordonata Y a începutului rândului), getLineBottom(index) (coordonata Y a sfârșitului), getLineWidth(index) (lățimea rândului). Combinându-le, puteți poziționa elementele interfeței exact în raport cu rândurile textului.
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
Metoda getLineWidth(lineIndex) returnează lățimea rândului specificat în pixeli. Poate fi mai mică decât maxWidth dacă rândul nu atinge marginea containerului. Cunoscând lățimea fiecărui rând, puteți implementa manual efectul de justificare sau plasa elemente inline lângă rândurile scurte.
Pentru a obține poziția unui caracter specific, utilizați getBoundingBox(offset), care returnează un Rect cu coordonatele left, top, right, bottom. Acest lucru este util pentru poziționarea cursorului în câmpuri de text personalizate sau pentru evidențierea unui interval de text cu animație.
Cel mai puternic scenariu de utilizare a TextMeasurer — componentele Layout personalizate, unde textul este învecinat cu alte elemente, iar dimensiunile lor sunt interdependente. De exemplu, widget-ul „Pictogramă + text“, unde pictograma trebuie centrată în raport cu primul rând al textului, nu cu întregul bloc.
În Row-ul standard, pictograma este centrată pe înălțimea întregului Row, ceea ce pare nenatural când textul ocupă 3 rânduri, iar pictograma — unul. Cu TextMeasurer puteți măsura primul rând și alinia pictograma exact în raport cu acesta.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Text pe mai multe rânduri aici")
},
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)
}
}
)
În acest exemplu, măsurarea primului rând prin TextMeasurer determină poziția pictogramei. Fără TextMeasurer, pictograma s-ar centra pe întreaga înălțime a textului pe mai multe rânduri, ceea ce este vizual incorect. Acest model este utilizat pe scară largă în chat-uri, liste de contacte și carduri informative.
TextMeasurer — este o API sincronă care funcționează în firul UI. La utilizare excesivă, poate cauza scăderi de cadre, mai ales la măsurarea unor volume mari de text pe dispozitive slabe.
Prima regulă — stocați în cache rezultatul. Înfășurați întotdeauna apelul measure() în remember cu dependențe de text și stil. Nu apelați niciodată measure() în CompositionLocal sau în căile fierbinți de recompoziție.
A doua regulă — limitați maxWidth și maxHeight. Dacă setați maxWidth = Constraints.Infinity, TextMeasurer nu va putea transfera rândurile și textul va ieși în afara ecranului. Transmiteți întotdeauna constrângeri realiste, mai ales pentru textul pe mai multe rânduri.
A treia — evitați măsurarea repetată cu aceiași parametri. Dacă textul și stilul nu s-au schimbat, rezultatul measure() va fi același. Combinați măsurarea cu Deferred sau LaunchedEffect pentru scenarii asincrone.
Întrebări frecvente
TextMeasurer măsoară textul înainte de randare, iar onSizeChanged — după. TextMeasurer este necesar când dimensiunea textului influențează layout-ul părintelui; onSizeChanged este potrivit pentru logică reactivă după compunere.
Da, TextMeasurer.measure() acceptă AnnotatedString care include SpannableString cu stiluri inline, linkuri și culori. Toate span-urile sunt luate în considerare la măsurare.
TextMeasurer funcționează la nivelul randării Skia, unde toate dimensiunile sunt în pixeli. Convertiți prin LocalDensity.current: density.run { width.toDp() }.
TextMeasurer utilizează FontFamily.Default până la încărcarea fontului personalizat. Pentru măsurare precisă, așteptați încărcarea prin FontResource și utilizați async/await.
Măsurarea textului în fiecare element al LazyColumn poate reduce FPS pe dispozitive slabe. Optimizare: stocați în cache rezultatul prin remember și limitați dimensiunea textului.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și