TextMeasurer هي واجهة برمجية من Jetpack Compose مصممة لقياس أبعاد النص قبل رسمه فعليًا على الشاشة. على عكس النهج التقليدي عبر Paint.measureText في Android View، توفر TextMeasurer طريقة أصلية لـ Compose للحصول على العرض والارتفاع ومقاييس الأسطر مع مراعاة النمط والقيود. وفقًا لوثائق Android Developers Documentation (2025)، تُستخدم TextMeasurer بنشاط في مكونات Layout المخصصة والتسميات المتحركة وعناصر واجهة المستخدم التكيفية حيث يؤثر حجم النص على تحديد موضع العناصر الأخرى.
النقاط الرئيسية
TextMeasurer هي واجهة وظيفية من حزمة androidx.compose.ui.text تسمح بقياس النص في Compose دون رسمه فعليًا على الشاشة. إنها جزء من محرك النص في Compose وتعمل على مستوى Paragraph API الذي يستخدم Skia للعرض.
الغرض الرئيسي من TextMeasurer هو القياس المسبق. في Compose، عادةً ما يتم تحديد حجم النص بعد وضع Composable بالفعل في Layout. ولكن هناك سيناريوهات تحتاج فيها إلى معرفة عرض النص قبل التخطيط — على سبيل المثال، لتحديد ما إذا كانت السلسلة تناسب الحاوية، أو لحساب حجم الزر بناءً على طول النص.
تحل TextMeasurer هذه المشكلة من خلال توفير طريقة measure()، التي تأخذ TextMeasureRequest (نص، نمط، قيود) وتُرجع TextLayoutResult (مقاييس، أسطر، أحرف). يتيح ذلك للمطور الحصول على جميع بيانات النص بشكل متزامن، دون تأخير إطار واحد.
استخدم TextMeasurer عندما يؤثر حجم النص على حجم المكون الأصلي، أو عند إنشاء انتقالات نصية متحركة، أو عند تنفيذ Layouts مخصصة حيث يتعايش النص مع عناصر أخرى.
تتكون واجهة TextMeasurer API من كيانين رئيسيين: 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 بشكل متزامن على سلسلة واجهة المستخدم ولا يسبب إعادة تركيب. ومع ذلك، يمكن أن يؤدي قياس النص المتكرر بأنماط مختلفة إلى إنشاء حمل زائد — لذلك يتم إنشاء المثيل عبر 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 هي واجهة برمجية متزامنة تعمل على سلسلة واجهة المستخدم. يمكن أن يتسبب الإفراط في استخدامها في انخفاض معدل الإطارات، خاصة عند قياس كميات كبيرة من النص على الأجهزة الضعيفة.
القاعدة الأولى — خزّن النتيجة مؤقتًا. دائمًا لف استدعاء 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. سوف نقدم لك النصح ونقترح أفضل حل.