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) гарантирует, что измерение перезапускается только при изменении текста. Constraints заданы с большим 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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