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)
)
// result.size.width, result.size.height, result.lineCount
Параметр 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) гарантує, що вимірювання перезапускається тільки при зміні тексту. Обмеження задані з великим 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, якщо рядок не досягає межі контейнера. Знаючи ширину кожного рядка, можна реалізувати ефект «прапорця» (justify) вручну або розмістити 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 = "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)
}
}
)
У цьому прикладі вимірювання першого рядка через TextMeasurer визначає позицію іконки. Без TextMeasurer іконка центрувалася б по всій висоті багаторядкового тексту, що візуально невірно. Цей патерн широко застосовується в чатах, списках контактів та інформаційних картках.
TextMeasurer — синхронний API, який працює в потоці UI. При надмірному використанні він може викликати дроп кадрів, особливо при вимірюванні великих обсягів тексту на слабких пристроях.
Перше правило — кешуйте результат. Завжди обгортайте виклик measure() в remember з залежностями від тексту та стилю. Ніколи не викликайте measure() всередині CompositionLocal або в гарячих шляхах recomposition.
Друге правило — обмежуйте maxWidth і maxHeight. Якщо задати maxWidth = Constraints.Infinity, TextMeasurer не зможе перенести рядки і текст вийде за межі екрану. Завжди передавайте реалістичні обмеження, особливо для багаторядкового тексту.
Третє — уникайте повторного вимірювання з однаковими параметрами. Якщо текст і стиль не змінилися, результат measure() буде тим самим. Комбінуйте вимірювання з Deferred або LaunchedEffect для асинхронних сценаріїв.
Часто задавані питання
TextMeasurer вимірює текст до рендерингу, а onSizeChanged — після. TextMeasurer потрібен, коли розмір тексту впливає на layout батька; onSizeChanged підходить для реактивної логіки після компонування.
Так, TextMeasurer.measure() приймає AnnotatedString, який включає SpannableString з inline-стилями, посиланнями та кольорами. Всі спани враховуються при вимірюванні.
TextMeasurer працює на рівні Skia-рендерингу, де всі розміри в пікселях. Конвертуйте через LocalDensity.current: density.run { width.toDp() }.
TextMeasurer використовує FontFamily.Default до завантаження кастомного шрифту. Для точного вимірювання зачекайте завантаження через FontResource і використовуйте async/await.
Вимірювання тексту в кожному елементі LazyColumn може знижувати FPS на слабких пристроях. Оптимізація: кешуйте результат через remember і обмежуйте розмір тексту.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також