TextMeasurer คือ API จาก Jetpack Compose ที่ออกแบบมาเพื่อวัดขนาดของข้อความก่อนที่จะเรนเดอร์บนหน้าจอจริง แตกต่างจากวิธีการแบบดั้งเดิมผ่าน Paint.measureText ใน Android View TextMeasurer มอบวิธีแบบ Compose-native เพื่อรับความกว้าง ความสูง และเมตริกของบรรทัด โดยคำนึงถึงสไตล์และข้อจำกัด ตาม 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) รับประกันว่าการวัดจะเริ่มต้นใหม่เมื่อข้อความเปลี่ยนแปลงเท่านั้น ข้อจำกัดถูกตั้งค่าด้วย 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 หากบรรทัดไม่ถึงขอบเขตของคอนเทนเนอร์ เมื่อทราบความกว้างของแต่ละบรรทัด คุณสามารถใช้งานเอฟเฟกต์ชิดขอบด้วยตนเองหรือวางองค์ประกอบแบบอินไลน์ข้างบรรทัดสั้นๆ
เพื่อรับตำแหน่งของอักขระเฉพาะ ให้ใช้ 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 หรือในเส้นทางการประกอบใหม่ที่ร้อน
กฎข้อที่สอง — จำกัด maxWidth และ maxHeight หากคุณตั้งค่า maxWidth = Constraints.Infinity TextMeasurer จะไม่สามารถตัดบรรทัดและข้อความจะล้นออกนอกหน้าจอ ควรส่งข้อจำกัดที่สมจริงเสมอ โดยเฉพาะสำหรับข้อความหลายบรรทัด
กฎข้อที่สาม — หลีกเลี่ยงการวัดซ้ำด้วยพารามิเตอร์เดียวกัน หากข้อความและสไตล์ไม่เปลี่ยนแปลง ผลลัพธ์ของ measure() จะเหมือนเดิม รวมการวัดกับ Deferred หรือ LaunchedEffect สำหรับสถานการณ์แบบอะซิงโครนัส
คำถามที่พบบ่อย
TextMeasurer วัดข้อความก่อนการเรนเดอร์ ในขณะที่ onSizeChanged วัดหลังจากนั้น TextMeasurer จำเป็นเมื่อขนาดข้อความส่งผลต่อเลย์เอาต์หลัก onSizeChanged เหมาะสำหรับลอจิกเชิงรับหลังการประกอบ
ได้ TextMeasurer.measure() รองรับ AnnotatedString ซึ่งรวมถึง SpannableString ที่มีสไตล์อินไลน์ ลิงก์ และสี สแปนทั้งหมดจะถูกนำมาพิจารณาในการวัด
TextMeasurer ทำงานที่ระดับ การเรนเดอร์ Skia ซึ่งขนาดทั้งหมดเป็นพิกเซล แปลงผ่าน LocalDensity.current: density.run { width.toDp() }.
TextMeasurer ใช้ FontFamily.Default จนกว่าแบบอักษรที่กำหนดเองจะถูกโหลด เพื่อการวัดที่แม่นยำ ให้รอการโหลดแบบอักษรผ่าน FontResource และใช้ async/await
การวัดข้อความในแต่ละรายการของ LazyColumn อาจลด FPS บนอุปกรณ์ที่อ่อนแอ การปรับปรุง: แคช ผลลัพธ์ผ่าน remember และจำกัดขนาดข้อความ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม