TextMeasurer — е API от Jetpack Compose, предназначено за измерване на размерите на текст преди действителното му рендериране на екрана. За разлика от класическия подход чрез Paint.measureText в Android View, TextMeasurer предоставя Compose-роден начин за получаване на ширина, височина и метрика на редове, като взема предвид стила и ограниченията. Според Android Developers Documentation (2025), TextMeasurer се използва активно в персонализирани Layout компоненти, анимирани надписи и адаптивни UI елементи, където размерът на текста влияе върху позиционирането на други елементи.
Основни точки
TextMeasurer — е функционален интерфейс от пакета androidx.compose.ui.text, който позволява измерване на текст в Compose без директно рендериране на екрана. Той е част от текстовия двигател на Compose и работи на ниво Paragraph API, което използва Skia за рендериране.
Основното предназначение на TextMeasurer е предварително измерване. В Compose размерът на текста обикновено се определя, след като Composable вече е поставен в Layout. Но има сценарии, където трябва да знаете ширината на текста преди композиция: например, за да решите дали редът се побира в контейнера, или за да изчислите размера на бутон въз основа на дължината на текста.
TextMeasurer решава този проблем, като предоставя метода measure(), който приема TextMeasureRequest (текст, стил, ограничения) и връща TextLayoutResult (метрики, редове, символи). Това позволява на разработчика да получи всички данни за текста синхронно, без забавяне от един кадър.
Използвайте TextMeasurer, когато размерът на текста влияе върху размера на родителския компонент, при създаване на анимирани текстови преходи или при имплементиране на персонализирани Layout, където текстът е съседен на други елементи.
API-то на TextMeasurer се състои от две ключови единици: rememberTextMeasurer() (създаване на инстанция) и measure() (стартиране на измерване). Инстанцията се създава веднъж за целия жизнен цикъл на компонента и се използва повторно за всички измервания.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// ширина на резултата, височина на резултата, брой редове
Параметърът constraints определя максималната ширина и височина, в рамките на които текстът трябва да се побере. Ако текстът не се побира по ширина, той се прехвърля на следващия ред. Резултатът от измерването съдържа TextLayoutResult, в който можете да получите броя на редовете, позицията на всеки символ, ширината на всеки ред и общия размер на текста.
TextMeasurer работи синхронно в нишката на UI и не предизвиква рекомпозиция. Въпреки това, честото измерване на текст с различни стилове може да създаде натоварване — затова инстанцията се създава чрез remember и се използва повторно.
Най-простият сценарий — измерване на ширината на едноредов текст, за да се определи размерът на родителския контейнер. Например, за бутон, чиято ширина зависи от дължината на етикета.
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)
}
В този пример remember(label) гарантира, че измерването се рестартира само при промяна на текста. Constraints са зададени с голям maxWidth (1000), за да се предотврати прехвърляне на ред — действителното ограничение ще бъде равно на ширината на текста. Ширината на Box се задава на измерената ширина на текста, което дава ефекта „бутон според размера на текста".
Забележка: ширината от резултата трябва да се конвертира в dp чрез .dp, тъй като TextMeasurer връща размери в пиксели, а Modifier.width очаква Dp. За точност използвайте density и LocalDensity.current.
За многоредов текст, TextMeasurer позволява определяне не само на общата височина, но и на броя редове и позицията на всеки символ в абзаца. Това е необходимо при създаване на персонализирани текстови полета, чатове или редактори.
Основните свойства на TextLayoutResult за многоредов текст: lineCount (брой редове), getLineTop(index) (Y-координата на началото на реда), getLineBottom(index) (Y-координата на края), getLineWidth(index) (ширина на реда). Комбинирайки ги, можете точно да позиционирате елементи на интерфейса спрямо редовете текст.
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
Методът getLineWidth(lineIndex) връща ширината на посочения ред в пиксели. Може да е по-малка от maxWidth, ако редът не достига границата на контейнера. Познавайки ширината на всеки ред, можете ръчно да имплементирате ефект на двустранно подравняване или да поставите inline елементи до късите редове.
За да получите позицията на конкретен символ, използвайте getBoundingBox(offset), който връща Rect с координати left, top, right, bottom. Това е полезно за позициониране на курсора в персонализирани текстови полета или за подчертаване на диапазон от текст с анимация.
Най-мощният сценарий за използване на TextMeasurer — персонализирани Layout компоненти, където текстът е съседен на други елементи и размерите им са взаимозависими. Например, уиджетът „Икона + текст", където иконата трябва да бъде центрирана спрямо първия ред текст, а не спрямо целия блок.
В стандартния Row иконата се центрира по височината на целия Row, което изглежда неестествено, когато текстът заема 3 реда, а иконата — един. С TextMeasurer можете да измерите първия ред и да подравните иконата точно спрямо него.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Многоредов текст тук")
},
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)
}
}
)
В този пример измерването на първия ред чрез TextMeasurer определя позицията на иконата. Без TextMeasurer иконата би се центрирала по цялата височина на многоредовия текст, което е визуално неправилно. Този модел се използва широко в чатове, списъци с контакти и информационни карти.
TextMeasurer — е синхронно API, което работи в нишката на UI. При прекомерна употреба може да причини спадане на кадри, особено при измерване на големи обеми текст на по-слаби устройства.
Първо правило — кеширайте резултата. Винаги обвивайте извикването на measure() в remember със зависимости от текст и стил. Никога не извиквайте measure() вътре в CompositionLocal или по горещите пътища на рекомпозиция.
Второ правило — ограничавайте maxWidth и maxHeight. Ако зададете maxWidth = Constraints.Infinity, TextMeasurer няма да може да прехвърля редове и текстът ще излезе извън екрана. Винаги предавайте реалистични ограничения, особено за многоредов текст.
Трето — избягвайте повторно измерване с еднакви параметри. Ако текстът и стилът не са се променили, резултатът от measure() ще бъде същият. Комбинирайте измерването с Deferred или LaunchedEffect за асинхронни сценарии.
Често задавани въпроси
TextMeasurer измерва текст преди рендериране, а onSizeChanged — след това. TextMeasurer е необходим, когато размерът на текста влияе върху оформлението на родителя; onSizeChanged е подходящ за реактивна логика след композиция.
Да, TextMeasurer.measure() приема AnnotatedString, който включва SpannableString с inline стилове, връзки и цветове. Всички span се вземат предвид при измерването.
TextMeasurer работи на ниво Skia-рендериране, където всички размери са в пиксели. Конвертирайте чрез LocalDensity.current: density.run { width.toDp() }.
TextMeasurer използва FontFamily.Default до зареждането на персонализирания шрифт. За точно измерване изчакайте зареждането чрез FontResource и използвайте async/await.
Измерването на текст във всеки елемент на LazyColumn може да намали FPS на слаби устройства. Оптимизация: кеширайте резултата чрез remember и ограничете размера на текста.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също