blob: d9bedb5f2a9f907a4e451949c8087aa618e4cc6b [file] [log] [blame]
/*
* 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
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import kotlin.math.max
import kotlin.math.min
/**
* A pane scaffold composable that can display up to three panes according to the instructions
* provided by [ThreePaneScaffoldValue] in the order that [ThreePaneScaffoldArrangement] specifies,
* and allocate margins and spacers according to [PaneScaffoldDirective].
*
* [ThreePaneScaffold] is the base composable functions of adaptive programming. Developers can
* freely pipeline the relevant adaptive signals and use them as input of the scaffold function
* to render the final adaptive layout.
*
* It's recommended to use [ThreePaneScaffold] with [calculateStandardPaneScaffoldDirective],
* [calculateThreePaneScaffoldValue] to follow the Material design guidelines on adaptive
* programming.
*
* @param modifier The modifier to be applied to the layout.
* @param scaffoldDirective The top-level directives about how the scaffold should arrange its panes.
* @param scaffoldValue The current adapted value of the scaffold.
* @param arrangement The arrangement of the panes in the scaffold.
* @param secondaryPane The content of the secondary pane that has a priority lower then the primary
* pane but higher than the tertiary pane.
* @param tertiaryPane The content of the tertiary pane that has the lowest priority.
* @param primaryPane The content of the primary pane that has the highest priority.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun ThreePaneScaffold(
modifier: Modifier,
scaffoldDirective: PaneScaffoldDirective,
scaffoldValue: ThreePaneScaffoldValue,
arrangement: ThreePaneScaffoldArrangement,
secondaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
tertiaryPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
primaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val ltrArrangement = remember(arrangement, layoutDirection) {
arrangement.toLtrArrangement(layoutDirection)
}
val previousScaffoldValue = remember { ThreePaneScaffoldValueHolder(scaffoldValue) }
val paneMotion = calculateThreePaneMotion(
previousScaffoldValue = previousScaffoldValue.value,
currentScaffoldValue = scaffoldValue,
arrangement = ltrArrangement
)
previousScaffoldValue.value = scaffoldValue
// Create PaneWrappers for each of the panes and map the transitions according to each pane
// role and arrangement.
val contents = listOf<@Composable () -> Unit>(
{
remember { ThreePaneScaffoldScopeImpl() }.apply {
paneAdaptedValue = scaffoldValue[ThreePaneScaffoldRole.Primary]
positionAnimationSpec = paneMotion.animationSpec
enterTransition = paneMotion.enterTransition(
ThreePaneScaffoldRole.Primary,
ltrArrangement
)
exitTransition = paneMotion.exitTransition(
ThreePaneScaffoldRole.Primary,
ltrArrangement
)
animationToolingLabel = "Primary"
}.primaryPane()
},
{
remember { ThreePaneScaffoldScopeImpl() }.apply {
paneAdaptedValue = scaffoldValue[ThreePaneScaffoldRole.Secondary]
positionAnimationSpec = paneMotion.animationSpec
enterTransition = paneMotion.enterTransition(
ThreePaneScaffoldRole.Secondary,
ltrArrangement
)
exitTransition = paneMotion.exitTransition(
ThreePaneScaffoldRole.Secondary,
ltrArrangement
)
animationToolingLabel = "Secondary"
}.secondaryPane()
},
{
if (tertiaryPane != null) {
remember { ThreePaneScaffoldScopeImpl() }.apply {
paneAdaptedValue = scaffoldValue[ThreePaneScaffoldRole.Tertiary]
positionAnimationSpec = paneMotion.animationSpec
enterTransition = paneMotion.enterTransition(
ThreePaneScaffoldRole.Tertiary,
ltrArrangement
)
exitTransition = paneMotion.exitTransition(
ThreePaneScaffoldRole.Tertiary,
ltrArrangement
)
animationToolingLabel = "Tertiary"
}.tertiaryPane()
}
},
)
val measurePolicy =
remember { ThreePaneContentMeasurePolicy(scaffoldDirective, scaffoldValue, ltrArrangement) }
measurePolicy.scaffoldDirective = scaffoldDirective
measurePolicy.scaffoldValue = scaffoldValue
measurePolicy.arrangement = ltrArrangement
LookaheadScope {
Layout(
contents = contents,
modifier = modifier,
measurePolicy = measurePolicy
)
}
}
/**
* Holds the transitions that can be applied to the different panes.
*/
@ExperimentalMaterial3AdaptiveApi
@Immutable
internal class ThreePaneMotion internal constructor(
internal val animationSpec: FiniteAnimationSpec<IntOffset> = snap(),
private val firstPaneEnterTransition: EnterTransition = EnterTransition.None,
private val firstPaneExitTransition: ExitTransition = ExitTransition.None,
private val secondPaneEnterTransition: EnterTransition = EnterTransition.None,
private val secondPaneExitTransition: ExitTransition = ExitTransition.None,
private val thirdPaneEnterTransition: EnterTransition = EnterTransition.None,
private val thirdPaneExitTransition: ExitTransition = ExitTransition.None
) {
/**
* Resolves and returns the [EnterTransition] for the given [ThreePaneScaffoldRole] at the given
* [ThreePaneScaffoldArrangement].
*/
fun enterTransition(
role: ThreePaneScaffoldRole,
arrangement: ThreePaneScaffoldArrangement
): EnterTransition {
// Quick return in case this instance is the NoMotion one.
if (this === NoMotion) return EnterTransition.None
return when (arrangement.indexOf(role)) {
0 -> firstPaneEnterTransition
1 -> secondPaneEnterTransition
else -> thirdPaneEnterTransition
}
}
/**
* Resolves and returns the [ExitTransition] for the given [ThreePaneScaffoldRole] at the given
* [ThreePaneScaffoldArrangement].
*/
fun exitTransition(
role: ThreePaneScaffoldRole,
arrangement: ThreePaneScaffoldArrangement
): ExitTransition {
// Quick return in case this instance is the NoMotion one.
if (this === NoMotion) return ExitTransition.None
return when (arrangement.indexOf(role)) {
0 -> firstPaneExitTransition
1 -> secondPaneExitTransition
else -> thirdPaneExitTransition
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThreePaneMotion) return false
if (this.animationSpec != other.animationSpec) return false
if (this.firstPaneEnterTransition != other.firstPaneEnterTransition) return false
if (this.firstPaneExitTransition != other.firstPaneExitTransition) return false
if (this.secondPaneEnterTransition != other.secondPaneEnterTransition) return false
if (this.secondPaneExitTransition != other.secondPaneExitTransition) return false
if (this.thirdPaneEnterTransition != other.thirdPaneEnterTransition) return false
if (this.thirdPaneExitTransition != other.thirdPaneExitTransition) return false
return true
}
override fun hashCode(): Int {
var result = animationSpec.hashCode()
result = 31 * result + firstPaneEnterTransition.hashCode()
result = 31 * result + firstPaneExitTransition.hashCode()
result = 31 * result + secondPaneEnterTransition.hashCode()
result = 31 * result + secondPaneExitTransition.hashCode()
result = 31 * result + thirdPaneEnterTransition.hashCode()
result = 31 * result + thirdPaneExitTransition.hashCode()
return result
}
companion object {
/**
* A ThreePaneMotion with all transitions set to [EnterTransition.None] and
* [ExitTransition.None].
*/
val NoMotion = ThreePaneMotion()
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class ThreePaneScaffoldValueHolder(var value: ThreePaneScaffoldValue)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun calculateThreePaneMotion(
previousScaffoldValue: ThreePaneScaffoldValue,
currentScaffoldValue: ThreePaneScaffoldValue,
arrangement: ThreePaneScaffoldArrangement
): ThreePaneMotion {
if (previousScaffoldValue.equals(currentScaffoldValue)) {
return ThreePaneMotion.NoMotion
}
val previousExpandedCount = getExpandedCount(previousScaffoldValue)
val currentExpandedCount = getExpandedCount(currentScaffoldValue)
if (previousExpandedCount != currentExpandedCount) {
// TODO(conradchen): Address this case
return ThreePaneMotion.NoMotion
}
return when (previousExpandedCount) {
1 -> when (PaneAdaptedValue.Expanded) {
previousScaffoldValue[arrangement.firstPane] -> {
ThreePaneScaffoldDefaults.panesRightMotion
}
previousScaffoldValue[arrangement.thirdPane] -> {
ThreePaneScaffoldDefaults.panesLeftMotion
}
currentScaffoldValue[arrangement.thirdPane] -> {
ThreePaneScaffoldDefaults.panesRightMotion
}
else -> {
ThreePaneScaffoldDefaults.panesLeftMotion
}
}
2 -> when {
previousScaffoldValue[arrangement.firstPane] != PaneAdaptedValue.Expanded -> {
ThreePaneScaffoldDefaults.panesLeftMotion
}
previousScaffoldValue[arrangement.thirdPane] != PaneAdaptedValue.Expanded -> {
ThreePaneScaffoldDefaults.panesRightMotion
}
previousScaffoldValue[arrangement.secondPane] != PaneAdaptedValue.Expanded &&
currentScaffoldValue[arrangement.firstPane] != PaneAdaptedValue.Expanded -> {
ThreePaneScaffoldDefaults.replaceLeftPaneMotion
}
else -> {
ThreePaneScaffoldDefaults.replaceRightPaneMotion
}
}
else -> {
// Should not happen
ThreePaneMotion.NoMotion
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun getExpandedCount(scaffoldValue: ThreePaneScaffoldValue): Int {
var count = 0
if (scaffoldValue.primary == PaneAdaptedValue.Expanded) {
count++
}
if (scaffoldValue.secondary == PaneAdaptedValue.Expanded) {
count++
}
if (scaffoldValue.tertiary == PaneAdaptedValue.Expanded) {
count++
}
return count
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class ThreePaneContentMeasurePolicy(
var scaffoldDirective: PaneScaffoldDirective,
var scaffoldValue: ThreePaneScaffoldValue,
var arrangement: ThreePaneScaffoldArrangement
) : MultiContentMeasurePolicy {
/**
* Data class that is used to store the position and width of an expanded pane to be reused when
* the pane is being hidden.
*/
private data class PanePlacement(var positionX: Int = 0, var measuredWidth: Int = 0)
private val placementsCache = mapOf(
ThreePaneScaffoldRole.Primary to PanePlacement(),
ThreePaneScaffoldRole.Secondary to PanePlacement(),
ThreePaneScaffoldRole.Tertiary to PanePlacement()
)
override fun MeasureScope.measure(
measurables: List<List<Measurable>>,
constraints: Constraints
): MeasureResult {
val primaryMeasurables = measurables[0]
val secondaryMeasurables = measurables[1]
val tertiaryMeasurables = measurables[2]
return layout(constraints.maxWidth, constraints.maxHeight) {
if (coordinates == null) {
return@layout
}
val visiblePanes = getPanesMeasurables(
arrangement = arrangement,
primaryMeasurables = primaryMeasurables,
scaffoldValue = scaffoldValue,
secondaryMeasurables = secondaryMeasurables,
tertiaryMeasurables = tertiaryMeasurables
) {
it != PaneAdaptedValue.Hidden
}
val hiddenPanes = getPanesMeasurables(
arrangement = arrangement,
primaryMeasurables = primaryMeasurables,
scaffoldValue = scaffoldValue,
secondaryMeasurables = secondaryMeasurables,
tertiaryMeasurables = tertiaryMeasurables
) {
it == PaneAdaptedValue.Hidden
}
val verticalSpacerSize = scaffoldDirective.gutterSizes.verticalSpacerSize.roundToPx()
val leftContentPadding =
scaffoldDirective.gutterSizes.contentPadding.calculateLeftPadding(
layoutDirection
).roundToPx()
val rightContentPadding =
scaffoldDirective.gutterSizes.contentPadding.calculateRightPadding(
layoutDirection
).roundToPx()
val topContentPadding =
scaffoldDirective.gutterSizes.contentPadding.calculateTopPadding().roundToPx()
val bottomContentPadding =
scaffoldDirective.gutterSizes.contentPadding.calculateBottomPadding().roundToPx()
val outerBounds = IntRect(
leftContentPadding,
topContentPadding,
constraints.maxWidth - rightContentPadding,
constraints.maxHeight - bottomContentPadding
)
if (scaffoldDirective.excludedBounds.isNotEmpty()) {
val layoutBounds = coordinates!!.boundsInWindow()
val layoutPhysicalPartitions = mutableListOf<Rect>()
var actualLeft = layoutBounds.left + leftContentPadding
var actualRight = layoutBounds.right - rightContentPadding
val actualTop = layoutBounds.top + topContentPadding
val actualBottom = layoutBounds.bottom - bottomContentPadding
// Assume hinge bounds are sorted from left to right, non-overlapped.
scaffoldDirective.excludedBounds.fastForEach { hingeBound ->
if (hingeBound.left <= actualLeft) {
// The hinge is at the left of the layout, adjust the left edge of
// the current partition to the actual displayable bounds.
actualLeft = max(actualLeft, hingeBound.right)
} else if (hingeBound.right >= actualRight) {
// The hinge is right at the right of the layout and there's no more
// room for more partitions, adjust the right edge of the current
// partition to the actual displayable bounds.
actualRight = min(hingeBound.left, actualRight)
return@fastForEach
} else {
// The hinge is inside the layout, add the current partition to the list
// and move the left edge of the next partition to the right of the
// hinge.
layoutPhysicalPartitions.add(
Rect(actualLeft, actualTop, hingeBound.left, actualBottom)
)
actualLeft +=
max(hingeBound.right, hingeBound.left + verticalSpacerSize)
}
}
if (actualLeft < actualRight) {
// The last partition
layoutPhysicalPartitions.add(
Rect(actualLeft, actualTop, actualRight, actualBottom)
)
}
if (layoutPhysicalPartitions.size == 0) {
// Display nothing
} else if (layoutPhysicalPartitions.size == 1) {
measureAndPlacePanes(
layoutPhysicalPartitions[0],
verticalSpacerSize,
visiblePanes,
isLookingAhead
)
} else if (layoutPhysicalPartitions.size < visiblePanes.size) {
// Note that the only possible situation is we have only two physical partitions
// but three expanded panes to show. In this case fit two panes in the larger
// partition.
if (layoutPhysicalPartitions[0].width > layoutPhysicalPartitions[1].width) {
measureAndPlacePanes(
layoutPhysicalPartitions[0],
verticalSpacerSize,
visiblePanes.subList(0, 2),
isLookingAhead
)
measureAndPlacePane(
layoutPhysicalPartitions[1],
visiblePanes[2],
isLookingAhead
)
} else {
measureAndPlacePane(
layoutPhysicalPartitions[0],
visiblePanes[0],
isLookingAhead
)
measureAndPlacePanes(
layoutPhysicalPartitions[1],
verticalSpacerSize,
visiblePanes.subList(1, 3),
isLookingAhead
)
}
} else {
// Layout each visible pane in a physical partition
visiblePanes.fastForEachIndexed { index, paneMeasurable ->
measureAndPlacePane(
layoutPhysicalPartitions[index],
paneMeasurable,
isLookingAhead
)
}
}
} else {
measureAndPlacePanesWithLocalBounds(
outerBounds,
verticalSpacerSize,
visiblePanes,
isLookingAhead
)
}
// Place the hidden panes. Those should only exist when isLookingAhead = true.
// Placing these type of pane during the lookahead phase ensures a proper motion
// at the AnimatedVisibility.
// The placement is done using the outerBounds, as the placementsCache holds
// absolute position values.
placeHiddenPanes(
outerBounds.top,
outerBounds.height,
hiddenPanes
)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun MeasureScope.getPanesMeasurables(
arrangement: ThreePaneScaffoldArrangement,
primaryMeasurables: List<Measurable>,
scaffoldValue: ThreePaneScaffoldValue,
secondaryMeasurables: List<Measurable>,
tertiaryMeasurables: List<Measurable>,
predicate: (PaneAdaptedValue) -> Boolean
): List<PaneMeasurable> {
return buildList {
arrangement.forEach { role ->
if (predicate(scaffoldValue[role])) {
when (role) {
ThreePaneScaffoldRole.Primary -> {
createPaneMeasurableIfNeeded(
primaryMeasurables,
ThreePaneScaffoldDefaults.PrimaryPanePriority,
role,
ThreePaneScaffoldDefaults.PrimaryPanePreferredWidth
.roundToPx()
)
}
ThreePaneScaffoldRole.Secondary -> {
createPaneMeasurableIfNeeded(
secondaryMeasurables,
ThreePaneScaffoldDefaults.SecondaryPanePriority,
role,
ThreePaneScaffoldDefaults.SecondaryPanePreferredWidth
.roundToPx()
)
}
ThreePaneScaffoldRole.Tertiary -> {
createPaneMeasurableIfNeeded(
tertiaryMeasurables,
ThreePaneScaffoldDefaults.TertiaryPanePriority,
role,
ThreePaneScaffoldDefaults.TertiaryPanePreferredWidth
.roundToPx()
)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun MutableList<PaneMeasurable>.createPaneMeasurableIfNeeded(
measurables: List<Measurable>,
priority: Int,
role: ThreePaneScaffoldRole,
defaultPreferredWidth: Int
) {
if (measurables.isNotEmpty()) {
add(PaneMeasurable(measurables[0], priority, role, defaultPreferredWidth))
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Placeable.PlacementScope.measureAndPlacePane(
partitionBounds: Rect,
measurable: PaneMeasurable,
isLookingAhead: Boolean
) {
val localBounds = getLocalBounds(partitionBounds)
measurable.measuredWidth = localBounds.width
measurable.apply {
measure(Constraints.fixed(measuredWidth, localBounds.height))
.place(localBounds.left, localBounds.top)
}
if (isLookingAhead) {
// Cache the values to be used when this measurable role is being hidden.
// See placeHiddenPanes.
val cachedPanePlacement = placementsCache[measurable.role]!!
cachedPanePlacement.measuredWidth = measurable.measuredWidth
cachedPanePlacement.positionX = localBounds.left
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Placeable.PlacementScope.measureAndPlacePanes(
partitionBounds: Rect,
spacerSize: Int,
measurables: List<PaneMeasurable>,
isLookingAhead: Boolean
) {
measureAndPlacePanesWithLocalBounds(
getLocalBounds(partitionBounds),
spacerSize,
measurables,
isLookingAhead
)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Placeable.PlacementScope.measureAndPlacePanesWithLocalBounds(
partitionBounds: IntRect,
spacerSize: Int,
measurables: List<PaneMeasurable>,
isLookingAhead: Boolean
) {
if (measurables.isEmpty()) {
return
}
val allocatableWidth = partitionBounds.width - (measurables.size - 1) * spacerSize
val totalPreferredWidth = measurables.sumOf { it.measuredWidth }
if (allocatableWidth > totalPreferredWidth) {
// Allocate the remaining space to the pane with the highest priority.
measurables.maxBy {
it.priority
}.measuredWidth += allocatableWidth - totalPreferredWidth
} else if (allocatableWidth < totalPreferredWidth) {
// Scale down all panes to fit in the available space.
val scale = allocatableWidth.toFloat() / totalPreferredWidth
measurables.fastForEach {
it.measuredWidth = (it.measuredWidth * scale).toInt()
}
}
var positionX = partitionBounds.left
measurables.fastForEach {
it.measure(Constraints.fixed(it.measuredWidth, partitionBounds.height))
.place(positionX, partitionBounds.top)
if (isLookingAhead) {
// Cache the values to be used when this measurable's role is being hidden.
// See placeHiddenPanes.
val cachedPanePlacement = placementsCache[it.role]!!
cachedPanePlacement.measuredWidth = it.measuredWidth
cachedPanePlacement.positionX = positionX
}
positionX += it.measuredWidth + spacerSize
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Placeable.PlacementScope.placeHiddenPanes(
partitionTop: Int,
partitionHeight: Int,
measurables: List<PaneMeasurable>
) {
// When panes are being hidden, apply each pane's width and position from the cache to
// maintain the those before it's hidden by the AnimatedVisibility.
measurables.fastForEach {
val cachedPanePlacement = placementsCache[it.role]!!
it.measure(
Constraints.fixed(
width = cachedPanePlacement.measuredWidth,
height = partitionHeight
)
).place(cachedPanePlacement.positionX, partitionTop)
}
}
private fun Placeable.PlacementScope.getLocalBounds(bounds: Rect): IntRect {
return bounds.translate(coordinates!!.windowToLocal(Offset.Zero)).roundToIntRect()
}
}
/**
* A conditional [Modifier.clipToBounds] that will only clip when the given [adaptedValue] is
* [PaneAdaptedValue.Hidden].
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Modifier.clipToBounds(adaptedValue: PaneAdaptedValue): Modifier =
if (adaptedValue == PaneAdaptedValue.Hidden) this.clipToBounds() else this
/**
* The root composable of pane contents in a [ThreePaneScaffold] that supports default motions
* during pane switching. It's recommended to use this composable to wrap your own contents when
* passing them into pane parameters of the scaffold functions, therefore your panes can have a
* nice default animation for free.
*
* See usage samples at:
* @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
* @sample androidx.compose.material3.adaptive.samples.ListDetailExtraPaneScaffoldSample
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun ThreePaneScaffoldScope.AnimatedPane(
modifier: Modifier,
content: (@Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit),
) {
AnimatedVisibility(
visible = paneAdaptedValue == PaneAdaptedValue.Expanded,
modifier = modifier
.clipToBounds(paneAdaptedValue)
.then(
if (paneAdaptedValue == PaneAdaptedValue.Expanded) {
Modifier.animateBounds(
// TODO Figure out why we need to pass a non-null here to get the bounds
// animation going on the first navigation event that pass in the spec
// later on. To resolve this, we default to the paneSpringSpec().
// Otherwise, the first motion shows a snap instead of a smooth
// transition.
positionAnimationSpec = positionAnimationSpec
?: ThreePaneScaffoldDefaults.PaneSpringSpec
)
} else {
Modifier
}
),
enter = enterTransition,
exit = exitTransition,
label = "AnimatedVisibility: $animationToolingLabel"
) {
content.invoke(this@AnimatedPane, paneAdaptedValue)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class PaneMeasurable(
val measurable: Measurable,
val priority: Int,
val role: ThreePaneScaffoldRole,
defaultPreferredWidth: Int
) : Measurable by measurable {
private val data = ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData())
var measuredWidth = if (data.preferredWidth == null || data.preferredWidth!!.isNaN()) {
defaultPreferredWidth
} else {
data.preferredWidth!!.toInt()
}
}
/**
* Scope for the panes of [ThreePaneScaffold].
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
interface ThreePaneScaffoldScope : PaneScaffoldScope {
/**
* The adapted value of the associated pane to the scope.
*/
val paneAdaptedValue: PaneAdaptedValue
/**
* The position animation spec of the associated pane to the scope. [AnimatedPane] will use this
* value to perform pane animations during scaffold state changes.
*/
val positionAnimationSpec: FiniteAnimationSpec<IntOffset>?
/**
* The [EnterTransition] of the associated pane. [AnimatedPane] will use this value to perform
* pane entering animations when it's showing during scaffold state changes.
*/
val enterTransition: EnterTransition
/**
* The [ExitTransition] of the associated pane. [AnimatedPane] will use this value to perform
* pane exiting animations when it's hiding during scaffold state changes.
*/
val exitTransition: ExitTransition
/**
* The label will be used by [AnimatedPane] to provide tooling labels to the foundation
* animation APIs like [AnimatedVisibility].
*/
val animationToolingLabel: String
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class ThreePaneScaffoldScopeImpl : ThreePaneScaffoldScope, PaneScaffoldScopeImpl() {
override var paneAdaptedValue: PaneAdaptedValue = PaneAdaptedValue.Hidden
override var positionAnimationSpec: FiniteAnimationSpec<IntOffset>? = null
override var enterTransition: EnterTransition = EnterTransition.None
override var exitTransition: ExitTransition = ExitTransition.None
override var animationToolingLabel: String = ""
}
/**
* Provides default values of [ThreePaneScaffold] and the calculation functions of
* [ThreePaneScaffoldValue].
*/
@ExperimentalMaterial3AdaptiveApi
object ThreePaneScaffoldDefaults {
/**
* Denotes [ThreePaneScaffold] to use the list-detail arrangement to arrange its panes, which
* allocates panes in the order of secondary, primary, and tertiary form start to end.
*/
// TODO(conradchen/sgibly): Consider moving this to the ListDetailPaneScaffoldDefaults
val ListDetailLayoutArrangement = ThreePaneScaffoldArrangement(
ThreePaneScaffoldRole.Secondary,
ThreePaneScaffoldRole.Primary,
ThreePaneScaffoldRole.Tertiary
)
/**
* Denotes [ThreePaneScaffold] to use the supporting-pane arrangement to arrange its panes,
* which allocates panes in the order of secondary, primary, and tertiary form start to end.
*/
val SupportingPaneLayoutArrangement = ThreePaneScaffoldArrangement(
ThreePaneScaffoldRole.Primary,
ThreePaneScaffoldRole.Secondary,
ThreePaneScaffoldRole.Tertiary
)
/**
* The default preferred width of [ThreePaneScaffoldRole.Secondary]. See more details in
* [ThreePaneScaffoldScope.preferredWidth].
*/
val SecondaryPanePreferredWidth = 412.dp
/**
* The default preferred width of [ThreePaneScaffoldRole.Tertiary]. See more details in
* [ThreePaneScaffoldScope.preferredWidth].
*/
val TertiaryPanePreferredWidth = 412.dp
// TODO(conradchen): maybe remove this after addressing unspecified preferred width issue
internal val PrimaryPanePreferredWidth = 600.dp
// TODO(conradchen): consider declaring a value class for priority
internal const val PrimaryPanePriority = 10
internal const val SecondaryPanePriority = 5
internal const val TertiaryPanePriority = 1
/**
* Creates a default [ThreePaneScaffoldAdaptStrategies].
*
* @param primaryPaneAdaptStrategy the adapt strategy of the primary pane
* @param secondaryPaneAdaptStrategy the adapt strategy of the secondary pane
* @param tertiaryPaneAdaptStrategy the adapt strategy of the tertiary pane
*/
fun adaptStrategies(
primaryPaneAdaptStrategy: AdaptStrategy = AdaptStrategy.Hide,
secondaryPaneAdaptStrategy: AdaptStrategy = AdaptStrategy.Hide,
tertiaryPaneAdaptStrategy: AdaptStrategy = AdaptStrategy.Hide,
): ThreePaneScaffoldAdaptStrategies =
ThreePaneScaffoldAdaptStrategies(
primaryPaneAdaptStrategy,
secondaryPaneAdaptStrategy,
tertiaryPaneAdaptStrategy
)
/**
* A default [SpringSpec] for the panes motion.
*/
// TODO(conradchen): open this to public when we support motion customization
internal val PaneSpringSpec: SpringSpec<IntOffset> =
spring(
dampingRatio = 0.7f,
stiffness = 600f,
visibilityThreshold = IntOffset.VisibilityThreshold
)
private val slideInFromLeft = slideInHorizontally(PaneSpringSpec) { -it }
private val slideInFromRight = slideInHorizontally(PaneSpringSpec) { it }
private val slideOutToLeft = slideOutHorizontally(PaneSpringSpec) { -it }
private val slideOutToRight = slideOutHorizontally(PaneSpringSpec) { it }
internal val panesLeftMotion = ThreePaneMotion(
PaneSpringSpec,
slideInFromLeft,
slideOutToRight,
slideInFromLeft,
slideOutToRight,
slideInFromLeft,
slideOutToRight
)
internal val panesRightMotion = ThreePaneMotion(
PaneSpringSpec,
slideInFromRight,
slideOutToLeft,
slideInFromRight,
slideOutToLeft,
slideInFromRight,
slideOutToLeft
)
// TODO(conradchen): figure out how to add delay and zOffset to spring animations
internal val replaceLeftPaneMotion = ThreePaneMotion(
PaneSpringSpec,
slideInFromLeft,
slideOutToLeft,
slideInFromLeft,
slideOutToLeft,
EnterTransition.None,
ExitTransition.None
)
// TODO(conradchen): figure out how to add delay and zOffset to spring animations
internal val replaceRightPaneMotion = ThreePaneMotion(
PaneSpringSpec,
EnterTransition.None,
ExitTransition.None,
slideInFromRight,
slideOutToRight,
slideInFromRight,
slideOutToRight
)
}