«پیمایش ۳» سیستم قدرتمند و انعطافپذیری را برای مدیریت جریان واسط کاربر برنامه شما ازطریق صحنهها معرفی میکند. صحنهها به شما امکان میدهند چیدمانهای بسیار سفارشیسازیشده ایجاد کنید، با اندازههای مختلف صفحهنمایش سازگار شوید، و تجربههای چند پانلی پیچیده را بهطور یکپارچه مدیریت کنید.
درک صحنهها
در «پیمایش ۳»، Scene واحد بنیادی است که یک یا چند نمونه از
NavEntry را پرداز میکند. Scene را بهعنوان وضعیت دیداری متمایز یا بخشی از میانای کاربری خود درنظر بگیرید که میتواند نمایش محتوا از پشته برگشت شما را دربرگیرد و مدیریت کند.
هر نمونه Scene با key و کلاس خود Scene بهطور منحصربهفردی شناسایی میشود. این شناسه یکتا بسیار مهم است زیرا
پویانمایی سطح بالا را هنگام تغییر Scene هدایت میکند.
واسط Scene خصوصیات زیر را دارد:
key: Any: شناسه یکتای این نمونه خاص ازScene. این کلید، همراه با کلاسScene، منحصربهفرد بودن را تضمین میکند، بهویژه برای اهداف پویانمایی.entries: List<NavEntry<T>>: این فهرستی ازNavEntryشیء است کهSceneمسئول نمایش آنها است. نکته مهم اینکه اگر همانNavEntryدر چندScenesدرطول انتقال نمایش داده شود (برای نمونه، در انتقال عنصر همرسانیشده)، محتوای آن فقط توسط جدیدترینSceneهدف که آن را نمایش میدهد پرداز میشود.previousEntries: List<NavEntry<T>>: این داراییNavEntryهایی را که درصورت انجام کنش «بازگشت» ازSceneکنونی حاصل میشود تعریف میکند. این کار برای محاسبه وضعیت مناسب برگشت پیشبینیکننده ضروری است، و بهNavDisplayامکان میدهد وضعیت قبلی صحیح را پیشبینی کند و به آن انتقال یابد، وضعیت قبلی ممکن است «صحنه» با کلاس، کلید، یا هر دو متفاوت باشد.content: @Composable () -> Unit: این تابع ترکیبی است که در آن تعریف میکنیدSceneچگونهentriesو هر عنصر رابط کاربری اطراف آن را که مختص آنSceneاست ارائه کند.-
metadata: Map<String, Any>: اطلاعات خاص صحنه را به دیگر عناصر کتابخانه مثلNavDisplayارائه میدهد. بهطور پیشفرض،metadataآخرینNavEntryدرentriesرا برمیگرداند.
پیادهسازی equals و hashCode در «صحنههای» سفارشی
پیادهسازیهای سفارشی Scene باید equals و hashCode را بهدرستی پیادهسازی کنند
تا از موارد زیر پشتیبانی کنند:
- گذار حالت صحنه: هر بار که صحنهای براساس فهرست استراتژیهای صحنه محاسبه میشود،
NavDisplayبرابری صحنه جدید و صحنه قبلی را بررسی میکند. اگر تغییری را تشخیص دهد،SeekableTransitionاستفادهشده برای انتقال بین صحنهها را بهروز میکند، که به نوبه خود بر وضعیت چرخه حیات صحنهها تأثیر میگذارد. - مدیریت رونهاد: برای
OverlaySceneها (مثل چارگوشهای گفتگو)،NavDisplayاز خود شیء صحنه بهعنوانkeyبرای پیگیری چرخههای عمر رونهاد و پویانماییهای خروج استفاده میکند.
تأیید کنید که همه داراییهایی که محتوای صحنه را تعریف میکنند، مثل key،
entries، و previousEntries، در پیادهسازیهای equals و hashCode
گنجانده شدهاند. بهطورکلی، از افزودن تماسهای برگشتی (مانند onBack) در پیادهسازی این روشها خودداری کنید زیرا این تماسها میتوانند نمونهها را بدون تغییر دادن وضعیت صحنه منطقی تغییر دهند.
آشنایی با استراتژیهای صحنه
SceneStrategy سازوکاری است که تعیین میکند فهرست معینی از
NavEntryهای پشته برگشت چگونه باید مرتب و به
Scene تبدیل شود. اساساً، وقتی با ورودیهای پشته برگشت فعلی مواجه میشود،
SceneStrategy دو سؤال کلیدی از خود میپرسد:
- آیا میتوانم از این ورودیها
Sceneبسازم؟ اگرSceneStrategyتشخیص دهد که میتواندNavEntryهای دادهشده را مدیریت کند وSceneمعناداری تشکیل دهد (برای نمونه، چیدمان چندپانلی یا گفتگویی)، ادامه میدهد. درغیراینصورت،nullرا برمیگرداند و به راهبردهای دیگر فرصت میدهدSceneایجاد کنند. - اگر اینطور است، چگونه باید آن ورودیها را در
Scene?مرتب کنم؟ وقتیSceneStrategyمتعهد میشود که ورودیها را مدیریت کند، مسئولیت ساختنSceneو تعریف نحوه نمایشNavEntryمشخصشده در آنSceneرا برعهده میگیرد.
هسته یک SceneStrategy روش calculateScene آن است:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
این روش یک تابع افزونه در SceneStrategyScope است که List<NavEntry<T>> فعلی را از پشته برگشت میگیرد. اگر بتواند از ورودیهای ارائهشده یکی را باموفقیت تشکیل دهد، باید Scene<T>
برگرداند، درغیراینصورت باید null برگرداند.
SceneStrategyScope مسئول حفظ هرگونه آرگومان اختیاری است
که SceneStrategy ممکن است به آن نیاز داشته باشد، مثل onBack تماس برگشتی.
نحوه عملکرد مشترک «صحنهها» و «استراتژیهای صحنه»
NavDisplay عنصر ترکیبی مرکزی است که پشته برگشت شما را مشاهده میکند و
از یک یا چند SceneStrategy برای تعیین و پرداز کردن Scene مناسب استفاده میکند.
پارامتر sceneStrategies در NavDisplay انتظار دارد فهرستی از نمونههای SceneStrategy
را که مسئول محاسبه Scene برای نمایش هستند دریافت کند. اگر براساس استراتژیهای ارائهشده Scene محاسبه نشود، NavDisplay بهطور خودکار به استفاده از SinglePaneSceneStrategy بهصورت پیشفرض برمیگردد.
در اینجا تفکیک تعامل آمده است:
- وقتی کلیدهایی را به پشته برگشت خود اضافه یا از آن حذف میکنید (برای نمونه، بااستفاده از
backStack.add()یاbackStack.removeLastOrNull())،NavDisplayاین تغییرات را مشاهده میکند. -
NavDisplayفهرست فعلیNavEntryها (مشتقشده از کلیدهای پشته برگشت) را بهترتیب بهsceneStrategiesپیکربندیشده ارسال میکند وcalculateSceneرا برای هرکدام فرا میخواند تا زمانی کهSceneبرگردانده شود. - وقتی
SceneStrategyباموفقیتSceneرا برمیگرداند،NavDisplayسپسcontentآنSceneرا ارائه میکند.NavDisplayهمچنین براساس ویژگیهایScene، پویانماییها و برگشت پیشبینانه را مدیریت میکند.
مثال: چیدمان تکقاب (رفتار پیشفرض)
سادهترین چیدمان سفارشی که میتوانید داشته باشید نمایشگر تکصفحهای است که اگر هیچ SceneStrategy دیگری اولویت نداشته باشد، رفتار پیشفرض است.
data class SinglePaneScene<T : Any>( override val key: Any, val entry: NavEntry<T>, override val previousEntries: List<NavEntry<T>>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(entry) override val content: @Composable () -> Unit = { entry.Content() } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
مثال: چیدمان تفصیلی-فهرستی پایه (صحنه و استراتژی سفارشی)
این مثال نشان میدهد که چگونه میتوان چیدمان فهرست-جزئیات ایجاد کرد که براساس دو شرط فعال میشود:
- عرض پنجره بهاندازه کافی پهن باشد تا از دو قاب پشتیبانی کند (یعنی حداقل
WIDTH_DP_MEDIUM_LOWER_BOUND). - پشته برگشت حاوی ورودیهایی است که پشتیبانی خود را برای نمایش در چیدمان فهرست-جزئیات بااستفاده از فرادادههای خاص اعلام کردهاند.
گزیده زیر کد منبع ListDetailScene.kt است و
هم ListDetailScene و هم ListDetailSceneStrategy را دربرمیگیرد:
// --- ListDetailScene --- /** * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. * */ data class ListDetailScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val listEntry: NavEntry<T>, val detailEntry: NavEntry<T>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.4f)) { listEntry.Content() } Column(modifier = Modifier.weight(0.6f)) { detailEntry.Content() } } } } @Composable fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) } } // --- ListDetailSceneStrategy --- /** * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item * is the backstack is a detail, and before it, at any point in the backstack is a list. */ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val detailEntry = entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null // We use the list's contentKey to uniquely identify the scene. // This allows the detail panes to be displayed instantly through recomposition, rather than // having NavDisplay animate the whole scene out when the selected detail item changes. val sceneKey = listEntry.contentKey return ListDetailScene( key = sceneKey, previousEntries = entries.dropLast(1), listEntry = listEntry, detailEntry = detailEntry ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, true) } } }
برای استفاده از این ListDetailSceneStrategy در NavDisplay، تماسهای entryProvider را تغییر دهید تا شامل فراداده ListDetailScene.listPane() برای ورودیای که میخواهید بهعنوان چیدمان فهرست نشان دهید و ListDetailScene.detailPane() برای ورودیای که میخواهید بهعنوان چیدمان جزئیات نشان دهید شود. سپس، ListDetailSceneStrategy() را بهعنوان sceneStrategy ارائه دهید،
با تکیه بر جایگزین پیشفرض برای سناریوهای تکصفحهای:
// Define your navigation keys @Serializable data object ConversationList : NavKey @Serializable data class ConversationDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ConversationList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ConversationList>( metadata = ListDetailSceneStrategy.listPane() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "I'm a Conversation List") Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) { Text(text = "Open detail") } } } entry<ConversationDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { Text(text = "I'm a Conversation Detail") } } ) } private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) { // Remove any existing detail routes, then add the new detail route removeIf { it is ConversationDetail } add(detailRoute) }
اگر نمیخواهید صحنه فهرست-جزئیات خودتان را بسازید، میتوانید از صحنه فهرست-جزئیات Material استفاده کنید که با جزئیات منطقی و پشتیبانی از جایبانها ارائه میشود، همانطور که در بخش بعدی نشان داده شده است.
نمایش محتوای فهرست-جزئیات در «صحنه تطبیقی Material»
برای مورد استفاده فهرست-جزئیات،
androidx.compose.material3.adaptive:adaptive-navigation3 عنصر
ListDetailSceneStrategy را ارائه میدهد که فهرست-جزئیات Scene را ایجاد میکند. این Scene
بهطور خودکار چیدمانهای پیچیده چند پانلی (فهرست، جزئیات، و پانلهای اضافی) را مدیریت میکند و آنها را براساس اندازه پنجره و وضعیت دستگاه تطبیق میدهد.
برای ایجاد کردن Scene جزئیات فهرست «ماتریال»، این مراحل را دنبال کنید:
- افزودن وابستگی:
androidx.compose.material3.adaptive:adaptive-navigation3را در فایلbuild.gradle.ktsپروژه خود بگنجانید. - ورودیهایتان را با فراداده
ListDetailSceneStrategyتعریف کنید: ازlistPane(), detailPane()وextraPane()برای علامتگذاریNavEntrysبرای نمایش مناسب قاب استفاده کنید. یاریرسانlistPane()همچنین به شما امکان میدهدdetailPlaceholderرا درصورتیکه هیچ موردی انتخاب نشده باشد مشخص کنید. - استفاده از
rememberListDetailSceneStrategy(): این تابع ترکیبیListDetailSceneStrategyپیشپیکربندیشدهای را ارائه میدهد که میتواند توسطNavDisplayاستفاده شود.
تکهکد زیر نمونهای از Activity است که نحوه استفاده از
ListDetailSceneStrategy را نشان میدهد:
@Serializable object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Serializable data object Profile : NavKey class MaterialListDetailActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3AdaptiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(ProductList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ProductList>( metadata = ListDetailSceneStrategy.listPane( detailPlaceholder = { ContentYellow("Choose a product from the list") } ) ) { ContentRed("Welcome to Nav3") { Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View product") } } } entry<ProductDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { product -> ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = { backStack.add(Profile) }) { Text("View profile") } } } } entry<Profile>( metadata = ListDetailSceneStrategy.extraPane() ) { ContentGreen("Profile") } } ) } } } }