
commonMain.androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator.kt Maven / Gradle / Ivy
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.adaptive.navigation
import androidx.annotation.FloatRange
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.MutableThreePaneScaffoldState
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.calculateThreePaneScaffoldValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastMap
import kotlin.collections.removeLast as removeLastKt
/**
* The common interface of the default navigation implementations for different three-pane
* scaffolds.
*
* In general, we suggest you to use [rememberListDetailPaneScaffoldNavigator] or
* [rememberSupportingPaneScaffoldNavigator] to get remembered default instances of this interface
* for [ListDetailPaneScaffold] and [SupportingPaneScaffold], respectively. Those default
* implementations work independently from any navigation frameworks.
*
* If you need to integrate with existing navigation frameworks or implement your own custom
* navigation logic, usually creating whole new APIs that's tailored for your own solution will be
* recommended, instead of implementing this interface. But we recommend you refer to the API design
* and the default implementation to get better understanding and address the intricacies of
* navigation in an adaptive scenario.
*
* @param T the type representing the content key/id for a navigation destination. This type must be
* storable in a Bundle. Used to customize navigation behavior (for example,
* [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
*/
@ExperimentalMaterial3AdaptiveApi
@Stable
interface ThreePaneScaffoldNavigator {
/**
* The current layout directives that the associated three pane scaffold needs to follow. It's
* supposed to be automatically updated when the window configuration changes.
*/
val scaffoldDirective: PaneScaffoldDirective
/**
* The current state of the associated three pane scaffold, used to query the transition between
* layout states.
*/
val scaffoldState: ThreePaneScaffoldState
/**
* The current layout value of the associated three pane scaffold, which represents unique
* layout states of the scaffold.
*/
val scaffoldValue: ThreePaneScaffoldValue
/**
* Returns the scaffold value associated with the previous destination, assuming there is a
* previous destination to navigate back to. If not, this is the same as [scaffoldValue].
*
* @param backNavigationBehavior the behavior describing which backstack entries may be skipped
* during the back navigation. See [BackNavigationBehavior].
*/
fun peekPreviousScaffoldValue(
backNavigationBehavior: BackNavigationBehavior =
BackNavigationBehavior.PopUntilScaffoldValueChange
): ThreePaneScaffoldValue
/**
* The current destination as tracked by the navigator.
*
* Implementors of this interface should ensure this value is updated whenever a navigation
* operation is performed.
*/
val currentDestination: ThreePaneScaffoldDestinationItem?
/**
* Indicates if the navigator should be aware of pane destination history when deciding the
* result [ThreePaneScaffoldValue] by a navigation operation. If the value is `false`, only the
* current destination will be considered in the scaffold value calculation.
*
* @see calculateThreePaneScaffoldValue for more detailed explanation about history awareness.
*/
var isDestinationHistoryAware: Boolean
/**
* Navigates to a new destination. The new destination is supposed to have the highest priority
* when calculating the new [scaffoldValue].
*
* Implementors of this interface should ensure the new destination pane will be expanded or
* adapted in a reasonable way so it provides users the sense that the new destination is the
* pane currently being used.
*
* @param pane the new destination pane.
* @param contentKey the optional key or id representing the content of the new destination.
*/
suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T? = null)
/**
* Returns `true` if there is a previous destination to navigate back to.
*
* Implementors of this interface should ensure the logic of this function is consistent with
* [navigateBack].
*
* @param backNavigationBehavior the behavior describing which backstack entries may be skipped
* during the back navigation. See [BackNavigationBehavior].
*/
fun canNavigateBack(
backNavigationBehavior: BackNavigationBehavior =
BackNavigationBehavior.PopUntilScaffoldValueChange
): Boolean
/**
* Navigates to the previous destination. Returns `true` if there is a previous destination to
* navigate back to.
*
* Implementors of this interface should ensure the logic of this function is consistent with
* [canNavigateBack].
*
* @param backNavigationBehavior the behavior describing which backstack entries may be skipped
* during the back navigation. See [BackNavigationBehavior].
*/
suspend fun navigateBack(
backNavigationBehavior: BackNavigationBehavior =
BackNavigationBehavior.PopUntilScaffoldValueChange
): Boolean
/**
* Seeks the [scaffoldState] transition to the previous destination, as in a predictive back
* animation.
*
* This does not affect the current [scaffoldValue] or backstack. To do so, call [navigateBack]
* when the back navigation action is finalized.
*
* @param backNavigationBehavior the behavior describing which backstack entries may be skipped
* during the back navigation. See [BackNavigationBehavior].
* @param fraction the progress fraction of the transition of backwards navigation.
*/
suspend fun seekBack(
backNavigationBehavior: BackNavigationBehavior =
BackNavigationBehavior.PopUntilScaffoldValueChange,
@FloatRange(from = 0.0, to = 1.0) fraction: Float = 1.0f,
)
}
/**
* Returns a remembered default implementation of [ThreePaneScaffoldNavigator] for
* [ListDetailPaneScaffold], which will be updated automatically when the input values change. The
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [ListDetailPaneScaffold].
*
* @param T the type representing the content key/id for a navigation destination. This type must be
* storable in a Bundle. Used to customize navigation behavior (for example,
* [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
* current context.
* @param adaptStrategies adaptation strategies of each pane.
* @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
* full destination history, instead of just the current destination. See
* [calculateThreePaneScaffoldValue] for more relevant details.
* @param initialDestinationHistory the initial pane destination history of the scaffold, by default
* it will be just the list pane.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberListDetailPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
ListDetailPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
initialDestinationHistory: List> =
DefaultListDetailPaneHistory,
): ThreePaneScaffoldNavigator =
rememberThreePaneScaffoldNavigator(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
initialDestinationHistory
)
/**
* Returns a remembered default implementation of [ThreePaneScaffoldNavigator] for
* [ListDetailPaneScaffold], which will be updated automatically when the input values change. The
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [ListDetailPaneScaffold].
*
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
* current context.
* @param adaptStrategies adaptation strategies of each pane.
* @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
* full destination history, instead of just the current destination. See
* [calculateThreePaneScaffoldValue] for more relevant details.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberListDetailPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
ListDetailPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
): ThreePaneScaffoldNavigator =
rememberListDetailPaneScaffoldNavigator(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
)
/**
* Returns a remembered default implementation of [ThreePaneScaffoldNavigator] for
* [SupportingPaneScaffold], which will be updated automatically when the input values change. The
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [SupportingPaneScaffold].
*
* @param T the type representing the content key/id for a navigation destination. This type must be
* storable in a Bundle. Used to customize navigation behavior (for example,
* [BackNavigationBehavior]). If this customization is unneeded, you can pass [Any].
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
* current context.
* @param adaptStrategies adaptation strategies of each pane.
* @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
* full destination history, instead of just the current destination. See
* [calculateThreePaneScaffoldValue] for more relevant details.
* @param initialDestinationHistory the initial destination history of the scaffold, by default it
* will be just the main pane.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberSupportingPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
SupportingPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
initialDestinationHistory: List> =
DefaultSupportingPaneHistory,
): ThreePaneScaffoldNavigator =
rememberThreePaneScaffoldNavigator(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
initialDestinationHistory
)
/**
* Returns a remembered default implementation of [ThreePaneScaffoldNavigator] for
* [SupportingPaneScaffold], which will be updated automatically when the input values change. The
* default navigator is supposed to be used independently from any navigation frameworks and handles
* the navigation purely inside the [SupportingPaneScaffold].
*
* @param scaffoldDirective the current layout directives to follow. The default value will be
* calculated with [calculatePaneScaffoldDirective] using
* [WindowAdaptiveInfo][androidx.compose.material3.adaptive.WindowAdaptiveInfo] retrieved from the
* current context.
* @param adaptStrategies adaptation strategies of each pane.
* @param isDestinationHistoryAware `true` if the scaffold value calculation should be aware of the
* full destination history, instead of just the current destination. See
* [calculateThreePaneScaffoldValue] for more relevant details.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberSupportingPaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective =
calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
adaptStrategies: ThreePaneScaffoldAdaptStrategies =
SupportingPaneScaffoldDefaults.adaptStrategies(),
isDestinationHistoryAware: Boolean = true,
): ThreePaneScaffoldNavigator =
rememberSupportingPaneScaffoldNavigator(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware,
)
@ExperimentalMaterial3AdaptiveApi
@Composable
internal fun rememberThreePaneScaffoldNavigator(
scaffoldDirective: PaneScaffoldDirective,
adaptStrategies: ThreePaneScaffoldAdaptStrategies,
isDestinationHistoryAware: Boolean,
initialDestinationHistory: List>
): ThreePaneScaffoldNavigator =
rememberSaveable(
saver =
DefaultThreePaneScaffoldNavigator.saver(
scaffoldDirective,
adaptStrategies,
isDestinationHistoryAware
)
) {
DefaultThreePaneScaffoldNavigator(
initialDestinationHistory = initialDestinationHistory,
initialScaffoldDirective = scaffoldDirective,
initialAdaptStrategies = adaptStrategies,
initialIsDestinationHistoryAware = isDestinationHistoryAware
)
}
.apply {
this.scaffoldDirective = scaffoldDirective
this.adaptStrategies = adaptStrategies
this.isDestinationHistoryAware = isDestinationHistoryAware
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal class DefaultThreePaneScaffoldNavigator(
initialDestinationHistory: List>,
initialScaffoldDirective: PaneScaffoldDirective,
initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies,
initialIsDestinationHistoryAware: Boolean
) : ThreePaneScaffoldNavigator {
private val destinationHistory =
mutableStateListOf>().apply {
addAll(initialDestinationHistory)
}
override var scaffoldDirective by mutableStateOf(initialScaffoldDirective)
override var isDestinationHistoryAware by mutableStateOf(initialIsDestinationHistoryAware)
var adaptStrategies by mutableStateOf(initialAdaptStrategies)
override val currentDestination
get() = destinationHistory.lastOrNull()
override val scaffoldValue by derivedStateOf {
calculateScaffoldValue(destinationHistory.lastIndex)
}
// Must be updated whenever `destinationHistory` changes to keep in sync.
override val scaffoldState = MutableThreePaneScaffoldState(scaffoldValue)
override fun peekPreviousScaffoldValue(
backNavigationBehavior: BackNavigationBehavior
): ThreePaneScaffoldValue {
val index = getPreviousDestinationIndex(backNavigationBehavior)
return if (index == -1) scaffoldValue else calculateScaffoldValue(index)
}
override suspend fun navigateTo(pane: ThreePaneScaffoldRole, contentKey: T?) {
destinationHistory.add(ThreePaneScaffoldDestinationItem(pane, contentKey))
animateStateToCurrentScaffoldValue()
}
override fun canNavigateBack(backNavigationBehavior: BackNavigationBehavior): Boolean =
getPreviousDestinationIndex(backNavigationBehavior) >= 0
override suspend fun navigateBack(
backNavigationBehavior: BackNavigationBehavior,
): Boolean {
val previousDestinationIndex = getPreviousDestinationIndex(backNavigationBehavior)
if (previousDestinationIndex < 0) {
destinationHistory.clear()
animateStateToCurrentScaffoldValue()
return false
}
val targetSize = previousDestinationIndex + 1
while (destinationHistory.size > targetSize) {
destinationHistory.removeLastKt()
}
animateStateToCurrentScaffoldValue()
return true
}
override suspend fun seekBack(backNavigationBehavior: BackNavigationBehavior, fraction: Float) {
val previousScaffoldValue = peekPreviousScaffoldValue(backNavigationBehavior)
scaffoldState.seekTo(fraction, previousScaffoldValue)
}
private suspend fun animateStateToCurrentScaffoldValue() {
scaffoldState.animateTo(scaffoldValue)
}
private fun getPreviousDestinationIndex(backNavBehavior: BackNavigationBehavior): Int {
if (destinationHistory.size <= 1) {
// No previous destination
return -1
}
when (backNavBehavior) {
BackNavigationBehavior.PopLatest -> return destinationHistory.lastIndex - 1
BackNavigationBehavior.PopUntilScaffoldValueChange ->
for (previousDestinationIndex in destinationHistory.lastIndex - 1 downTo 0) {
val previousValue = calculateScaffoldValue(previousDestinationIndex)
if (previousValue != scaffoldValue) {
return previousDestinationIndex
}
}
BackNavigationBehavior.PopUntilCurrentDestinationChange ->
for (previousDestinationIndex in destinationHistory.lastIndex - 1 downTo 0) {
val destination = destinationHistory[previousDestinationIndex].pane
if (destination != currentDestination?.pane) {
return previousDestinationIndex
}
}
BackNavigationBehavior.PopUntilContentChange ->
for (previousDestinationIndex in destinationHistory.lastIndex - 1 downTo 0) {
val contentKey = destinationHistory[previousDestinationIndex].contentKey
if (contentKey != currentDestination?.contentKey) {
return previousDestinationIndex
}
// A scaffold value change also counts as a content change.
val previousValue = calculateScaffoldValue(previousDestinationIndex)
if (previousValue != scaffoldValue) {
return previousDestinationIndex
}
}
}
return -1
}
private fun calculateScaffoldValue(destinationIndex: Int) =
if (destinationIndex == -1) {
calculateThreePaneScaffoldValue(
scaffoldDirective.maxHorizontalPartitions,
adaptStrategies,
null
)
} else if (isDestinationHistoryAware) {
calculateThreePaneScaffoldValue(
scaffoldDirective.maxHorizontalPartitions,
adaptStrategies,
destinationHistory.subList(0, destinationIndex + 1)
)
} else {
calculateThreePaneScaffoldValue(
scaffoldDirective.maxHorizontalPartitions,
adaptStrategies,
destinationHistory[destinationIndex]
)
}
companion object {
/** To keep destination history saved */
fun saver(
initialScaffoldDirective: PaneScaffoldDirective,
initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies,
initialDestinationHistoryAware: Boolean
): Saver, *> {
val destinationItemSaver = destinationItemSaver()
return listSaver(
save = {
it.destinationHistory.fastMap { destination ->
with(destinationItemSaver) { save(destination) }
}
},
restore = {
DefaultThreePaneScaffoldNavigator(
initialDestinationHistory =
it.fastMap { savedDestination ->
destinationItemSaver.restore(savedDestination!!)!!
},
initialScaffoldDirective = initialScaffoldDirective,
initialAdaptStrategies = initialAdaptStrategies,
initialIsDestinationHistoryAware = initialDestinationHistoryAware
)
}
)
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal fun destinationItemSaver(): Saver, Any> =
listSaver(
save = { listOf(it.pane, it.contentKey) },
restore = {
@Suppress("UNCHECKED_CAST")
(ThreePaneScaffoldDestinationItem(
pane = it[0] as ThreePaneScaffoldRole,
contentKey = it[1] as T?
))
}
)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val DefaultListDetailPaneHistory: List> =
listOf(ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List))
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val DefaultSupportingPaneHistory: List> =
listOf(ThreePaneScaffoldDestinationItem(SupportingPaneScaffoldRole.Main))
© 2015 - 2025 Weber Informatics LLC | Privacy Policy