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) гарантирует, что измерение перезапускается только при изменении текста. 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, если строка не достигает границы контейнера. Зная ширину каждой строки, можно реализовать эффект «флажка» (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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также