TextMeasurer — to API z Jetpack Compose przeznaczone do mierzenia rozmiarów tekstu przed jego faktycznym wyrenderowaniem na ekranie. W przeciwieństwie do klasycznego podejścia przez Paint.measureText w Android View, TextMeasurer zapewnia natywny dla Compose sposób uzyskania szerokości, wysokości i metryk wierszy z uwzględnieniem stylu i ograniczeń. Według Android Developers Documentation (2025), TextMeasurer jest aktywnie używany w niestandardowych komponentach Layout, animowanych podpisach i adaptacyjnych elementach UI, gdzie rozmiar tekstu wpływa na pozycjonowanie innych elementów.
Najważniejsze
TextMeasurer — to funkcyjny interfejs z pakietu androidx.compose.ui.text, który umożliwia mierzenie tekstu w Compose bez bezpośredniego renderowania na ekranie. Jest częścią silnika tekstowego Compose i działa na poziomie Paragraph API, który wykorzystuje Skia do renderowania.
Głównym przeznaczeniem TextMeasurer jest pomiar wstępny. W Compose rozmiar tekstu jest zwykle określany po tym, jak Composable został już umieszczony w Layout. Istnieją jednak scenariusze, w których trzeba znać szerokość tekstu przed kompozycją: na przykład, aby zdecydować, czy wiersz mieści się w kontenerze, lub aby obliczyć rozmiar przycisku na podstawie długości tekstu.
TextMeasurer rozwiązuje ten problem, udostępniając metodę measure(), która przyjmuje TextMeasureRequest (tekst, styl, ograniczenia) i zwraca TextLayoutResult (metryki, wiersze, znaki). Pozwala to programiście uzyskać wszystkie dane o tekście synchronicznie, bez opóźnienia o jedną klatkę.
Używaj TextMeasurer, gdy rozmiar tekstu wpływa na rozmiar komponentu nadrzędnego, podczas tworzenia animowanych przejść tekstowych lub przy implementacji niestandardowych Layout, gdzie tekst sąsiaduje z innymi elementami.
API TextMeasurer składa się z dwóch kluczowych elementów: rememberTextMeasurer() (tworzenie instancji) i measure() (uruchomienie pomiaru). Instancja jest tworzona raz na cały cykl życia komponentu i ponownie używana do wszystkich pomiarów.
val textMeasurer = rememberTextMeasurer()
val result = textMeasurer.measure(
text = "Hello, Compose!",
style = MaterialTheme.typography.bodyLarge,
constraints = Constraints(maxWidth = 200, maxHeight = 50)
)
// szerokość wyniku, wysokość wyniku, liczba wierszy
Parametr constraints określa maksymalną szerokość i wysokość, w ramach których tekst musi się zmieścić. Jeśli tekst nie mieści się w szerokości, jest zawijany do następnego wiersza. Wynik pomiaru zawiera TextLayoutResult, w którym można uzyskać liczbę wierszy, pozycję każdego znaku, szerokość każdego wiersza i całkowity rozmiar tekstu.
TextMeasurer działa synchronicznie w wątku UI i nie powoduje ponownej kompozycji. Jednak częste mierzenie tekstu z różnymi stylami może obciążać system — dlatego instancja jest tworzona przez remember i ponownie używana.
Najprostszy scenariusz — zmierzyć szerokość jednowierszowego tekstu, aby określić rozmiar kontenera nadrzędnego. Na przykład dla przycisku, którego szerokość zależy od długości etykiety.
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)
}
W tym przykładzie remember(label) gwarantuje, że pomiar jest restartowany tylko przy zmianie tekstu. Constraints są ustawione z dużym maxWidth (1000), aby zapobiec zawijaniu wiersza — faktyczne ograniczenie będzie równe szerokości tekstu. Szerokość Box jest ustawiana na zmierzoną szerokość tekstu, co daje efekt „przycisku dopasowanego do tekstu”.
Uwaga: szerokość z wyniku należy przekonwertować na dp przez .dp, ponieważ TextMeasurer zwraca rozmiary w pikselach, a Modifier.width oczekuje Dp. Dla dokładności użyj density i LocalDensity.current.
Dla tekstu wielowierszowego TextMeasurer pozwala określić nie tylko całkowitą wysokość, ale także liczbę wierszy oraz położenie każdego znaku w akapicie. Jest to niezbędne przy tworzeniu niestandardowych pól tekstowych, czatów lub edytorów.
Główne właściwości TextLayoutResult dla tekstu wielowierszowego: lineCount (liczba wierszy), getLineTop(index) (współrzędna Y początku wiersza), getLineBottom(index) (współrzędna Y końca), getLineWidth(index) (szerokość wiersza). Łącząc je, można precyzyjnie pozycjonować elementy interfejsu względem wierszy tekstu.
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
Metoda getLineWidth(lineIndex) zwraca szerokość określonego wiersza w pikselach. Może być mniejsza niż maxWidth, jeśli wiersz nie sięga granicy kontenera. Znając szerokość każdego wiersza, można ręcznie zaimplementować efekt justowania lub umieścić elementy inline obok krótkich wierszy.
Aby uzyskać pozycję konkretnego znaku, użyj getBoundingBox(offset), który zwraca Rect ze współrzędnymi left, top, right, bottom. Jest to przydatne do pozycjonowania kursora w niestandardowych polach tekstowych lub do podświetlania zakresu tekstu animacją.
Najbardziej zaawansowanym scenariuszem użycia TextMeasurer są niestandardowe komponenty Layout, w których tekst sąsiaduje z innymi elementami, a ich rozmiary są współzależne. Na przykład widget „Ikona + tekst”, w którym ikona powinna być wyśrodkowana względem pierwszego wiersza tekstu, a nie całego bloku.
W standardowym Row ikona jest centrowana względem wysokości całego Row, co wygląda nienaturalnie, gdy tekst zajmuje 3 wiersze, a ikona — jeden. Z TextMeasurer można zmierzyć pierwszy wiersz i wyrównać ikonę właśnie względem niego.
val textMeasurer = rememberTextMeasurer()
Layout(
content = {
Icon(imageVector = Icons.Default.Star, contentDescription = null)
Text(text = "Tekst wielowierszowy tutaj")
},
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)
}
}
)
W tym przykładzie pomiar pierwszego wiersza przez TextMeasurer określa pozycję ikony. Bez TextMeasurer ikona byłaby centrowana względem całej wysokości tekstu wielowierszowego, co jest wizualnie niepoprawne. Ten wzorzec jest szeroko stosowany w czatach, listach kontaktów i kartach informacyjnych.
TextMeasurer — to synchroniczne API działające w wątku UI. Przy nadmiernym użyciu może powodować spadki klatek, szczególnie przy mierzeniu dużych ilości tekstu na słabszych urządzeniach.
Pierwsza zasada — zapisuj wynik w pamięci podręcznej. Zawsze opakowuj wywołanie measure() w remember z zależnościami od tekstu i stylu. Nigdy nie wywołuj measure() wewnątrz CompositionLocal ani na gorących ścieżkach rekompozycji.
Druga zasada — ograniczaj maxWidth i maxHeight. Jeśli ustawisz maxWidth = Constraints.Infinity, TextMeasurer nie będzie mógł zawijać wierszy i tekst wyjdzie poza ekran. Zawsze przekazuj realistyczne ograniczenia, szczególnie dla tekstu wielowierszowego.
Trzecia — unikaj ponownego mierzenia z tymi samymi parametrami. Jeśli tekst i styl się nie zmieniły, wynik measure() będzie taki sam. Łącz pomiar z Deferred lub LaunchedEffect dla scenariuszy asynchronicznych.
Często zadawane pytania
TextMeasurer mierzy tekst przed renderowaniem, a onSizeChanged — po. TextMeasurer jest potrzebny, gdy rozmiar tekstu wpływa na układ rodzica; onSizeChanged nadaje się do reaktywnej logiki po kompozycji.
Tak, TextMeasurer.measure() przyjmuje AnnotatedString, który zawiera SpannableString z stylami inline, linkami i kolorami. Wszystkie spany są uwzględniane przy pomiarze.
TextMeasurer działa na poziomie Skia-renderowania, gdzie wszystkie rozmiary są w pikselach. Konwertuj przez LocalDensity.current: density.run { width.toDp() }.
TextMeasurer używa FontFamily.Default do momentu załadowania niestandardowej czcionki. Dla dokładnego pomiaru poczekaj na załadowanie przez FontResource i użyj async/await.
Mierzenie tekstu w każdym elemencie LazyColumn może obniżać FPS na słabszych urządzeniach. Optymalizacja: zapisuj w pamięci podręcznej wynik przez remember i ograniczaj rozmiar tekstu.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również