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)
)
// عرض نتیجه، ارتفاع نتیجه، تعداد خطوط
پارامتر 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 کمتر باشد. با دانستن عرض هر خط، میتوان اثر justified را به صورت دستی پیادهسازی کرد یا عناصر 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 = "متن چندخطی اینجا")
},
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 زمانی نیاز است که اندازه متن بر layout والد تأثیر میگذارد؛ onSizeChanged برای منطق واکنشی پس از ترکیب مناسب است.
بله، TextMeasurer.measure() AnnotatedString را میپذیرد که شامل SpannableString با سبکهای inline، لینکها و رنگها است. همه spanها در اندازهگیری در نظر گرفته میشوند.
TextMeasurer در سطح رندر Skia کار میکند، جایی که همه اندازهها بر حسب پیکسل هستند. از طریق LocalDensity.current تبدیل کنید: density.run { width.toDp() }.
TextMeasurer تا بارگذاری فونت سفارشی از FontFamily.Default استفاده میکند. برای اندازهگیری دقیق، بارگذاری را از طریق FontResource منتظر بمانید و از async/await استفاده کنید.
اندازهگیری متن در هر عنصر LazyColumn میتواند FPS را در دستگاههای ضعیف کاهش دهد. بهینهسازی: نتیجه را از طریق remember کش کنید و اندازه متن را محدود کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید