پشتیبانی از سایزهای مختلف صفحه در Jetpack Compose


تو این پست میخوایم بریم سراغ مشکل سایزهای مختلف که موقع استفاده از اندازههای ثابت (hard-coded) توی رابط کاربری پیش میاد، و یه روش بهتر برای پشتیبانی از صفحهنمایشهای مختلف یاد بگیریم.
از اول هم گفتن برای سایزهای مختلف از dp استفاده کنید
از قدیمالایام که اندروید اومده بود، همیشه توصیه میشد به جای Pixel از واحد dp (یا dip) استفاده کنیم برای طراحی رابط کاربری و سایزهای مختلف. چرا؟ چون dp باعث میشه طراحی توی صفحهنمایشهایی با اندازه و چگالی مختلف، راحتتر و قابل کنترلتر بشه.
«dp یه واحد پیکسلی مجازیه که به اندازه ۱ پیکسل روی صفحهای با 160dpi (یعنی mdpi) فضا میگیره»
یعنی اگه یه چیزی رو ۵۰dp طراحی کنی، رو یه گوشی قدیمی مثل Nexus One و یه گوشی جدید مثل Pixel 5 تقریباً همون فضای واقعی رو میگیره (اندازه نوک انگشت مثلاً).
ولی یه لحظه فکر کن… خب فضای فیزیکی گوشی کوچیک کمتره دیگه، حتی اگه چگالیش برابر باشه. با این حال ما همون مقادیر dp رو روی گوشی کوچیک و بزرگ استفاده میکنیم!
مشکل کجاست؟ همون اندازه برای Nexus One و Pixel 5
تصویر زیر رو ببین:
(تصویر نشون میده که همون رابط کاربری روی Nexus One و Pixel 5 چه شکلی دیده میشه)
خب، توی Pixel 5 همهچی قشنگ و مرتب دیده میشه. ولی رو Nexus One… اوضاع افتضاحه 😅
Padding زیادی از صفحه رو گرفته، سایز نوشتهها خیلی بزرگه، دکمه جا نمیشه و بریده شده.
موقعی که اینو روی گوشی واقعی ببینی، متوجه میشی که تجربه کاربری خیلی بده.
خب شاید بگی “اوکی اسکرولش میکنم”، ولی ما میخوایم بهتر از این باشیم، 🤓
راهحل اول استفاده از آبجکت Dimens
اوایل کار اومدیم یه آبجکت Kotlin به اسم Dimens ساختیم که همهی ابعاد خاص اپ رو اونجا تعریف میکردیم. ولی خب مشکلی که داشت این بود که این مقادیر برای همه سایزهای مختلف یکی بودن.
مثلاً Dimens.grid_1 همیشه ۸dp بود، فرقی نمیکرد رو چه گوشیای اجرا بشه.
object Dimens {
val grid_0_25 = 2.dp
val grid_0_5 = 4.dp
val grid_1 = 8.dp
val grid_1_5 = 12.dp
val grid_2 = 16.dp
val grid_2_5 = 20.dp
val grid_3 = 24.dp
val grid_3_5 = 28.dp
val grid_4 = 32.dp
val grid_4_5 = 36.dp
val grid_5 = 40.dp
val grid_5_5 = 44.dp
val grid_6 = 48.dp
}
ایده گرفتن از Themeها
بیایم یه نگاه به کاری که برای رنگها توی تم انجام میدیم بندازیم. اونجا ما دوتا آبجکت رنگی داریم: LightThemeColors و DarkThemeColors. بعد با توجه به اینکه دارکتم فعاله یا نه، یکی رو به CompositionLocal پاس میدیم.
یعنی وقتی داریم رنگها رو توی کامپوز استفاده میکنیم، از طریق تم بهشون دسترسی پیدا میکنیم.
val color = MyTheme.colors.onBackground
حالا بیایم همین روش رو برای ابعاد (dimensions) هم پیاده کنیم!
مطالعه بیشتر : درست کردن داینامیک تم در کامپوز
تعریف دو دسته Dimension برای دو نوع دستگاه
میتونیم یه سری مقدار پیشفرض برای ابعاد تعریف کنیم، و یه سری دیگه هم مخصوص دستگاههایی که smallestWidth اونها حداقل ۳۶۰dp هست.
(دقیقاً مثل اینکه توی xml دوتا فایل dimens داشته باشی: یکی توی res/values و یکی توی res/values-sw360dp)
حتی میتونی بعضی ابعاد رو به صورت پیشفرض بذاری، مثل:
val minimumTouchTarget = 48.dp
class Dimensions(
val grid_0_25: Dp,
val grid_0_5: Dp,
val grid_1: Dp,
val grid_1_5: Dp,
val grid_2: Dp,
val grid_2_5: Dp,
val grid_3: Dp,
val grid_3_5: Dp,
val grid_4: Dp,
val grid_4_5: Dp,
val grid_5: Dp,
val grid_5_5: Dp,
val grid_6: Dp,
val plane_0: Dp,
val plane_1: Dp,
val plane_2: Dp,
val plane_3: Dp,
val plane_4: Dp,
val plane_5: Dp,
val minimum_touch_target: Dp = 48.dp,
)
val smallDimensions = Dimensions(
grid_0_25 = 1.5f.dp,
grid_0_5 = 3.dp,
grid_1 = 6.dp,
grid_1_5 = 9.dp,
grid_2 = 12.dp,
grid_2_5 = 15.dp,
grid_3 = 18.dp,
grid_3_5 = 21.dp,
grid_4 = 24.dp,
grid_4_5 = 27.dp,
grid_5 = 30.dp,
grid_5_5 = 33.dp,
grid_6 = 36.dp,
plane_0 = 0.dp,
plane_1 = 1.dp,
plane_2 = 2.dp,
plane_3 = 3.dp,
plane_4 = 6.dp,
plane_5 = 12.dp,
)
val sw360Dimensions = Dimensions(
grid_0_25 = 2.dp,
grid_0_5 = 4.dp,
grid_1 = 8.dp,
grid_1_5 = 12.dp,
grid_2 = 16.dp,
grid_2_5 = 20.dp,
grid_3 = 24.dp,
grid_3_5 = 28.dp,
grid_4 = 32.dp,
grid_4_5 = 36.dp,
grid_5 = 40.dp,
grid_5_5 = 44.dp,
grid_6 = 48.dp,
plane_0 = 0.dp,
plane_1 = 1.dp,
plane_2 = 2.dp,
plane_3 = 4.dp,
plane_4 = 8.dp,
plane_5 = 16.dp,
)
حالا سیستم چطور بفهمه کدوم رو استفاده کنه؟
بازم از CompositionLocal استفاده میکنیم. توی کد، بررسی میکنیم اگه screenWidthDp حداقل ۳۶۰ بود، از sw360Dimensions استفاده بشه، وگرنه همون smallDimensions.
@Composable
fun ProvideDimens(
dimensions: Dimensions,
content: @Composable () -> Unit
) {
val dimensionSet = remember { dimensions }
CompositionLocalProvider(LocalAppDimens provides dimensionSet, content = content)
}
private val LocalAppDimens = staticCompositionLocalOf {
smallDimensions
}
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) DarkThemeColors else LightThemeColors
val configuration = LocalConfiguration.current
val dimensions = if (configuration.screenWidthDp <= 360) smallDimensions else sw360Dimensions
val typography = if (configuration.screenWidthDp <= 360) smallTypography else sw360Typography
ProvideDimens(dimensions = dimensions) {
ProvideColors(colors = colors) {
MaterialTheme(
colors = colors,
shapes = Shapes,
typography = typography,
content = content,
)
}
}
}
object AppTheme {
val colors: Colors
@Composable
get() = LocalAppColors.current
val dimens: Dimensions
@Composable
get() = LocalAppDimens.current
}
val Dimens: Dimensions
@Composable
get() = AppTheme.dimens
بعدش، توی کامپوزابلها، همینطور که از theme برای رنگ استفاده میکردیم، برای ابعاد هم استفاده میکنیم:
Row(
modifier = Modifier.padding(
horizontal = AppTheme.dimens.grid_2,
vertical = AppTheme.dimens.grid_3
),
) {
...
}
نتیجه؟ خیلی بهتر!
(تصویر قبل و بعد که نشون میده UI روی Nexus One هم الان خیلی بهتر شده)
جمعبندی
تو بیشتر موارد، میتونیم از Modifier.fillMaxWidth() یا wrapContentSize() یا حتی نسبت (aspectRatio) استفاده کنیم. ولی بعضی وقتا نیاز داریم حتماً از ابعاد مشخص استفاده کنیم.
حتی میتونیم ستهای جدیدی برای تبلتها درست کنیم مثل sw600dp (تبلت ۷ اینچ) و sw720dp (تبلت ۱۰ اینچ). قبلاً برای اینا layout جدا میساختیم توی res/layout-sw600dp، ولی الان میتونیم فقط ابعاد مخصوصشون رو تعریف کنیم و از تکرار جلوگیری کنیم. و بتونیم سایزهای مختلف دستگاه ها رو پوشش بدیم
منابع:









