Navigation 3 引入了一个强大而灵活的系统,用于通过场景 管理应用的界面流程。借助场景,您可以创建高度自定义的布局,适应不同的屏幕尺寸,并无缝管理复杂的多窗格体验。
了解场景
在 Navigation 3 中,Scene 是呈现一个或多个
NavEntry 实例的基本单元。您可以将 Scene 视为界面的一个不同的视觉状态或部分,它可以包含和管理返回堆栈中内容的显示。
每个 Scene 实例都由其 key 和
Scene 本身的类进行唯一标识。此唯一标识符至关重要,因为它会驱动
顶级动画,当 Scene 发生变化时。
Scene 接口具有以下属性:
key: Any:此特定Scene实例的唯一标识符。此键与Scene的类相结合,可确保独特性,主要用于动画目的。entries: List<NavEntry<T>>:这是NavEntry对象列表, 由Scene负责显示。重要的是,如果在转换期间(例如,在共享元素转换中)在多个Scenes中显示相同的NavEntry,则其内容将仅由显示它的最新目标Scene呈现。previousEntries: List<NavEntry<T>>:此属性定义了如果从当前Scene执行“返回”操作,将会产生的NavEntry。它对于计算正确的预测性返回状态至关重要,可让NavDisplay预测并转换到正确的前一个状态,该状态可能是具有不同类和/或键的场景。content: @Composable () -> Unit:这是一个可组合函数,您可以在其中定义Scene如何呈现其entries以及特定于该Scene的任何周围界面元素。metadata: Map<String, Any>:向其他 库组件(例如NavDisplay)提供特定于场景的信息。默认情况下,返回entries中最后一个NavEntry的metadata。
了解场景策略
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>
,则应返回该 Scene<T>
;如果不能,则应返回 null。
The SceneStrategyScope 负责维护
可能需要的任何可选参数,例如 onBack 回调。SceneStrategy
场景和场景策略如何协同工作
NavDisplay 是一个中心可组合项,用于观察返回堆栈并
使用一个或多个 SceneStrategy 来确定和呈现适当的
Scene。
NavDisplay 的 sceneStrategies 参数需要一个 SceneStrategy
实例列表,这些实例负责计算要显示的 Scene。如果提供的策略未计算出任何
Scene,则NavDisplay会自动
回退到默认使用SinglePaneSceneStrategy。
以下是交互的细分:
- 当您从返回堆栈中添加或移除键(例如,使用
backStack.add()或backStack.removeLastOrNull())时,NavDisplay会观察到这些更改。 NavDisplay会按顺序将当前的NavEntry列表(从后退 堆栈键派生)传递给配置的sceneStrategies,并对每个列表调用calculateScene,直到返回Scene。- 当
SceneStrategy成功返回Scene时,NavDisplay随后会呈现该Scene的content。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. * */ 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) } } }
如需在 NavDisplay 中使用此 ListDetailSceneStrategy,请修改 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 会自动处理复杂的多窗格排列(列表、详情和额外窗格),并根据窗口大小和设备状态进行调整。
如需创建 Material 列表-详情 Scene,请按以下步骤操作:
- 添加依赖项:在项目的
build.gradle.kts文件中添加androidx.compose.material3.adaptive:adaptive-navigation3。 - 使用
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") } } ) } } } }