建構清單/詳細資料版面配置

清單詳細資料是一種 UI 模式,由雙窗格版面配置組成,其中一個窗格會顯示項目清單,另一個窗格則會顯示從清單中選取項目的詳細資料。

這個模式特別適合用來深入瞭解大型集合中元素的應用程式,例如電子郵件用戶端,其中包含電子郵件清單和每封郵件的詳細內容。清單詳細資料也可以用於較不重要的路徑,例如將應用程式偏好設定分成類別清單,並在詳細資料窗格中顯示每個類別的偏好設定。

詳細資料窗格會顯示在清單頁面旁。
圖 1. 當螢幕大小足夠時,詳細資料窗格會與清單窗格並排顯示。
選取項目後,詳細資料窗格會占滿整個畫面。
圖 2. 螢幕大小受限時,詳細資料窗格 (因為已選取項目) 會佔用整個空間。

使用 NavigableListDetailPaneScaffold 實作清單/詳細資料模式

NavigableListDetailPaneScaffold 可組合函式可簡化在 Jetpack Compose 中實作清單詳細資料版面配置的程序。這個函式會包裝 ListDetailPaneScaffold,並新增內建導覽和預測返回動畫。

清單詳細資料支架最多可支援三個窗格:

  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
  • 自動調整式版面配置:自動調整式版面配置,例如 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)。由於這個型別可封送,因此導覽器可以儲存及還原狀態,自動處理設定變更。

  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
                        )

                    }
                }
            )
        }
    }