TextMeasurer là một API từ Jetpack Compose được thiết kế để đo kích thước văn bản trước khi nó thực sự được hiển thị trên màn hình. Khác với cách tiếp cận cổ điển qua Paint.measureText trong Android View, TextMeasurer cung cấp một cách Compose-native để lấy chiều rộng, chiều cao và các chỉ số dòng có tính đến kiểu và ràng buộc. Theo Android Developers Documentation (2025), TextMeasurer được sử dụng tích cực trong các thành phần Layout tùy chỉnh, nhãn hoạt hình và các yếu tố UI thích nghi nơi kích thước văn bản ảnh hưởng đến vị trí của các yếu tố khác.
Điểm Chính
TextMeasurer là một giao diện chức năng từ gói androidx.compose.ui.text cho phép đo văn bản trong Compose mà không cần hiển thị thực tế trên màn hình. Nó là một phần của công cụ văn bản Compose và hoạt động ở cấp độ Paragraph API, sử dụng Skia để hiển thị.
Mục đích chính của TextMeasurer là đo trước. Trong Compose, kích thước văn bản thường được xác định sau khi Composable đã được đặt trong Layout. Tuy nhiên, có những tình huống bạn cần biết chiều rộng của văn bản trước khi bố cục — ví dụ, để quyết định một chuỗi có vừa với container hay không, hoặc để tính kích thước nút dựa trên độ dài văn bản.
TextMeasurer giải quyết điều này bằng cách cung cấp phương thức measure(), nhận TextMeasureRequest (văn bản, kiểu, ràng buộc) và trả về TextLayoutResult (chỉ số, dòng, ký tự). Điều này cho phép nhà phát triển lấy tất cả dữ liệu văn bản một cách đồng bộ, không bị trễ một khung hình.
Sử dụng TextMeasurer khi kích thước văn bản ảnh hưởng đến kích thước thành phần cha, khi tạo chuyển tiếp văn bản hoạt hình, hoặc khi triển khai Layout tùy chỉnh nơi văn bản cùng tồn tại với các yếu tố khác.
API TextMeasurer bao gồm hai thực thể chính: rememberTextMeasurer() (tạo thể hiện) và measure() (chạy đo lường). Thể hiện được tạo một lần cho toàn bộ vòng đời thành phần và được tái sử dụng cho tất cả các phép đo.
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
Tham số constraints xác định chiều rộng và chiều cao tối đa mà văn bản phải nằm gọn. Nếu văn bản không vừa với chiều rộng, nó sẽ xuống dòng tiếp theo. Kết quả đo chứa TextLayoutResult, cung cấp số dòng, vị trí của từng ký tự, chiều rộng của từng dòng và kích thước tổng thể của văn bản.
TextMeasurer hoạt động đồng bộ trên luồng UI và không kích hoạt tái hợp thành. Tuy nhiên, việc đo văn bản thường xuyên với các kiểu khác nhau có thể tạo ra chi phí — vì vậy thể hiện được tạo qua remember và tái sử dụng.
Kịch bản đơn giản nhất là đo chiều rộng của văn bản một dòng để xác định kích thước container cha. Ví dụ, cho một nút có chiều rộng phụ thuộc vào độ dài nhãn.
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)
}
Trong ví dụ này, remember(label) đảm bảo phép đo chỉ khởi động lại khi văn bản thay đổi. Các ràng buộc được đặt với maxWidth lớn (1000) để ngăn xuống dòng — ràng buộc thực tế sẽ bằng chiều rộng văn bản. Chiều rộng của Box được đặt bằng chiều rộng đo được của văn bản, tạo hiệu ứng “nút vừa với văn bản”.
Lưu ý: chiều rộng từ kết quả cần được chuyển đổi thành dp qua .dp, vì TextMeasurer trả về kích thước bằng pixel, trong khi Modifier.width mong đợi Dp. Để chính xác, hãy sử dụng density và LocalDensity.current.
Đối với văn bản nhiều dòng, TextMeasurer cho phép xác định không chỉ tổng chiều cao mà còn số dòng và vị trí của từng ký tự trong đoạn văn. Điều này cần thiết khi tạo các trường văn bản tùy chỉnh, chat hoặc trình soạn thảo.
Các thuộc tính TextLayoutResult chính cho văn bản nhiều dòng: lineCount (số dòng), getLineTop(index) (tọa độ Y đầu dòng), getLineBottom(index) (tọa độ Y cuối dòng), getLineWidth(index) (chiều rộng dòng). Kết hợp chúng, bạn có thể định vị chính xác các yếu tố giao diện so với các dòng văn bản.
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
Phương thức getLineWidth(lineIndex) trả về chiều rộng của dòng được chỉ định bằng pixel. Nó có thể nhỏ hơn maxWidth nếu dòng không chạm đến ranh giới container. Biết chiều rộng của từng dòng, bạn có thể thực hiện hiệu ứng căn đều thủ công hoặc đặt các phần tử inline bên cạnh các dòng ngắn.
Để lấy vị trí của một ký tự cụ thể, hãy sử dụng getBoundingBox(offset), trả về Rect với các tọa độ left, top, right, bottom. Điều này hữu ích cho việc định vị con trỏ trong các trường văn bản tùy chỉnh hoặc làm nổi bật một phạm vi văn bản bằng hoạt hình.
Ứng dụng mạnh nhất của TextMeasurer là các thành phần Layout tùy chỉnh, nơi văn bản cùng tồn tại với các yếu tố khác và kích thước của chúng phụ thuộc lẫn nhau. Ví dụ, widget “Biểu tượng + văn bản” nơi biểu tượng phải được căn giữa so với dòng đầu tiên của văn bản, không phải toàn bộ khối.
Trong Row tiêu chuẩn, biểu tượng được căn giữa theo toàn bộ chiều cao của Row, trông không tự nhiên khi văn bản chiếm 3 dòng trong khi biểu tượng chỉ một. Với TextMeasurer, bạn có thể đo dòng đầu tiên và căn chỉnh biểu tượng chính xác với nó.
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)
}
}
)
Trong ví dụ này, việc đo dòng đầu tiên qua TextMeasurer xác định vị trí của biểu tượng. Không có TextMeasurer, biểu tượng sẽ được căn giữa theo toàn bộ chiều cao của văn bản nhiều dòng, điều này không đúng về mặt thị giác. Mẫu này được sử dụng rộng rãi trong chat, danh sách liên hệ và thẻ thông tin.
TextMeasurer là API đồng bộ chạy trên luồng UI. Việc sử dụng quá mức có thể gây tụt khung hình, đặc biệt khi đo khối lượng văn bản lớn trên các thiết bị yếu.
Quy tắc đầu tiên — lưu vùng đệm kết quả. Luôn bao bọc lời gọi measure() trong remember với các phụ thuộc văn bản và kiểu. Không bao giờ gọi measure() bên trong CompositionLocal hoặc trong các đường tái hợp thành nóng.
Quy tắc thứ hai — giới hạn maxWidth và maxHeight. Nếu bạn đặt maxWidth = Constraints.Infinity, TextMeasurer sẽ không thể xuống dòng và văn bản sẽ tràn ra ngoài màn hình. Luôn truyền các ràng buộc thực tế, đặc biệt cho văn bản nhiều dòng.
Quy tắc thứ ba — tránh đo lặp lại với cùng tham số. Nếu văn bản và kiểu không thay đổi, kết quả của measure() sẽ giống nhau. Kết hợp đo lường với Deferred hoặc LaunchedEffect cho các kịch bản bất đồng bộ.
Câu hỏi thường gặp
TextMeasurer đo văn bản trước khi hiển thị, trong khi onSizeChanged đo sau. TextMeasurer cần thiết khi kích thước văn bản ảnh hưởng đến bố cục cha; onSizeChanged phù hợp cho logic phản ứng sau khi hợp thành.
Có, TextMeasurer.measure() chấp nhận AnnotatedString, bao gồm SpannableString với kiểu inline, liên kết và màu sắc. Tất cả các span đều được tính đến khi đo.
TextMeasurer hoạt động ở cấp độ kết xuất Skia, nơi tất cả kích thước đều bằng pixel. Chuyển đổi qua LocalDensity.current: density.run { width.toDp() }.
TextMeasurer sử dụng FontFamily.Default cho đến khi phông chữ tùy chỉnh được tải. Để đo chính xác, hãy chờ tải phông qua FontResource và sử dụng async/await.
Đo văn bản trong mỗi mục LazyColumn có thể giảm FPS trên các thiết bị yếu. Tối ưu: lưu vùng đệm kết quả qua remember và giới hạn kích thước văn bản.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm