TextMeasurer: що це таке та як працює вимірювання тексту в Compose

Автор: IT Sectr Опубліковано: 2026-07-25 Час читання: 8 хв

TextMeasurer — це API з Jetpack Compose, призначене для вимірювання розмірів тексту до його фактичного рендерингу на екрані. На відміну від класичного підходу через Paint.measureText в Android View, TextMeasurer надає Compose-нативний спосіб отримати ширину, висоту та метрики рядків з урахуванням стилю та обмежень. Згідно з Android Developers Documentation (2025), TextMeasurer активно використовується в кастомних Layout-компонентах, анімованих підписах та адаптивних елементах UI, де розмір тексту впливає на позиціонування інших елементів.

Головне

  • TextMeasurer — Compose-API для вимірювання тексту до рендерингу.
  • Метод measure() повертає TextLayoutResult з метриками рядків і символів.
  • API враховує стиль, шрифт, розмір та обмеження контейнера в пікселях.
  • TextMeasurer підтримує вимірювання одного рядка та багаторядкового тексту.
  • Результат вимірювання використовується для кастомної верстки та анімації тексту.

Що таке TextMeasurer в Jetpack Compose?

TextMeasurer — це функціональний інтерфейс з пакета androidx.compose.ui.text, який дозволяє виміряти текст в Compose без безпосереднього рендерингу на екрані. Він є частиною текстового двигуна Compose і працює на рівні Paragraph API, який використовує Skia для рендерингу.

Основне призначення TextMeasurer — попереднє вимірювання. В Compose розмір тексту зазвичай визначається після того, як Composable вже розміщений в Layout. Але бувають сценарії, де потрібно знати ширину тексту до компонування: наприклад, щоб вирішити, чи поміщається рядок у контейнер, або щоб розрахувати розмір кнопки на основі довжини тексту.

TextMeasurer вирішує це завдання, надаючи метод measure(), який приймає TextMeasureRequest (текст, стиль, обмеження) і повертає TextLayoutResult (метрики, лінії, символи). Це дозволяє розробнику отримати всі дані про текст синхронно, без лагу в один кадр.

Використовуйте TextMeasurer, коли розмір тексту впливає на розмір батьківського компонента, при створенні анімованих текстових переходів або при реалізації кастомних Layout, де текст сусідить з іншими елементами.

Як працює TextMeasurer

API TextMeasurer складається з двох ключових сутностей: rememberTextMeasurer() (створення екземпляра) і measure() (запуск вимірювання). Екземпляр створюється один раз на весь життєвий цикл компонента і перевикористовується для всіх вимірювань.

kotlin
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 і перевикористовується.

Вимірювання одного рядка тексту

Найпростіший сценарій — виміряти ширину однорядкового тексту, щоб визначити розмір батьківського контейнера. Наприклад, для кнопки, ширина якої залежить від довжини підпису.

kotlin
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) (ширина рядка). Комбінуючи їх, можна точно позиціонувати елементи інтерфейсу відносно рядків тексту.

kotlin
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 в кастомних компонентах

Найбільш потужний сценарій застосування TextMeasurer — кастомні Layout-компоненти, де текст сусідить з іншими елементами, і їх розміри взаємозалежні. Наприклад, віджет «Іконка + текст», де іконка повинна бути центрована відносно першого рядка тексту, а не всього блоку.

У стандартному Row іконка центрується по висоті всього Row, що виглядає неприродно, коли текст займає 3 рядки, а іконка — один. З TextMeasurer можна виміряти перший рядок і вирівняти іконку саме по ньому.

kotlin
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 в Compose?

TextMeasurer вимірює текст до рендерингу, а onSizeChanged — після. TextMeasurer потрібен, коли розмір тексту впливає на layout батька; onSizeChanged підходить для реактивної логіки після компонування.

Чи можна виміряти SpannableString через TextMeasurer?

Так, TextMeasurer.measure() приймає AnnotatedString, який включає SpannableString з inline-стилями, посиланнями та кольорами. Всі спани враховуються при вимірюванні.

Чому TextMeasurer повертає розмір у пікселях, а не в dp?

TextMeasurer працює на рівні Skia-рендерингу, де всі розміри в пікселях. Конвертуйте через LocalDensity.current: density.run { width.toDp() }.

Як виміряти текст до того, як завантажиться шрифт?

TextMeasurer використовує FontFamily.Default до завантаження кастомного шрифту. Для точного вимірювання зачекайте завантаження через FontResource і використовуйте async/await.

Чи впливає TextMeasurer на продуктивність списків LazyColumn?

Вимірювання тексту в кожному елементі LazyColumn може знижувати FPS на слабких пристроях. Оптимізація: кешуйте результат через remember і обмежуйте розмір тексту.

Підсумки

  • TextMeasurer — Compose-API для вимірювання тексту до рендерингу, повертає TextLayoutResult.
  • Метод measure() приймає текст, стиль і Constraints у пікселях.
  • Екземпляр створюється через rememberTextMeasurer() і перевикористовується.
  • Для однорядкового тексту результат використовується для адаптивної ширини контейнера.
  • Для багаторядкового — lineCount, getLineWidth() і getBoundingBox() дають повну картину.
  • У кастомних Layout TextMeasurer дозволяє вирівняти елементи по першому рядку тексту.
  • Кешуйте результат через remember і уникайте вимірювання в гарячих recomposition-шляхах.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також