對話場景裝飾器食譜
這項食譜說明在對話方塊中顯示場景的方法。舉例來說,當應用程式處於展開視窗時,這項功能可用於將清單詳細資料設定 UI 顯示為疊加層,否則會顯示為全螢幕頁面。
運作方式
如要在對話方塊中顯示場景,請執行下列操作:
- 建立對話方塊
SceneDecorationStrategy:這個套件會定義DialogSceneDecoratorStrategy,以及用來在對話方塊中顯示輸入內容Scene的DialogDecoratorScene。定義對話方塊場景裝飾策略時,請考量下列事項:- 何時可以在對話方塊中顯示場景?在這個食譜中,只有在視窗寬度至少展開時,場景才會顯示在對話方塊中。
- 哪些中繼資料應決定場景是否可顯示在對話方塊中?在本食譜中,如果場景中的第一個項目含有
DialogSceneMetadataKey中繼資料,系統就會在對話方塊中顯示該場景。視用途而定,您可能更適合採用其他方法,例如規定場景中的所有項目都必須具有相同的中繼資料。 - 對話方塊的關閉行為為何?在本食譜中,點選對話方塊外部會移除場景中的所有項目,而返回手勢或按鈕一次只會移除一個項目。食譜會透過
DialogDecoratorSceneConfiguration類別提供這項行為的設定。
- 使用對話方塊
SceneDecorationStrategy:如要使用場景裝飾器策略,請將其做為sceneDecoratorStrategies參數的一部分傳遞至NavDisplay。- 注意:由於
NavDisplay不會裝飾OverlayScene執行個體,您可能需要注意對話方塊場景裝飾策略在sceneDecoratorStrategies清單中的位置。如果使用這個屬性裝飾場景,系統就不會再依據場景裝飾策略裝飾場景。 - 注意:使用
contentKey導入onDismissAll來識別要關閉的項目時,請注意contentKey預設會使用key.toString()。本食譜使用backStack.removeAll { it.toString() in contentKeys },適用於簡單的物件鍵,但如果鍵較複雜或實作自訂contentKey,可能就需要調整。
- 注意:由於
package com.example.nav3recipes.dialogscenedecorator import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.SharedTransitionLayout import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen import com.example.nav3recipes.content.ContentYellow import com.example.nav3recipes.scenes.listdetail.ListDetailScene import com.example.nav3recipes.scenes.listdetail.rememberListDetailSceneStrategy import com.example.nav3recipes.ui.setEdgeToEdgeConfig import kotlinx.serialization.Serializable @Serializable private data object Main : NavKey @Serializable private data object SettingsList : NavKey @Serializable private data object SettingsDetail : NavKey class DialogSceneDecoratorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setEdgeToEdgeConfig() setContent { val backStack = rememberNavBackStack(Main) val listDetailSceneStrategy = rememberListDetailSceneStrategy<NavKey>() val dialogSceneDecoratorStrategy = rememberDialogSceneDecoratorStrategy<NavKey>( onDismissAll = { entriesToDismiss -> // Caution: This relies on the default behavior of NavEntry using key.toString() // to define its contentKey property. entriesToDismiss.forEach { entry -> backStack .indexOfLast { it.toString() == entry.contentKey } .takeIf { it >= 0 } ?.let { backStack.removeAt(it) } } } ) SharedTransitionLayout { NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailSceneStrategy), sceneDecoratorStrategies = listOf(dialogSceneDecoratorStrategy), sharedTransitionScope = this, entryProvider = entryProvider { entry<Main> { ContentGreen("Welcome to Nav3") { Button(onClick = dropUnlessResumed { backStack.add(SettingsList) }) { Text("Click to open settings") } } } entry<SettingsList>( metadata = DialogSceneDecoratorStrategy.sceneDialog( DialogDecoratorSceneConfiguration( backDismissalBehavior = DismissalBehavior.Single ) ) + ListDetailScene.listPane() ) { ContentBlue( title = "Settings List", ) { Button(onClick = dropUnlessResumed { if (backStack.last() !is SettingsDetail) { backStack.add(SettingsDetail) } }) { Text("Open detail") } } } entry<SettingsDetail>( metadata = ListDetailScene.detailPane() ) { ContentYellow("Settings Detail") } } ) } } } }
package com.example.nav3recipes.dialogscenedecorator import androidx.activity.compose.BackHandler import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2 import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.SecureFlagPolicy import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavMetadataKey import androidx.navigation3.runtime.get import androidx.navigation3.runtime.metadata import androidx.navigation3.scene.OverlayScene import androidx.navigation3.scene.Scene import androidx.navigation3.scene.SceneDecoratorStrategy import androidx.navigation3.scene.SceneDecoratorStrategyScope import androidx.window.core.layout.WindowSizeClass fun DialogProperties.copy( dismissOnBackPress: Boolean = this.dismissOnBackPress, dismissOnClickOutside: Boolean = this.dismissOnClickOutside, securePolicy: SecureFlagPolicy = this.securePolicy, usePlatformDefaultWidth: Boolean = this.usePlatformDefaultWidth, decorFitsSystemWindows: Boolean = this.decorFitsSystemWindows, windowTitle: String = this.windowTitle ): DialogProperties = DialogProperties( dismissOnBackPress = dismissOnBackPress, dismissOnClickOutside = dismissOnClickOutside, securePolicy = securePolicy, usePlatformDefaultWidth = usePlatformDefaultWidth, decorFitsSystemWindows = decorFitsSystemWindows, windowTitle = windowTitle ) enum class DismissalBehavior { All, Single } /** * Configuration for the [DialogDecoratorScene]. * * @property dialogProperties The [DialogProperties] used to configure the dialog. * @property backDismissalBehavior Whether all entries in the scene should be dismissed when the * back button is pressed. This configuration only applies if the * [DialogProperties.dismissOnBackPress] is set to `true`. * */ class DialogDecoratorSceneConfiguration( val dialogProperties: DialogProperties = DialogProperties(), val backDismissalBehavior: DismissalBehavior = DismissalBehavior.Single, val shape: Shape = RoundedCornerShape(16.dp), ) { fun toDialogProperties(): DialogProperties { // If the desired behavior is to not dismiss all, the DialogProperties passed to the Dialog // needs to be configured to not handle the back press itself. if (dialogProperties.dismissOnBackPress && backDismissalBehavior != DismissalBehavior.All) { return dialogProperties.copy(dismissOnBackPress = false) } return dialogProperties } fun shouldDismissSingleOnBackPress() = dialogProperties.dismissOnBackPress && backDismissalBehavior == DismissalBehavior.Single override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false other as DialogDecoratorSceneConfiguration return dialogProperties == other.dialogProperties && backDismissalBehavior == other.backDismissalBehavior && shape == other.shape } override fun hashCode(): Int { return dialogProperties.hashCode() * 31 + backDismissalBehavior.hashCode() * 31 + shape.hashCode() * 31 } } /** * [DialogDecoratorScene] is an [OverlayScene] used by [DialogSceneDecoratorStrategy] to present * another [Scene] within a [Dialog]. * * @property scene The [Scene] to be displayed within the dialog. * @property overlaidEntries The [NavEntry]s that are overlaid by the dialog. * @property dialogDecoratorSceneConfiguration The [DialogDecoratorSceneConfiguration] used to * configure the dialog scene. * @property onBack The callback to be invoked when a back event should be handled * @property onDismissAll The callback to be invoked when the entire dialog stack should be dismissed **/ data class DialogDecoratorScene<T : Any>( private val scene: Scene<T>, override val overlaidEntries: List<NavEntry<T>>, private val dialogDecoratorSceneConfiguration: DialogDecoratorSceneConfiguration, private val onBack: () -> Unit, private val onDismissAll: () -> Unit ) : OverlayScene<T>, Scene<T> by scene { override val content: @Composable () -> Unit = { Dialog( onDismissRequest = onDismissAll, properties = dialogDecoratorSceneConfiguration.toDialogProperties() ) { Surface( shape = dialogDecoratorSceneConfiguration.shape ) { scene.content() } // Because back events are dispatched to the currently focused window, this back handler // must be contained within the dialog's content to receive the events. BackHandler(dialogDecoratorSceneConfiguration.shouldDismissSingleOnBackPress()) { onBack() } } } } @Composable fun <T : Any> rememberDialogSceneDecoratorStrategy( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfoV2().windowSizeClass, onDismissAll: ((List<NavEntry<T>>) -> Unit) ): DialogSceneDecoratorStrategy<T> = remember(windowSizeClass, onDismissAll) { DialogSceneDecoratorStrategy(windowSizeClass, onDismissAll = onDismissAll) } /** * A [SceneDecoratorStrategy] that wraps a [Scene] in a [DialogDecoratorScene] if the first * [NavEntry] within it has been marked with the [DialogSceneMetadataKey] and the window width * is at least [WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND]. * * If you only need to display a single [NavEntry] in a [Dialog], using * [androidx.navigation3.scene.DialogSceneStrategy] instead may be preferable. * * @property windowSizeClass The current [WindowSizeClass] used to determine if dialogs should be used. * @property windowWidthDpBreakpoint the width in dp at or above which a dialog should be displayed. * @property onDismissAll callback invoked to dismiss all dialog entries, receives the entries that * are currently in the dialog. */ class DialogSceneDecoratorStrategy<T : Any>( private val windowSizeClass: WindowSizeClass, private val windowWidthDpBreakpoint: Int = WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND, private val onDismissAll: (List<NavEntry<T>>) -> Unit ) : SceneDecoratorStrategy<T> { override fun SceneDecoratorStrategyScope<T>.decorateScene(scene: Scene<T>): Scene<T> { if (!windowSizeClass.isWidthAtLeastBreakpoint(windowWidthDpBreakpoint)) return scene val dialogDecoratorSceneConfiguration = scene.entries.firstOrNull()?.metadata[DialogSceneMetadataKey] ?: return scene // This is critical to ensure that the scenes rendered beneath the dialog don't contain // any entries that are in the dialog. val overlaidEntries = scene.previousEntries.dropLastWhile { it in scene.entries } return DialogDecoratorScene( scene, overlaidEntries, dialogDecoratorSceneConfiguration, onBack, onDismissAll = { onDismissAll.invoke(scene.entries) }) } companion object { object DialogSceneMetadataKey : NavMetadataKey<DialogDecoratorSceneConfiguration> fun sceneDialog(dialogDecoratorSceneConfiguration: DialogDecoratorSceneConfiguration = DialogDecoratorSceneConfiguration()): Map<String, Any> = metadata { put(DialogSceneMetadataKey, dialogDecoratorSceneConfiguration) } } }