Navigation 3 引入了一个强大且灵活的系统,可通过场景管理应用的界面流程。借助场景,您可以创建高度自定义的布局、适应不同的屏幕尺寸,以及无缝管理复杂的多窗格体验。
了解场景
在 Navigation 3 中,Scene
是用于呈现一个或多个 NavEntry
实例的基本单元。您可以将 Scene
视为界面的独特视觉状态或部分,它可以包含并管理来自返回堆栈的内容的显示。
每个 Scene
实例都通过其 key
和 Scene
本身的类进行唯一标识。此唯一标识符至关重要,因为它会在 Scene
发生变化时驱动顶级动画。
Scene
接口具有以下属性:
key: Any
:此特定Scene
实例的唯一标识符。此键与Scene
的类结合使用可确保独特性,主要用于动画用途。entries: List<NavEntry<T>>
:这是Scene
负责显示的NavEntry
对象的列表。请务必注意,如果在转换期间(例如在共享元素转换期间)同一NavEntry
显示在多个Scenes
中,则其内容将仅由最近显示该NavEntry
的目标Scene
呈现。previousEntries: List<NavEntry<T>>
:此属性用于定义在当前Scene
中发生“返回”操作时会产生的NavEntry
。这对于计算适当的预测性返回状态至关重要,可让NavDisplay
预测并转换到正确的上一个状态,该状态可能是具有不同类和/或键的场景。content: @Composable () -> Unit
:这是可组合函数,您可以在其中定义Scene
如何呈现其entries
以及特定于该Scene
的任何周围界面元素。
了解场景策略
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>?
此方法会从返回堆栈中获取当前 List<NavEntry<T>>
和 onBack
回调。如果它可以根据提供的条目成功构成一个,则应返回 Scene<T>
;如果无法构成,则应返回 null
。
SceneStrategy
还提供了方便的 then
中缀函数,可让您将多个策略链接在一起。这会创建一个灵活的决策流水线,其中每个策略都可以尝试计算 Scene
,如果无法计算,则会委托给链中的下一个策略。
场景和场景策略如何协同发挥作用
NavDisplay
是中心可组合项,用于监控返回堆栈并使用 SceneStrategy
确定和呈现适当的 Scene
。
NavDisplay's sceneStrategy
参数需要一个 SceneStrategy
,该 SceneStrategy
负责计算要显示的 Scene
。如果所提供的策略(或策略链)未计算出 Scene
,NavDisplay
会默认自动回退为使用 SinglePaneSceneStrategy
。
下面详细介绍了此互动过程:
- 当您向返回堆栈添加或从中移除键(例如,使用
backStack.add()
或backStack.removeLastOrNull()
)时,NavDisplay
会观察这些更改。 NavDisplay
会将当前的NavEntrys
列表(从返回堆栈键派生)传递给已配置的SceneStrategy's calculateScene
方法。- 如果
SceneStrategy
成功返回Scene
,则NavDisplay
会呈现该Scene
的content
。NavDisplay
还会根据Scene
的属性管理动画和预测性返回。
示例:单窗格布局(默认行为)
您可以采用的最简单的自定义布局是单窗格显示,如果没有其他 SceneStrategy
优先级更高,则采用这种默认行为。
data class SinglePaneScene<T : Any>( override val key: T, 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.invoke(entry.key) } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
示例:基本双窗格布局(自定义场景和策略)
此示例演示了如何创建一个简单的双窗格布局,该布局会根据以下两个条件激活:
- 窗口宽度足够宽,可以支持两个窗格(即至少
WIDTH_DP_MEDIUM_LOWER_BOUND
)。 - 返回堆栈上的前两个条目明确声明它们支持使用特定元数据以双窗格布局显示。
以下代码段是 TwoPaneScene.kt
和 TwoPaneSceneStrategy.kt
的组合源代码:
// --- TwoPaneScene --- /** * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. */ class TwoPaneScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val firstEntry: NavEntry<T>, val secondEntry: NavEntry<T> ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { firstEntry.content.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } companion object { internal const val TWO_PANE_KEY = "TwoPane" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * in a two-pane layout. */ fun twoPane() = mapOf(TWO_PANE_KEY to true) } } // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ class TwoPaneSceneStrategy<T : Any> : SceneStrategy<T> { @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val lastTwoEntries = entries.takeLast(2) // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. return if (lastTwoEntries.size == 2 && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. val sceneKey = Pair(firstEntry.key, secondEntry.key) TwoPaneScene( key = sceneKey, // Where we go back to is a UX decision. In this case, we only remove the top // entry from the back stack, despite displaying two entries in this scene. // This is because in this app we only ever add one entry to the // back stack at a time. It would therefore be confusing to the user to add one // when navigating forward, but remove two when navigating back. previousEntries = entries.dropLast(1), firstEntry = firstEntry, secondEntry = secondEntry ) } else { null } } }
如需在 NavDisplay
中使用此 TwoPaneSceneStrategy
,请修改 entryProvider
调用,以包含您打算在双窗格布局中显示的条目的 TwoPaneScene.twoPane()
元数据。然后,将 TwoPaneSceneStrategy()
作为 sceneStrategy
提供,并依赖于单窗格场景的默认回退:
// Define your navigation keys @Serializable data object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ProductList) NavDisplay( backStack = backStack, entryProvider = entryProvider { entry<ProductList>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Column { Text("Product List") Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View Details for ABC (Two-Pane Eligible)") } } } entry<ProductDetail>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Text("Product Detail: ${key.id} (Two-Pane Eligible)") } // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. sceneStrategy = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } } ) }
在 Material 自适应场景中显示列表-详情内容
对于列表-详情用例,androidx.compose.material3.adaptive:adaptive-navigation3
工件提供了用于创建列表-详情 Scene
的 ListDetailSceneStrategy
。此 Scene
会自动处理复杂的多窗格布局(列表、详情和额外窗格),并根据窗口大小和设备状态进行调整。
如需创建材质列表-详情 Scene
,请按以下步骤操作:
- 添加依赖项:在项目的
build.gradle.kts
文件中添加androidx.compose.material3.adaptive:adaptive-navigation3
。 - 使用
ListDetailSceneStrategy
元数据定义条目:使用listPane(), detailPane()
和extraPane()
标记NavEntrys
,以便适当地显示窗格。借助listPane()
辅助程序,您还可以在未选择任何项时指定detailPlaceholder
。 - 使用
rememberListDetailSceneStrategy
():此可组合函数提供可供NavDisplay
使用的预配置ListDetailSceneStrategy
。
以下代码段是一个 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<Any>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, sceneStrategy = 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") } } ) } } } }