بومسازگان Android بسیار گسترده است و کاربران بهطور یکپارچه بین تلفنها، رایانههای لوحی، دستگاههای تاشو، و محیطهای رایانه جابهجا میشوند. اکنون که Compose First هستیم، این جلسه به بررسی نحوه معماری برنامه شما برای پنجرههای قابلتغییر اندازه و نسبتهای ابعادی متنوع میپردازد—جایی که فرضهای چیدمان سنتی ازهم میپاشند و منجر به رابطهای کاربری کشیده و ازدست رفتن حالت میشوند.
کاوش کنید که چگونه جدیدترین قابلیتها در Jetpack Compose، Navigation 3، و WindowManager میتوانند به شما کمک کنند چیدمانهای پویایی بسازید که بهطور طبیعی از صفحهنمایشهای فشرده به نمایشگرهای دو قابی مقیاسبندی میشوند. با راهبردهایی برای تسریع گردش کار توسعه بااستفاده از Gemini و مهارتهای ما، مانند مهارت تطبیقی، برای بازسازی خودکار پایگاههای کد قدیمی برای صفحهنمایشهای بزرگتر آشنا شوید.
MediaQuery API آزمایشی بهصورت اعلانی قابلیتهای فعال واسط کاربر را مانند اندازه پنجره و وضعیتهای تاشو ردیابی میکند. این روش جایگزین متصل کردن چندین مدیر پلاتفرم قدیمی به یکدیگر در حالت Compose میشود.Grid و FlexBox واحدهای سازنده چیدمان تطبیقی را ارائه میدهند. Grid امکان چیدمانهای چندبعدی پارامتریشده را در یک ترکیبپذیر واحد فراهم میکند، درحالیکه FlexBox توزیع و پیچیدن فضایی یکبعدی را ارائه میدهد.آوانویسی
فرانچسکو رومانو
سلام به همه.
امروز، میخواهیم بهطور عمیق به جنبههای فنی مقیاسبندی برنامه شما برای هر صفحهنمایش بااستفاده از Jetpack Compose بپردازیم.
چرا مقیاسبندی برای هر صفحهنمایش؟
زیرا بومسازگان Android در انواع مختلفی از ابعاد گسترش یافته است.
APK شما میتواند در تلفنها، دستگاههای تاشو، رایانههای لوحی، Chromebook و Googlebook، تلویزیونها، سیستمهای اطلاعات سرگرمی خودرو، دستگاههای XR، و غیره اجرا شود.
بنابراین اولین چیز این است که با کاربران خود در جایی که هستند ملاقات کنید.
و دوم، تغییر پلاتفرم Android بهسمت برنامههای تطبیقی، گذرا نیست.
اینطور نیست که به دنیای فقط تلفن برگردیم، این فناوری ماندگار است.
و من یک خط زمان سریع در اینجا دارم.
در سال ۲۰۲۲، شروع به پایهگذاری کردیم.
ما Android 12L، اولین نسخه Jetpack WindowManager را داشتیم، میاناهای برنامهسازی کاربردی مانند کلاسهای اندازه پنجره را به شما ارائه دادیم، و اولین نسخه از دستورالعملهای کیفیت برنامه تطبیقی را منتشر کردیم.
سپس، Google Play را نیز بهروز کردیم تا ردهبندیها براساس نوع دستگاه تقسیم شوند و شروع کردیم به گنجاندن دستورالعملهای کیفیت برنامه تطبیقی در تصمیمات رتبهبندی.
در Android 15، پنجرهبندی رایانهای را هم در AOSP قرار دادیم و Compose Material 3 Adaptive را به نسخه پایدار رساندیم.
و سپس از Android 16، کیت توسعه نرمافزار ۳۶، سیستم بهسادگی قفلهای جهت، قفلهای نسبت ابعادی، و پرچمهای قابلیت تغییر اندازه را در صفحهنمایشهای بزرگتر نادیده میگیرد.
همراه با انتشار نمایشگرهای متصل، اکنون APK شما میتواند تقریباً در هر اندازه پنجرهای اجرا شود.
و کیت توسعه نرمافزار ۳۶ یک پرچم مانیفست موقت برای انصراف از لغو جهت و نسبت ابعادی به شما ارائه داد.
در Android 17، این گزینه انصراف حذف شده است.
بنابراین، ساختن بهصورت تطبیقپذیر، یک الزام پلاتفرم پایه است.
اکنون، درطول دهه گذشته، ما از نزدیک با تیمهای سراسر بومسازگان کار کردهایم و مستقیماً متوجه شدهایم که چرا ساختن برنامههای سازگار میتواند دشوار باشد.
برخیاز وضعیتها را ازدست دادهاید، میاناهای برنامهسازی کاربردی پنجره تکهتکه شدهاند، سیگنالهای نامشخصی درباره زمان تغییر چیدمان وجود دارد، و برخیاز فرضهای قدیمی در عمق پایگاه کد قرار گرفتهاند.
بنابراین، هر آنچه را که از این دهه چالشهای توسعهدهندگان آموخته بودیم، مستقیماً در دو جا پیادهسازی کردیم: اول، در خود جعبهابزار واسط کاربر.
امروز، برخیاز قابلیتهای جدید مهم در Jetpack Compose را بررسی خواهیم کرد: پرسمان رسانه، جدول، FlexBox، و Navigation 3.
اما همچنین در ابزارهای عاملگرا که کد را با شما ازطریق مهارتهای رسمی Android برای Gemini در Android Studio و Android CLI مینویسند.
برای مثال، مهارتهای تطبیقی Compose را که اوایل امسال در Google I/O منتشر شد خواهیم دید.
این ابزار ازقبل میتوانست چیدمانهای تلفن را به اندازههای پنجره بزرگتر انتقال دهد.
اکنون، این ابزار میتواند ازدست رفتن وضعیت را نیز برطرف کند، تجربههای منحصربهفرد دستگاههای تاشو مثل حالت رومیزی را پیادهسازی کند، و با کشف و پیادهسازی میانبرهای صفحهکلید، مکاننماهای سفارشی، نکات ابزار، و غیره، پشتیبانی از ورودی را بهبود بخشد.
بسیارخوب، بیایید با Jetpack Compose شروع کنیم که پایه همه اینها است.
Jetpack Compose در سال ۲۰۲۱ پایدار شد، بنابراین این پنجمین سالگرد آن است.
و درطول این پنج سال، در هزاران برنامه تولیدی دیدهایم که ساختن برنامههای تطبیقی با Compose چقدر آسانتر و قابلاطمینانتر است.
توسعه Android اکنون رسماً Compose-first است.
این یعنی همه چیدمانهای تطبیقی مدرن، سبکبندی، و مؤلفههای پیمایش جدید برای Compose ساخته شدهاند.
و از دیدگاه معماری سیستم، بزرگترین مزیت Compose برای برنامههای تطبیقی نسبتبه سیستم نمای قدیمی، نحوه تعامل وضعیت و میانای کاربر است.
و برای توضیح این موضوع، میخواهم مقایسه سریعی با سیستم نما انجام دهم.
بنابراین، به نحوه عملکرد سیستم نمایشی فکر کنید.
رابط کاربری شما درختی از اشیاء قابلتغییر بود که یکبار از فایلهای XML ثابت ایجاد میکردید.
توصیفگرهای منبع متفاوتی داشتید، و بیایید با مشکل اصلی در اینجا شروع کنیم: تکرار در توصیفگرهای منبع مختلف.
وقتی واسط کاربری تطبیقی در XML میسازید، هیچ منطق شرطیای در فایل چیدمان ندارید.
درعوض، فقط کل سلسلهمراتب را در پوشههای منبع، مانند layout، layout smallest window 600 dp، layout land، و غیره تکرار میکنید.
بسیارخوب، به مثال EditText در سمت چپ نگاه کنید.
<LinearLayout android:orientation="vertical">
<EditText
android:id="@+id/text_msg"
android:inputType="textCapCharacters"
android:maxLength="12" />
</LinearLayout><ConstraintLayout>
<TextInputEditText
android:id="@+id/text_input"
android:inputType="text" />
</ConstraintLayout>این دقیقاً همان مؤلفه منطقی است که ورودی نوشتاری را مدیریت میکند و در فایلهای XML مختلف پراکنده است.
اگر مشخصهای مانند inputType یا maxLength را در چیدمان تلفن بهروز کنید، هیچچیز در کامپایلر شما را مجبور نمیکند که آن را در چیدمان رایانه لوحی نیز بهروز کنید.
و چون مؤلفهها در فایلهای مختلف ازهم جدا شدهاند، انحراف ساختاری درطول زمان تقریباً تضمینشده است.
و این تکرار به مشکل دیگری نیز مرتبط است که ازدست رفتن وضعیت درطول تغییرات پیکربندی است.
بهخاطر داشته باشید که در سیستم View، مؤلفههای واسط کاربر دارای حالت هستند.
EditText مالک میانگیر «قابلویرایش» داخلی، موقعیت مکاننمای سفارشی، وضعیت تمرکز، و برخی وضعیتهای دیگر است.
بنابراین، وقتی کاربر، مثلاً، دستگاه را باز میکند یا اندازه پنجره را از آستانه خاصی تغییر میدهد، Android کل سلسلهمراتب نما را ازبین میبرد و فایلهای XML جدید را از ابتدا ایجاد میکند.
برای ذخیره کردن ورودی کاربر، چارچوب به onSaveInstanceState متکی است که وضعیت پیوندشده به شناسه آن نما را بستهبندی میکند.
اما این کار سه حالت خرابی احتمالی ایجاد میکند.
اول اینکه اگر کسی بهطور تصادفی شناسه را در XML دیگر بهجای text_msg، text_input نامگذاری کند، بازیابی وضعیت بدون هیچ هشداری نوشتار ورودی کاربر را حذف میکند.
دوم، onRestoreInstanceState پساز onViewCreated اجرا میشود که میتواند مکرراً با ناظران ViewModel تداخل داشته باشد و باعث ایجاد حلقههای callback شود.
و سوم، اگر نوع ابزاره را در اصلاحکنندهها عوض کنید، مثلاً در صفحهنمایش فشرده شاید بخواهید از اسپینر استفاده کنید و در صفحهنمایش عریضتر از RadioGroup، اگر شناسه یکسانی داشته باشند، این کار در زمان اجرا با ClassCastException از کار میافتد.
در Jetpack Compose، درعوض، میانای کاربر شما شیئی نیست که یکبار آن را بسازید.
میانای کاربر شما تابع محض حالت است.
And that change completely what a configuration change even is.
تغییر پیکربندی در Compose چیست؟
یک راهنمایی دارم.
فرانچسکو رومانو
آره.
در Compose، تغییر پیکربندی فقط یک ترکیببندی مجدد دیگر است و ترکیببندیهای مجدد همیشه اتفاق میافتند.
وقتی پنجره عریضتر میشود، شاید به این دلیل که کاربر دستگاه را باز کرده است، ترکیبپذیرهای شما اندازه جدید را میخوانند و آنهایی که خروجیشان به آن بستگی دارد، دوباره اجرا میشوند.
کل رویداد همین است.
اما اکنون، بیایید ببینیم نوشتن برنامههای تطبیقی مدرن بااستفاده از جدیدترین ابزارها و مؤلفههای Compose چه معنایی دارد.
بنابراین، دنیای XML باعث شد سؤالاتی مانند «آیا من روی تلفن، رایانه لوحی، یا رایانه اجرا میشوم؟» بپرسید و سپس واسط کاربر را براساس دسته دستگاه منشعب کنید.
اما دلیل اساسیای برای اجتناب از انجام این کار وجود دارد.
حتی هنگام اجرا روی همان دستگاه فیزیکی، بافتار برنامه و قابلیتهای میانای کاربر میتواند بهطور پویا در زمان اجرا تغییر کند.
بنابراین، پیامی که میخواهم بگذارم این است که تصمیمات UI باید توسط قابلیتهای فعال فعلی هدایت شوند: اندازه پنجره فعلی، وضعیت، برخی ویژگیهای سختافزاری.
بنابراین، بیایید یک مثال کلاسیک را ببینیم که پنجرهبندی رایانهای در مانیتور خارجی است.
اگر تلفن Pixel خود را بردارید و آن را به یک مانیتور خارجی روی میزتان وصل کنید، همان فرایند برنامهای که روی صفحهنمایش عمودی ۶ اینچی درحال اجرا بود، اکنون روی مانیتور عریض ۳۲ اینچی با صفحهکلید فیزیکی و موشواره متصل اجرا میشود.
بنابراین، بررسی نوع دستگاهی که برنامه در آن اجرا میشود هیچ تضمینی درباره صفحهنمایشی که برنامه در آن نمایش داده میشود یا روشهای ورودی ارائه نمیدهد.
علاوهبراین، Android از چندپنجرهای پشتیبانی میکند، بنابراین اندازه پنجرهها میتواند تغییر کند.
حتی اگر صفحهنمایش Googlebook بزرگتر از تلفن باشد، ممکن است برنامه شما در پنجرهای با عرض ۴۰۰ dp در کنار برنامههای دیگر اجرا شود.
بنابراین، پرسمان سنجههای نمایش فیزیکی ایستا نیز میتواند بیمعنی باشد.
بنابراین، برای معماری برنامهها برای Compose ابتدا باید نحوه پرسوجو از محیط را درک کنیم.
و میخواهم با تلاش برای رسمی کردن مفهوم قابلیتهای واسط کاربر شروع کنم.
قابلیتهای واسط کاربر نهتنها شامل عرض و ارتفاع پنجره یا وضعیت تاشو است، بلکه شامل حالتهای ورودی و خروجی است که درحالحاضر فعال هستند.
دیدهایم که در محیط رایانه، کاربران همیشه از صفحه لمسی استفاده نمیکنند.
میتوانند از صفحهکلید و موشواره استفاده کنند.
در سیستم خودرو، برخیاز خودروها دکمه چرخشی یا ورودی متفاوتی دارند، روش متفاوتی برای وارد کردن سیگنالها.
و همچنین، علاوهبر اندازه صفحهنمایش و ورودی، فاصله کاربر از نمایشگر، مثل فاصله دید، یا اینکه آیا دوربین یا میکروفون سختافزاری درحالحاضر متصل است یا نه.
اینها همه قابلیتهای واسط کاربر است.
فرانچسکو رومانو
اکنون، اگر میخواستید همه سیگنالها را بازیابی کنید، باید ۵ یا ۶ میانای برنامهسازی کاربردی مختلف را از منابع مختلف به هم متصل میکردید.
به شیء پیکربندی، WindowMetricsCalculator، WindowInfoTracker، InputManager، PackageManager، و سپس تبدیل دستی هرکدام از آنها به یک حالت، یک حالت Compose، نیاز داشتید.
آیا از انجام آن لذت بردید؟
من نبودم.
برای حل این مشکل، MediaQuery API را در androidx.compose.ui معرفی کردیم.
mediaQuery { ... }
derivedMediaQuery { ... }این ویژگی از پُرسمانهای رسانه CSS الهام گرفته شده است.
و کلمه رسانه بهمعنای بازپخش صدا یا ویدیو نیست.
این اصطلاح به رسانه، یعنی محیطی که واسط کاربری شما در آن ارائه میشود، اشاره دارد.
و MediaQuery روشی واحد، اعلانی، و بومی Compose برای پُرسمان کردن قابلیتهای میانای کاربر دراختیارتان قرار میدهد.
بیایید ویژگیهای افشاشده در UiMediaScope را بررسی کنیم.
sealed interface UiMediaScope {
@get:FrequentlyChangingValue val windowWidth: Dp
@get:FrequentlyChangingValue val windowHeight: Dp
val windowPosture: Posture // Flat, Tabletop, Book
val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
val keyboardKind: KeyboardKind // Physical, Virtual, None
val hasCamera: Boolean
val hasMicrophone: Boolean
val viewingDistance: ViewingDistance // Near, Medium, Far
}windowWidth و windowHeight در Dp.
windowPosture، که درحالحاضر برای دستگاههای تاشو است و میتواند «تخت»، «رومیزی»، یا «کتابی» باشد.
pointerPrecision دقت دستگاه اشارهگر اصلی را به شما میگوید: «دقیق» برای موشواره، قلم، یا پد لمسی؛ «درشت» برای لمس؛ «کند» برای دسته فرمان، کنترلکنندهها، یا «هیچکدام».
keyboardKind میتواند فیزیکی، مجازی، یا هیچکدام باشد.
hasCamera و hasMicrophone بهطور پویا دردسترس بودن این سختافزار را ردیابی میکنند.
به دوربین خارجیای فکر کنید که بتوان آن را به برق وصل کرد.
و viewingDistance گزارشهای «نزدیک»، «متوسط»، یا «دور».
اما فکر میکنم اگر به یک مثال واقعی نگاه کنیم، آسانتر است، که تشخیص میدهد آیا دستگاه در حالت «رومیزی» است یا نه.
حالت افقی روبهصورت وضعیتی است که به کاربران امکان میدهد دستگاه را تا نیمه تا کنند و معمولاً آن را روی سطحی قرار دهند.
و برای پخش طولانی ویدیو، تماس تصویری، و عکاسی زمانگریز عالی است.
همچنین برای دنبال کردن دستور پخت یا تماشای نمایش اسلاید بسیار مفید است.
اکنون، اگر میخواستید وضعیت «رومیزی» را قبلاز MediaQuery تشخیص دهید، باید همه این کدها را مینوشتید.
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
val context = LocalContext.current
val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
.collectAsStateWithLifecycle(null)
val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
displayFeature is FoldingFeature &&
displayFeature.state == FoldingFeature.State.HALF_OPENED &&
displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
} ?: false
return if (isTabletopPosture) {
VideoPlayerLayout.Tabletop
} else {
VideoPlayerLayout.Default
}
}باید زمینه را بهدست میآوردید، نمونهای از WindowInfoTracker را دریافت میکردید، windowLayoutInfo را که یک جریان است، یک حالت Compose آگاه از چرخه حیات، جمعآوری میکردید، و سپس displayFeatures را تکرار میکردید تا بررسی کنید که آیا موردی وجود دارد که FoldingFeature باشد، نیمهباز باشد، و افقی باشد.
و این فقط برای تشخیص وضعیت بدن است.
این مقدار زیادی کد تکراری است.
با MediaQuery API جدید، همه اینها به چند خط کد کاهش مییابد.
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}تابع mediaQuery یک پارامتر لامبدا تکی میگیرد و در داخل محدوده گیرنده آن لامبدا، مجموعهای از ویژگیهایی را که بهتازگی دیدیم نشان میدهد.
در این مثال، با بررسی ساده windowPosture در عبارت when، میتوانیم فوراً ترکیبپذیر مناسب را برگردانیم.
و در اینجا دو مثال دیگر از نحوه استفاده از API آورده شده است.
@Composable
fun AdaptiveChatControls(
onStartVideoCall: () -> Unit,
onSendMessage: () -> Unit,
) {
// 1. Return a Dp value directly based on pointer precision
val chipPadding = mediaQuery {
if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
}
// 2. Return a Boolean combining hardware capabilities
val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
val hasPhysicalKeyboard = mediaQuery {
keyboardKind == KeyboardKind.Physical
}
Row(modifier = Modifier.padding(chipPadding)) {
if (canVideoCall) {
IconButton(onClick = onStartVideoCall) {
Icon(Icons.Default.Videocam, contentDescription = "Video call")
}
}
Button(onClick = onSendMessage) {
Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
}
}
}اولین مورد تراکم اطلاعات است.
بنابراین وقتی کاربر از رایانه کیفی یا مانیتور رایانه با موشواره استفاده میکند و دقت اشارهگر «دقیق» است، میتوانیم حاشیه داخلی مؤلفههای میانای کاربر را از ۸ پیکسل مستقل از تراکم به ۲ پیکسل مستقل از تراکم کاهش دهیم تا محتوای بیشتری بهراحتی نشان داده شود، زیرا کاربران از انگشتانشان استفاده نمیکنند و هدفهای لمسی میتوانند کوچکتر باشند.
همچنین، کنشهای مبتنی بر قابلیت.
فرض کنید نوار ورودی گپ داریم، میتوانیم hasCamera و hasMicrophone را بررسی کنیم تا دکمه کنش تماس تصویری را بهطور پویا نشان دهیم یا پنهان کنیم، یا میتوانیم نوع صفحهکلید فیزیکی را ترکیب کنیم تا برخیاز اشارههای میانبر صفحهکلید مانند فشار دادن Ctrl+Enter برای ارسال را نشان دهیم یا ندهیم.
همه اینها در زمان اجرا بازیابی میشوند.
MediaQuery برای آزمایش و تولید پیشنمایش نیز عالی است.
@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
val simulatedMediaScope = object : UiMediaScope {
override val windowWidth = 840.dp
override val windowHeight = 700.dp
override val windowPosture = UiMediaScope.Posture.Tabletop
override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
override val keyboardKind = UiMediaScope.KeyboardKind.Physical
override val hasCamera = true
override val hasMicrophone = true
override val viewingDistance = UiMediaScope.ViewingDistance.Medium
}
CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
VideoPlayerScreen()
}
}بنابراین، اساساً MediaQuery گیرنده خود را ازطریق یک ترکیببندی محلی بهنام LocalUiMediaScope حل میکند.
بنابراین، میتوانید بهسادگی یک UiMediaScope جعلی را ازطریق CompositionLocalProvider در پیشنمایش خود ارائه دهید و نیازی به راهاندازی دستگاههای تاشو و قرار دادن آنها در زاویه ۹۰ درجه یا اتصال ماوس فقط برای اجرای آزمایش Compose UI ندارید.
فرانچسکو رومانو
بسیارخوب، اکنون میدانیم چگونه قابلیتهای واسط کاربر را پُرسمان کنیم.
اکنون، بیایید ببینیم چگونه چیدمان واقعی را بسازیم.
برای این کار، میخواهم Grid را معرفی کنم که یک چیدمان اولیه جدید در androidx.compose.foundation.layout است و اکنون پایدار است.
Grid(
modifier = Modifier.fillMaxSize().padding(16.dp),
config = {
val maxWidthDp = constraints.maxWidth.toDp()
val columnCount = if (maxWidthDp < 600.dp) 2 else 4
repeat(columnCount) {
column(1.fr)
}
row(GridTrackSize.Auto)
row(1.fr)
gap(row = 16.dp, column = 8.dp)
flow = GridFlow.Row
}
) {
// GridScope children...
}«جدول» به شما امکان میدهد با نمایش موارد در ستونها و ردیفهایی که با اندازه محفظه موجود سازگار میشوند، چیدمانی دوبعدی را پیادهسازی کنید.
بیایید ببینیم چه کارهایی میتوانید در بلوک پیکربندی انجام دهید.
میتوانید با فراخوانی ستون یا ردیف، عناصر یا قطعههای جدیدی را اضافه کنید و میتوانید از حلقههای استاندارد Kotlin مانند repeat استفاده کنید.
میتوانید بین قطعهها حاشیه تنظیم کنید.
میتوانید جهت جایگذاری خودکار را ازطریق جریان-- GridFlow.Row یا GridFlow.Column کنترل کنید.
و در اینجا جزئیات معماری کلیدی آورده شده است.
بلوک پیکربندی درطول گذر اندازهگیری اجرا میشود و محدوده پیکربندی شبکه تراکم را پیادهسازی میکند و محدودیتها را نشان میدهد.
یعنی میتوانید constraints.maxWidth.toDp() را درست در داخل پیکربندی بازرسی کنید تا قطعهها را بدون هیچگونه پاس بازترکیب اضافی محاسبه کنید.
در Grid، وقتی فرزند اضافه میکنید-- وقتی فرزندی بدون اصلاحکننده اضافه میکنید، بهطور خودکار به سلول بعدی منتقل میشود.
Grid(
config = {
repeat(3) { column(1.fr) }
repeat(3) { row(100.dp) }
gap(8.dp)
}
) {
// Spans 2x2 area (rows 1..2, columns 1..2)
HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
// Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
LiveBadge(
modifier = Modifier.gridItem(
row = 1,
column = 1,
alignment = Alignment.TopStart
)
)
// Negative index: placed in last row (-1), spanning all 3 columns
FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}وقتی کنترل صریح میخواهید، میتوانید از Modifier.gridItem استفاده کنید و مختصات یکپایه دارید.
بنابراین، ردیف ۱، ستون ۱ سلول بالا سمت راست است.
از نمایهسازی منفی پشتیبانی میکند، بنابراین ردیف ۱- آخرین ردیف است و به همین ترتیب.
میتوانید از rowSpan برای گسترش دادن یک مورد در یک ناحیه دوبعدی استفاده کنید، و همچنین همپوشانی وجود دارد.
بنابراین، اگر دو مورد در سلولهای همپوشان قرار گیرند، Grid آنها را در آن محدودهها رویهم قرار میدهد و ترتیب Z براساس ترتیب ترکیببندی تعیین میشود.
بنابراین، موارد بعدی روی موارد دیگر کشیده میشوند، درست مثل Box.
اکنون با اصول اولیه Grid آشنا هستیم.
بیایید به نمونه پخشکننده ویدیو نگاه کنیم.
بسته به اینکه دستگاه صاف باشد یا نیمهباز، باید بین چیدمان پیشفرض یا چیدمان رومیزی جابهجا شویم.
قبلاز Grid، باید دو ترکیبپذیر متفاوت، DefaultLayout و TabletopLayout، ایجاد میکردیم و سپس کد را در یک عبارت when منشعب میکردیم.
@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
when(currentVideoPlayerLayout()) {
VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
}
}و ممکن است فکر کنید، «این چه اشکالی دارد؟»
خب، اگر به این چیدمانها نگاه کنید، محتوای آنها یکسان است.
هر دو نمایشگر دقیقاً همان پخشکننده و دقیقاً همان کنترلکننده را نشان میدهند.
تنها تفاوت بین آنها نحوه چیدمان این دو عنصر در صفحه است.
تاکنون، چون راهی برای ارسال خودِ آرایش چیدمان بهعنوان پارامتر نداشتیم، مجبور بودیم دو ترکیبشدنی جداگانه تعریف کنیم.
اکنون میتوانیم پیکربندی چیدمان را پارامتری کنیم و همه چیز را در یک تابع ترکیبپذیر مدیریت کنیم.
و خواهیم دید چگونه.
بنابراین، با Grid API، میتوانیم مناطق نامگذاریشده را بهعنوان شمارش تعریف کنیم.
enum class VideoPlayerArea { Player, Controller }
Grid(
config = { ... },
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}سپس، میتوانیم ترکیبپذیرهای فرزند، پخشکننده و کنترلکننده را برای قرار دادن در شبکه مشخص کنیم و میتوانیم هر فرزند را با یک ناحیه نامگذاریشده با فراخوانی Modifier.gridItem مرتبط کنیم و areaId را ارسال کنیم.
و در اینجا به Grid میگوییم که پخشکننده را در ناحیهای بهنام VideoPlayerArea.Player و کنترلکننده را در VideoPlayerArea.Controller قرار دهد.
و در اینجا دو لامبدا پیکربندی را در VideoPlayerGridConfiguration تعریف میکنیم.
object VideoPlayerGridConfiguration {
val Default: GridConfigurationScope.() -> Unit = {
row(1f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
}
val Tabletop: GridConfigurationScope.() -> Unit = {
row(0.5f)
row(0.5f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
}
}برای چیدمان پیشفرض پخشکننده ویدیو، شبکهای با یک ردیف و یک ستون پیکربندی میکنیم، اما توجه داشته باشید که حتی اگر فقط یک سلول وجود داشته باشد، همچنان میتوانیم دو نام ناحیه را به همان سلول اختصاص دهیم.
در این مورد، همانطور که قبلاً دیدهایم، کنترلر روی پخشکننده همپوشانی میشود.
برای حالت افقی رو بهصورت، فقط دو ردیف و یک ستون تعریف میکنیم، پخشکننده را در نیمه بالایی و کنترلکننده را در نیمه پایینی قرار میدهیم، اساساً به این صورت.
وقتی آن لامبداهای پیکربندی را تعریف کردیم، میتوانیم از MediaQuery مستقیماً در پیکربندی شبکه استفاده کنیم.
@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Grid(
config = { mediaQuery {
if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
else { VideoPlayerGridConfiguration.Default }
}
}
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}
}بنابراین، اکنون میتوانیم یک طرحبندی پخشکننده ویدیو واحد داشته باشیم که با رابط کاربری ترکیبپذیر است و بهطور پویا بین دو پیکربندی جابهجا میشود.
مفهوم اولیه بعدی که میخواهم درباره آن صحبت کنم FlexBox است.
@Composable
fun AdaptiveActionHeader(tags: List<String>) {
FlexBox(
config = {
direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
wrap(FlexWrap.Wrap)
alignItems(FlexAlignItems.Center)
gap(row = 8.dp, column = 12.dp)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Home",
modifier = Modifier.flex {
basis(FlexBasis.Auto)
grow(1f)
shrink(1f)
}
)
tags.forEach { tag ->
FilterChipItem(
label = tag,
modifier = Modifier.flex { shrink(0f) }
)
}
}
}بنابراین، درحالیکه Grid چیدمانهای ردیف و ستون دوبعدی را مدیریت میکند، FlexBox که اکنون نیز پایدار است، چیدمانهای یکبعدی را مدیریت میکند که هنوز به توزیع فضای هوشمند، کوچکسازی، و پیچیدن چندخطی نیاز دارند.
و این یک چیدمان راحت و واکنشگرا است که میتوانید بهجای دو ترکیبپذیر جداگانه، مانند FlowRow و FlowColumn، استفاده کنید، زیرا یک ظرف واحد است که جهت آن میتواند بهطور پویا در داخل بلوک پیکربندی آن بدون بازآفرینی ترکیبپذیرهای فرزند تغییر کند.
و الگوریتم کامل CSS FlexBox را پیادهسازی میکند، بنابراین wrap، justify-content، align-items، و gap را دارید.
و در هر فرزند، Modifier.flex دارید که به شما مبنا، رشد نسبی، عوامل کوچک شدن، alignSelf، و غیره را میدهد.
فرانچسکو رومانو
بسیار خوب، اکنون بیایید از تعریف سطح صفحه به چیدمانهای سطح برنامه برگردیم، اینکه چگونه پیمایش سطح بالا و چیدمانهای صفحه چندبخشی را در کل برنامه، نه فقط در یک صفحه، ساختاردهی میکنیم.
درحالیکه MediaQuery عرض و ارتفاع دقیق پنجره را در Dp به شما میدهد، برای معماری در سطح برنامه، به نقاط شکست نمای استانداردشده و مشخصی نیاز دارید که همان کلاسهای اندازه پنجره هستند.
اساساً، میخواهید هر صفحه در هریک از این دستهها واکنشگرا باشد، درحالیکه میتوانید تغییرات و بهروزرسانیها و تفاوتهای ساختاری بیشتری بین دستههای مختلف داشته باشید.
برای مثال، با نشان دادن فقط یک فهرست در صفحهنمایش فشرده یا هم فهرست و هم جزئیات در پنجره ازهم بازشده.
میتوانید کلاسهای اندازه پنجره را مستقیماً روی UiMediaScope به روشی بسیار تمیز بسازید، که تعریف یک ویژگی افزونه در UiMediaScope است.
val UiMediaScope.windowSizeClass: WindowSizeClass
@Composable get() = derivedMediaQuery {
WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
widthDp = windowWidth.value,
heightDp = windowHeight.value
)
}
@Composable
fun AppRoot() {
val sizeClass = mediaQuery { windowSizeClass }
val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
)
// Drive app-level navigation and scene strategies...
}یک نکته قابلتوجه این است که از derivedMediaQuery استفاده میکند، نه MediaQuery استاندارد.
چرا؟
زیرا پهنا و ارتفاع پنجره مقادیری هستند که مرتباً تغییر میکنند.
وقتی کاربران اندازه برنامهتان را تغییر میدهند، مثلاً در رایانه، این مقادیر میتوانند در هر قاب تغییر کنند.
و احتمالاً نمیخواهید داربست سطح برنامه را در هر فریم بازسازی کنید، بلکه فقط زمانی که پنجره واقعاً از نقطه شکست عبور میکند.
و وقتی کلاسهای اندازه پنجره را داشته باشیم، چگونه برنامهمان را از جریان تکقاب به چیدمانهای چندقاب منتقل کنیم وقتی فضای بیشتری داریم؟
اینجا است که Jetpack Navigation 3 میدرخشد، زیرا Nav 3 از ابتدا براساس حالت و صحنههای Compose طراحی شده است.
و اول، شما مالک پشته قبلیها هستید که صرفاً فهرستی از NavKey است، بنابراین بسیار انعطافپذیر است و بهراحتی بهعنوان یک حالت ذخیره میشود.
هر کلید در پشته به یک NavEntry حاوی ترکیبپذیر و فراداده آن حل میشود.
و NavDisplay نحوه ترسیم ورودیها را کدگذاری سخت نمیکند؛ درعوض، فهرستی از استراتژیهای صحنه را ارزیابی میکند.
در این استراتژی، میتوانید قابلیتهای واسط کاربر را بررسی کنید، برای مثال، کلاسهای اندازه پنجره.
if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}میتوانید پشته قبلیها را بررسی کنید و سپس تصمیم بگیرید که یک NavEntry را در یک قاب واحد، یا چند مورد را بهطور همزمان، یا یکی را روی دیگری رندر کنید.
بسیار انعطافپذیر است.
و در کنار استراتژی چندقاب، کلاسهای اندازه پنجره همچنین تعیین میکنند که از کدام مؤلفههای پیمایش میخواهید برای بهحداکثر رساندن ارگونومی و استفاده از فضای صفحه استفاده کنید.
برای مثال، در عرضهای فشرده، نوار پیمایش پایین، در عرضهای متوسط، NavRail، و در عرضهای گسترده، کشوی پیمایش کامل میخواهید.
وقتی Nav3 را برای تصمیمگیریهای چیدمان در سطح برنامه و مؤلفههای واکنشگرا را برای تعریف در سطح صفحهنمایش ترکیب میکنید، برنامههایی خواهید داشت که اینگونه بهنظر میرسند و تقریباً در هر اندازه پنجره و محیطی میدرخشند.
بهطور خاص برای Nav 3، اگر میخواهید درباره جدیدترین بهروزرسانیها، ازجمله پشتیبانی از پیوندهای عمیق، بیشتر بدانید، به گفتگوی «ساختن پیوندهای عمیق با نوع ایمن با Jetpack Navigation 3» مراجعه کنید.
فرانچسکو رومانو
بسیار خوب، امروز میاناهای برنامهسازی کاربردی جدید زیادی را پوشش دادیم: MediaQuery، Grid، FlexBox، Nav3.
این بود نحوه کمک Jetpack Compose به شما برای ساختن برنامههای تطبیقی.
اما در عصر توسعه عاملی، مزیت بسیار مهم دیگری در استفاده از Jetpack Compose وجود دارد: بسیار مؤثرتر است.
احتمالاً درطول سخنرانی شنیدهاید که برنامههایی را دیدهایم که پساز انتقال به Compose، بیشاز ۳۰٪ کمتر از توکنها استفاده میکنند و ما در تلاشیم تا گردش کار عاملی شما را برای برنامههای Compose حتی بیشتر بهینهسازی کنیم.
Today, I want to highlight the major updates to the Compose Adaptive skill.
android skills list
android skills add adaptiveهمانطور که گفتم، علاوهبر انتقال چیدمانها به Grid، Flexbox، و MediaQuery، اکنون بهطور صریح مدیریت وضعیت سرتاسری را در سراسر برنامه انجام میدهد و ما این مهارت را دربرابر اشکالهای واقعی ازدست دادن وضعیت آزمایش میکنیم: حفظ پیمایش، موقعیت پیمایش، بازپخش رسانه فعال، ورودیهای نوشتاری نیمهتایپشده، و غیره.
این مهارت اکنون شامل بهینهسازی وضعیت تاشو نیز میشود، مانند بازسازی خودکار رسانه و صفحههای دوربین در حالت Tabletop، اما وقتی برنامه شما روی Googlebook یا مانیتور خارجی با ماوس و صفحهکلید اجرا میشود، چیدمان تنها نیمی از داستان است.
مهارت «تطبیقی» اکنون میتواند میانای کاربر شما را ممیزی کند تا میانبرهای صفحهکلید، مکاننماهای سفارشی، حالتهای نگه داشتن نشانگر، نکتهابزارها، و غیره را پیدا و پیادهسازی کند.
پس بیایید آن را در عمل ببینیم.
نسخه نمایشی آن را ضبط کردم.
بنابراین ما برنامه خود را داریم.
میتوانیم به حیوانات خانگیمان نگاه کنیم.
میتوانیم با آنها پیام ردوبدل کنیم.
میتوانیم ویدیویی از آنها را ببینیم.
بله، میتوانیم حیوانات خانگی برتر را ببینیم.
اما خودتان میبینید که این وضعیت خوبی نیست.
حالا بیایید ببینیم من فقط میخواهم عکسی را ببینم که کمی درام و گربه در آن باشد.
بسیارخوب، میخواهم آنها را بزرگتر ببینم.
Dah، یعنی برگشتن به صفحه اصلی.
بسیار خوب، بگذارید به دوستم بگویم.
بله، پس، «بادی»، همم.
برنامه خوب نیست.
همچنین خیلی عریض است.
میبینید که خیلی عریض است.
بیایید اندازه آن را تغییر دهیم.
چی؟
آره، خوبه.
و نوشتاری که تایپ کرده بودم ازدست رفت.
بسیار خوب، وقت آن است که از Gemini بخواهید این مشکل را برطرف کند.
بنابراین، بهلطف مهارت «تطبیقی»، پیامواره میتواند بهسادگی «برنامه را تطبیقی کن» باشد.
همانطور که میبینید، من از Gemini 3.0 Flash استفاده میکنم که با کمک مهارت میتواند این کار را انجام دهد.
مهارت را دریافت کرد و اکنون درحال شروع اعمال تغییرات است.
چند سؤال میپرسد، مثلاً «اوه، MediaQuery آزمایشی است.
واقعاً میخواهید از آن استفاده کنید؟»
و ادامه دهید.
مجبور شدم سرعت آن را افزایش دهم تا چیزهایی را حذف کنم، اما اگر به منطقه نمایشی بیایید، میتوانید خودتان مهارتها را امتحان کنید.
و این فقط یکی از مهارتهایی است که منتشر کردهایم.
برای مثال، مهارت جدیدی هم داریم که به شما کمک میکند تجربههای شگفتانگیزی با قلم ایجاد کنید.
پس به این منطقه بیایید تا آن را امتحان کنید.
بنابراین، انجام شد.
۱۱ فایل را اصلاح کرد.
و بیایید آن را در عمل ببینیم.
بسیارخوب.
Grid درحالحاضر کارش را انجام میدهد.
پیمایش به کنار منتقل شد.
بیایید فیلترها را امتحان کنیم.
فکر کنم الان کار کنند.
بله، همینطور است.
بسیارخوب، بیایید با دوستم پیامرسانی کنیم و به او بگوییم که این برنامه اکنون عالی است.
بگذارید ببینیم با فضای بیشتر چطور بهنظر میرسد.
بله، به چیدمان فهرست-جزئیات منتقل شد.
این اجرا حدود ۲۰ دقیقه طول کشید و حدود ۲۰۰٬۰۰۰ نشانه مصرف کرد.
بسیارخوب.
فرانچسکو رومانو
اکنون، من معمولاً صحبت را با سنجههای کسبوکار و موارد موفقیت شروع میکنم، اما برای ADS، میخواستم مستقیماً به کد بپردازم.
بااینحال، میخواهم چند دقیقه وقت بگذارم تا بازگشت سرمایه مهندسی را همرسانی کنم.
امروزه، برنامه Android شما میتواند بهطور بالقوه در بیشاز ۵۸۰ میلیون دستگاه فعال با صفحهنمایش بزرگ اجرا شود، و همچنین میدانیم که کاربران دستگاههای چندگانه و تاشو ازجمله بخشهای با بالاترین میزان تعامل در Android هستند.
بیایید به دو مطالعه موردی واقعی نگاه کنیم که نشان میدهد وقتی ساخت تطبیقی را ارسال میکنید چه اتفاقی میافتد.
اولین برنامه Gratitude است که یک برنامه محبوب خودمراقبتی و روزنگاری است.
Gratitude با اتخاذ طراحیهای تطبیقی صفحهنمایش، رایانههای لوحی و دستگاههای تاشو را بهعنوان کانال جذب با رشد بالا تثبیت کرد.
ابتدا، سرعت جذب کاربر را در صفحهنمایشهای بزرگ دوبرابر کردند.
نصبهای روزانه که بهطور خاص از رایانههای لوحی و دستگاههای تاشو منشأ میگرفتند ۱۰۰٪ رشد داشت.
آنها سهم صفحهنمایش بزرگ را از کل پایگاههای کاربر خود از ۷٪ به ۱۰٪ از کل نصبها افزایش دادند.
دومی BoxBoxClub است که یک برنامه همراه برای فرمول ۱ و ورزشهای موتوری است و پساز بهروزرسانی ساخت خود برای دستگاههای تاشو و صفحهنمایشهای بزرگتر، دورسنجی آنها دستاوردهای بزرگی را نشان داد: افزایش ۷٫۶ درصدی در کل کاربران فعال و افزایش ۵۴ درصدی در زمان تعامل.
بسیارخوب.
به پایان صحبت رسیدهایم و میخواهم سه نکته مهم را به شما بگویم.
ساختن «برنامه تطبیقی» به برنامه شما امکان میدهد به دستگاههای بیشتر و کاربران بیشتری خدمات ارائه دهد و درعینحال برنامه شما را برای این بومسازگان درحال گسترش برای آینده آماده میکند.
توسعه نرمافزار Android در درجه اول Compose است.
همه ابزارها، کتابخانهها، و مهارتهای جدید برای Compose ساخته شدهاند.
Compose برای چیدمانهای تطبیقی و توسعه عاملگرا عالی است.
پایگاههای کد کوچکتر هستند و درک آنها آسانتر است، و عاملها هنگام کار با کد Compose از کدهای کمتری استفاده میکنند.
پس، ادامه دهید، همه ابزارها، مهارتها، و میاناهای برنامهسازی کاربردی جدید را امتحان کنید و لذت ببرید.
متشکریم.
<LinearLayout android:orientation="vertical">
<EditText
android:id="@+id/text_msg"
android:inputType="textCapCharacters"
android:maxLength="12" />
</LinearLayout><ConstraintLayout>
<TextInputEditText
android:id="@+id/text_input"
android:inputType="text" />
</ConstraintLayout>mediaQuery { ... }
derivedMediaQuery { ... }sealed interface UiMediaScope {
@get:FrequentlyChangingValue val windowWidth: Dp
@get:FrequentlyChangingValue val windowHeight: Dp
val windowPosture: Posture // Flat, Tabletop, Book
val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
val keyboardKind: KeyboardKind // Physical, Virtual, None
val hasCamera: Boolean
val hasMicrophone: Boolean
val viewingDistance: ViewingDistance // Near, Medium, Far
}@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
val context = LocalContext.current
val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
.collectAsStateWithLifecycle(null)
val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
displayFeature is FoldingFeature &&
displayFeature.state == FoldingFeature.State.HALF_OPENED &&
displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
} ?: false
return if (isTabletopPosture) {
VideoPlayerLayout.Tabletop
} else {
VideoPlayerLayout.Default
}
}@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}@Composable
fun AdaptiveChatControls(
onStartVideoCall: () -> Unit,
onSendMessage: () -> Unit,
) {
// 1. Return a Dp value directly based on pointer precision
val chipPadding = mediaQuery {
if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
}
// 2. Return a Boolean combining hardware capabilities
val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
val hasPhysicalKeyboard = mediaQuery {
keyboardKind == KeyboardKind.Physical
}
Row(modifier = Modifier.padding(chipPadding)) {
if (canVideoCall) {
IconButton(onClick = onStartVideoCall) {
Icon(Icons.Default.Videocam, contentDescription = "Video call")
}
}
Button(onClick = onSendMessage) {
Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
}
}
}@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
val simulatedMediaScope = object : UiMediaScope {
override val windowWidth = 840.dp
override val windowHeight = 700.dp
override val windowPosture = UiMediaScope.Posture.Tabletop
override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
override val keyboardKind = UiMediaScope.KeyboardKind.Physical
override val hasCamera = true
override val hasMicrophone = true
override val viewingDistance = UiMediaScope.ViewingDistance.Medium
}
CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
VideoPlayerScreen()
}
}Grid(
modifier = Modifier.fillMaxSize().padding(16.dp),
config = {
val maxWidthDp = constraints.maxWidth.toDp()
val columnCount = if (maxWidthDp < 600.dp) 2 else 4
repeat(columnCount) {
column(1.fr)
}
row(GridTrackSize.Auto)
row(1.fr)
gap(row = 16.dp, column = 8.dp)
flow = GridFlow.Row
}
) {
// GridScope children...
}Grid(
config = {
repeat(3) { column(1.fr) }
repeat(3) { row(100.dp) }
gap(8.dp)
}
) {
// Spans 2x2 area (rows 1..2, columns 1..2)
HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
// Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
LiveBadge(
modifier = Modifier.gridItem(
row = 1,
column = 1,
alignment = Alignment.TopStart
)
)
// Negative index: placed in last row (-1), spanning all 3 columns
FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
when(currentVideoPlayerLayout()) {
VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
}
}enum class VideoPlayerArea { Player, Controller }
Grid(
config = { ... },
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}object VideoPlayerGridConfiguration {
val Default: GridConfigurationScope.() -> Unit = {
row(1f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
}
val Tabletop: GridConfigurationScope.() -> Unit = {
row(0.5f)
row(0.5f)
column(1f)
area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
}
}@Composable
fun VideoPlayerLayout(
player: @Composable () -> Unit,
controller: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Grid(
config = { mediaQuery {
if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
else { VideoPlayerGridConfiguration.Default }
}
}
modifier = modifier
) {
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
player()
}
Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
controller()
}
}
}@Composable
fun AdaptiveActionHeader(tags: List<String>) {
FlexBox(
config = {
direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
wrap(FlexWrap.Wrap)
alignItems(FlexAlignItems.Center)
gap(row = 8.dp, column = 12.dp)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Home",
modifier = Modifier.flex {
basis(FlexBasis.Auto)
grow(1f)
shrink(1f)
}
)
tags.forEach { tag ->
FilterChipItem(
label = tag,
modifier = Modifier.flex { shrink(0f) }
)
}
}
}val UiMediaScope.windowSizeClass: WindowSizeClass
@Composable get() = derivedMediaQuery {
WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
widthDp = windowWidth.value,
heightDp = windowHeight.value
)
}
@Composable
fun AppRoot() {
val sizeClass = mediaQuery { windowSizeClass }
val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
)
// Drive app-level navigation and scene strategies...
}if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}android skills list
android skills add adaptive