构建列表详情布局

列表-详情是一种界面模式,由双窗格布局组成,其中一个窗格显示项目列表,另一个窗格显示从列表中选择的项目的详情。

该模式特别适用于提供大型集合中元素的深入信息的应用,例如,具有电子邮件列表和每封电子邮件详细内容的电子邮件客户端。 列表详情也可用于不太重要的路径,例如将应用偏好设置划分为类别列表,并在“详细信息”窗格中显示每个类别的偏好设置。

与列表页面并排显示的“详细信息”窗格。
图 1. 当屏幕尺寸足够大时,详情窗格会与列表窗格并排显示。
选择某项内容后,“详细信息”窗格会占据整个屏幕。
图 2. 当屏幕尺寸有限时,“详细信息”窗格(因为已选择某项内容)会占据整个空间。

使用 NavigableListDetailPaneScaffold 实现“列表-详情”模式

NavigableListDetailPaneScaffold 是一个可组合项,可简化在 Jetpack Compose 中实现列表详情布局的过程。它封装了 ListDetailPaneScaffold,并添加了内置导航和预测性返回动画。

列表详情 Scaffold 最多支持三个窗格:

  1. 列表窗格:显示一系列内容。
  2. “详细信息”窗格:显示所选项的详细信息。
  3. 额外窗格(可选):在需要时提供更多背景信息。

该脚手架会根据窗口大小进行自适应:

  • 在大型窗口中,列表和“详细信息”窗格会并排显示。
  • 在小窗口中,一次只能看到一个窗格,并随着用户导航而切换。

声明依赖项

NavigableListDetailPaneScaffold 是 Material 3 自适应导航库的一部分。

将以下三个相关依赖项添加到应用或模块的 build.gradle 文件中:

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • 自适应:低级构建块,例如 HingeInfo 和 Posture
  • adaptive-layout:自适应布局,例如 ListDetailPaneScaffold 和 SupportingPaneScaffold
  • adaptive-navigation:用于在窗格内和窗格之间导航的可组合项,以及默认支持导航的自适应布局,例如 NavigableListDetailPaneScaffold 和 NavigableSupportingPaneScaffold

确保您的项目包含 compose-material3-adaptive 1.1.0-beta1 或更高版本。

选择启用预测性返回手势

如需在 Android 15 或更低版本中启用预测性返回动画,您必须选择启用对预测性返回手势的支持。如需选择启用,请将 android:enableOnBackInvokedCallback="true" 添加到 AndroidManifest.xml 文件中的 <application> 标记或各个 <activity> 标记。如需了解详情,请参阅选择启用预测性返回手势。

当应用以 Android 16(API 级别 36)或更高版本为目标平台时,系统会默认启用预测性返回手势。

基本用法

按如下方式实现 NavigableListDetailPaneScaffold:

  1. 使用表示所选内容的类。使用 Parcelable 类支持保存和恢复所选列表项。使用 kotlin-parcelize 插件为您生成代码。
  2. 使用 rememberListDetailPaneScaffoldNavigator 创建 ThreePaneScaffoldNavigator。

此导航器用于在列表、详情和额外窗格之间移动。通过声明通用类型,导航器还会跟踪 Scaffold 的状态(即正在显示哪个 MyItem)。由于此类型是 Parcelable,因此导航器可以保存和恢复状态,以自动处理配置更改。

  1. 将导航器传递给 NavigableListDetailPaneScaffold 可组合项。

  2. 将列表窗格实现提供给 NavigableListDetailPaneScaffold。使用 AnimatedPane 在导航期间应用默认窗格动画。然后,使用 ThreePaneScaffoldNavigator 导航到“详细信息”窗格 ListDetailPaneScaffoldRole.Detail,并显示传递的项。

  3. 在 NavigableListDetailPaneScaffold 中添加详细信息窗格实现。

导航完成后,currentDestination 会包含应用已导航到的窗格,包括窗格中显示的内容。contentKey 属性与原始调用中指定的类型相同,因此您可以访问需要显示的任何数据。

  1. (可选)更改 NavigableListDetailPaneScaffold 中的 defaultBackBehavior。默认情况下,NavigableListDetailPaneScaffold 会将 PopUntilScaffoldValueChange 用于 defaultBackBehavior。

如果您的应用需要其他返回导航模式,您可以通过指定其他 BackNavigationBehavior 选项来替换此行为。

BackNavigationBehavior选项

以下部分以电子邮件应用为例,其中一个窗格中包含电子邮件列表,另一个窗格中包含详细视图。

此行为侧重于整体布局结构的更改。在多窗格设置中,更改详细信息窗格中的电子邮件内容不会改变底层布局结构。因此,返回按钮可能会退出应用或当前导航图,因为在当前上下文中没有可还原的布局更改。在单窗格布局中,按返回键会跳过详情视图中的内容更改,并返回到列表视图,因为这代表着明显的布局更改。

请参考以下示例:

  • 多窗格:您正在详细信息窗格中查看一封邮件(第 1 项)。点击另一封电子邮件(第 2 项)会更新“详细信息”窗格,但列表窗格和“详细信息”窗格仍会显示。按返回键可能会退出应用或当前导航流程。
  • 单窗格:您查看邮件 1,然后查看邮件 2,按返回键将直接返回到邮件列表窗格。

如果您希望用户在每次执行返回操作时都能感知到明显的布局转换,请使用此选项。

插图:显示不同布局窗格中的导航值变化。
PopUntilContentChange

此行为会优先显示内容。如果您先查看第 1 项,然后查看第 2 项,那么无论布局如何,按返回键都会返回到第 1 项。

请参考以下示例:

  • 多窗格:您在“详细信息”窗格中查看第 1 项,然后点击列表中的第 2 项。“详细信息”窗格会更新。按返回键会将“详细信息”窗格恢复为第 1 项。
  • 单窗格:内容会恢复到之前的状态。

当用户希望通过返回操作返回到之前查看的内容时,请使用此选项。

两个“详细信息”窗格之间的过渡
PopUntilCurrentDestinationChange

此行为会弹出返回堆栈,直到当前导航目的地发生更改。这同样适用于单窗格和多窗格布局。

请参考以下示例:

无论您使用的是单窗格布局还是多窗格布局,按返回键始终会将焦点从突出显示的导航元素移至上一个目的地。在我们的电子邮件应用中,这意味着所选窗格的视觉指示会发生变化。

当保持当前导航的清晰视觉指示对用户体验至关重要时,请使用此选项。

在详情窗格和列表窗格之间导航
PopLatest

此选项仅从返回堆栈中移除最近的目的地。使用此选项可进行返回导航,而不会跳过中间状态。

执行完这些步骤后,您的代码应如下所示:

NavigableListDetailPaneScaffold(
    navigator = navigator,
    listPane = {
        AnimatedPane {
            ListContent(
                words = sampleWords,
                selectionState = navigator.currentDestination?.contentKey?.let {
                    SelectionVisibilityState.ShowSelection(it)
                } ?: SelectionVisibilityState.NoSelection,
                onWordClick = { word ->
                    scope.launch {
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, word)
                    }
                },
                animatedVisibilityScope = this@AnimatedPane,
                sharedTransitionScope = this@SharedTransitionLayout
            )
        }
    },
    detailPane = {
        AnimatedPane {
            DetailContent(
                definedWord = navigator.currentDestination?.contentKey,
                animatedVisibilityScope = this@AnimatedPane,
                sharedTransitionScope = this@SharedTransitionLayout,
                onClosePane = {
                    scope.launch {
                        navigator.navigateBack(
                            backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
                        )

                    }
                }
            )
        }
    }