
commonMain.androidx.compose.material3.adaptive.layout.ThreePaneMotion.kt Maven / Gradle / Ivy
/*
* Copyright 2024 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.layout
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorizedFiniteAnimationSpec
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEachIndexed
import kotlin.jvm.JvmStatic
/**
* Calculates the default [ThreePaneMotion] of [ListDetailPaneScaffold] according to the given
* [ThreePaneScaffoldState]'s current and target values.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun ThreePaneScaffoldState.calculateListDetailPaneScaffoldMotion(): ThreePaneMotion =
calculateThreePaneMotion(ListDetailPaneScaffoldDefaults.PaneOrder)
/**
* Calculates the default [ThreePaneMotion] of [ListDetailPaneScaffold] according to the target and
* the previously remembered [ThreePaneScaffoldValue].
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun calculateListDetailPaneScaffoldMotion(
targetScaffoldValue: ThreePaneScaffoldValue
): ThreePaneMotion =
calculateThreePaneMotion(targetScaffoldValue, ListDetailPaneScaffoldDefaults.PaneOrder)
/**
* Calculates the default [ThreePaneMotion] of [SupportingPaneScaffold] according to the given
* [ThreePaneScaffoldState]'s current and target values.
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun ThreePaneScaffoldState.calculateSupportingPaneScaffoldMotion(): ThreePaneMotion =
calculateThreePaneMotion(SupportingPaneScaffoldDefaults.PaneOrder)
/**
* Calculates the default [ThreePaneMotion] of [SupportingPaneScaffold] according to the target and
* the previously remembered [ThreePaneScaffoldValue].
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun calculateSupportingPaneScaffoldMotion(
targetScaffoldValue: ThreePaneScaffoldValue
): ThreePaneMotion =
calculateThreePaneMotion(targetScaffoldValue, SupportingPaneScaffoldDefaults.PaneOrder)
@ExperimentalMaterial3AdaptiveApi
@Composable
internal fun ThreePaneScaffoldState.calculateThreePaneMotion(
paneOrder: ThreePaneScaffoldHorizontalOrder
): ThreePaneMotion {
class ThreePaneMotionHolder(var value: ThreePaneMotion)
val resultHolder = remember { ThreePaneMotionHolder(ThreePaneMotion.NoMotion) }
if (currentState != targetState) {
// Only update motions when the state changes to prevent unnecessary recomposition at the
// end of state transitions.
val ltrPaneOrder = paneOrder.toLtrOrder(LocalLayoutDirection.current)
val paneMotions = calculatePaneMotion(currentState, targetState, ltrPaneOrder)
resultHolder.value =
ThreePaneMotion(
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Primary)],
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Secondary)],
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Tertiary)]
)
}
return resultHolder.value
}
@ExperimentalMaterial3AdaptiveApi
@Composable
internal fun calculateThreePaneMotion(
targetScaffoldValue: ThreePaneScaffoldValue,
paneOrder: ThreePaneScaffoldHorizontalOrder
): ThreePaneMotion {
class ThreePaneScaffoldValueHolder(var value: ThreePaneScaffoldValue)
val layoutDirection = LocalLayoutDirection.current
val ltrPaneOrder =
remember(paneOrder, layoutDirection) { paneOrder.toLtrOrder(layoutDirection) }
val previousScaffoldValue = remember { ThreePaneScaffoldValueHolder(targetScaffoldValue) }
val threePaneMotion =
remember(targetScaffoldValue, ltrPaneOrder) {
val previousValue = previousScaffoldValue.value
previousScaffoldValue.value = targetScaffoldValue
val paneMotions = calculatePaneMotion(previousValue, targetScaffoldValue, ltrPaneOrder)
ThreePaneMotion(
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Primary)],
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Secondary)],
paneMotions[ltrPaneOrder.indexOf(ThreePaneScaffoldRole.Tertiary)]
)
}
return threePaneMotion
}
/**
* The class that provides motion settings for three pane scaffolds like [ListDetailPaneScaffold]
* and [SupportingPaneScaffold].
*
* @param primaryPaneMotion the specified [PaneMotion] of the primary pane, i.e.,
* [ListDetailPaneScaffoldRole.Detail] or [SupportingPaneScaffoldRole.Main].
* @param secondaryPaneMotion the specified [PaneMotion] of the secondary pane, i.e.,
* [ListDetailPaneScaffoldRole.List] or [SupportingPaneScaffoldRole.Supporting].
* @param tertiaryPaneMotion the specified [PaneMotion] of the tertiary pane, i.e.,
* [ListDetailPaneScaffoldRole.Extra] or [SupportingPaneScaffoldRole.Extra].
* @param sizeAnimationSpec the specified [FiniteAnimationSpec] when animating pane size changes.
* @param positionAnimationSpec the specified [FiniteAnimationSpec] when animating pane position
* changes.
* @param delayedPositionAnimationSpec the specified [FiniteAnimationSpec] when animating pane
* position changes with a delay to emphasize entering panes.
*/
@ExperimentalMaterial3AdaptiveApi
@Immutable
class ThreePaneMotion(
internal val primaryPaneMotion: PaneMotion,
internal val secondaryPaneMotion: PaneMotion,
internal val tertiaryPaneMotion: PaneMotion,
val sizeAnimationSpec: FiniteAnimationSpec =
ThreePaneMotionDefaults.PaneSizeAnimationSpec,
val positionAnimationSpec: FiniteAnimationSpec =
ThreePaneMotionDefaults.PanePositionAnimationSpec,
val delayedPositionAnimationSpec: FiniteAnimationSpec =
ThreePaneMotionDefaults.PanePositionAnimationSpecDelayed
) {
/**
* Makes a copy of [ThreePaneMotion] with override values.
*
* @param primaryPaneMotion the specified [PaneMotion] of the primary pane, i.e.,
* [ListDetailPaneScaffoldRole.Detail] or [SupportingPaneScaffoldRole.Main].
* @param secondaryPaneMotion the specified [PaneMotion] of the secondary pane, i.e.,
* [ListDetailPaneScaffoldRole.List] or [SupportingPaneScaffoldRole.Supporting].
* @param tertiaryPaneMotion the specified [PaneMotion] of the tertiary pane, i.e.,
* [ListDetailPaneScaffoldRole.Extra] or [SupportingPaneScaffoldRole.Extra].
* @param sizeAnimationSpec the specified [FiniteAnimationSpec] when animating pane size
* changes.
* @param positionAnimationSpec the specified [FiniteAnimationSpec] when animating pane position
* changes.
* @param delayedPositionAnimationSpec the specified [FiniteAnimationSpec] when animating pane
* position changes with a delay to emphasize entering panes.
*/
fun copy(
primaryPaneMotion: PaneMotion = this.primaryPaneMotion,
secondaryPaneMotion: PaneMotion = this.secondaryPaneMotion,
tertiaryPaneMotion: PaneMotion = this.tertiaryPaneMotion,
sizeAnimationSpec: FiniteAnimationSpec = this.sizeAnimationSpec,
positionAnimationSpec: FiniteAnimationSpec = this.positionAnimationSpec,
delayedPositionAnimationSpec: FiniteAnimationSpec =
this.delayedPositionAnimationSpec
): ThreePaneMotion =
ThreePaneMotion(
primaryPaneMotion,
secondaryPaneMotion,
tertiaryPaneMotion,
sizeAnimationSpec,
positionAnimationSpec,
delayedPositionAnimationSpec
)
/**
* Gets the specified [PaneMotion] of a given pane role.
*
* @param role the specified role of the pane, see [ListDetailPaneScaffoldRole] and
* [SupportingPaneScaffoldRole].
*/
operator fun get(role: ThreePaneScaffoldRole): PaneMotion =
when (role) {
ThreePaneScaffoldRole.Primary -> primaryPaneMotion
ThreePaneScaffoldRole.Secondary -> secondaryPaneMotion
ThreePaneScaffoldRole.Tertiary -> tertiaryPaneMotion
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThreePaneMotion) return false
if (primaryPaneMotion != other.primaryPaneMotion) return false
if (secondaryPaneMotion != other.secondaryPaneMotion) return false
if (tertiaryPaneMotion != other.tertiaryPaneMotion) return false
if (sizeAnimationSpec != other.sizeAnimationSpec) return false
if (positionAnimationSpec != other.positionAnimationSpec) return false
if (delayedPositionAnimationSpec != other.delayedPositionAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = primaryPaneMotion.hashCode()
result = 31 * result + secondaryPaneMotion.hashCode()
result = 31 * result + tertiaryPaneMotion.hashCode()
result = 31 * result + sizeAnimationSpec.hashCode()
result = 31 * result + positionAnimationSpec.hashCode()
result = 31 * result + delayedPositionAnimationSpec.hashCode()
return result
}
override fun toString(): String {
return "ThreePaneMotion(" +
"primaryPaneMotion=$primaryPaneMotion, " +
"secondaryPaneMotion=$secondaryPaneMotion, " +
"tertiaryPaneMotion=$tertiaryPaneMotion, " +
"sizeAnimationSpec=$sizeAnimationSpec, " +
"positionAnimationSpec=$positionAnimationSpec, " +
"delayedPositionAnimationSpec=$delayedPositionAnimationSpec)"
}
internal fun toPaneMotionList(ltrOrder: ThreePaneScaffoldHorizontalOrder): List =
listOf(this[ltrOrder.firstPane], this[ltrOrder.secondPane], this[ltrOrder.thirdPane])
companion object {
/** A default [ThreePaneMotion] instance that specifies no motions. */
val NoMotion =
ThreePaneMotion(PaneMotion.NoMotion, PaneMotion.NoMotion, PaneMotion.NoMotion)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Suppress("PrimitiveInCollection") // No way to get underlying Long of IntSize or IntOffset
internal class ThreePaneScaffoldMotionScopeImpl : PaneScaffoldMotionScope {
internal lateinit var threePaneMotion: ThreePaneMotion
private set
override val sizeAnimationSpec: FiniteAnimationSpec
get() = threePaneMotion.sizeAnimationSpec
override val positionAnimationSpec: FiniteAnimationSpec
get() = threePaneMotion.positionAnimationSpec
override val delayedPositionAnimationSpec: FiniteAnimationSpec
get() = threePaneMotion.delayedPositionAnimationSpec
override var scaffoldSize: IntSize = IntSize.Zero
override val paneMotionDataList: List =
listOf(PaneMotionData(), PaneMotionData(), PaneMotionData())
internal fun updateThreePaneMotion(
threePaneMotion: ThreePaneMotion,
ltrOrder: ThreePaneScaffoldHorizontalOrder
) {
val paneMotions = threePaneMotion.toPaneMotionList(ltrOrder)
this.paneMotionDataList.fastForEachIndexed { index, it -> it.motion = paneMotions[index] }
this.threePaneMotion = threePaneMotion
}
}
internal class DelayedSpringSpec(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
private val delayedRatio: Float,
visibilityThreshold: T? = null
) : FiniteAnimationSpec {
private val originalSpringSpec = spring(dampingRatio, stiffness, visibilityThreshold)
override fun vectorize(
converter: TwoWayConverter
): VectorizedFiniteAnimationSpec =
DelayedVectorizedSpringSpec(originalSpringSpec.vectorize(converter), delayedRatio)
}
private class DelayedVectorizedSpringSpec(
val originalVectorizedSpringSpec: VectorizedFiniteAnimationSpec,
val delayedRatio: Float,
) : VectorizedFiniteAnimationSpec {
var delayedTimeNanos: Long = 0
var cachedInitialValue: V? = null
var cachedTargetValue: V? = null
var cachedInitialVelocity: V? = null
var cachedOriginalDurationNanos: Long = 0
override fun getValueFromNanos(
playTimeNanos: Long,
initialValue: V,
targetValue: V,
initialVelocity: V
): V {
updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
return if (playTimeNanos <= delayedTimeNanos) {
initialValue
} else {
originalVectorizedSpringSpec.getValueFromNanos(
playTimeNanos - delayedTimeNanos,
initialValue,
targetValue,
initialVelocity
)
}
}
override fun getVelocityFromNanos(
playTimeNanos: Long,
initialValue: V,
targetValue: V,
initialVelocity: V
): V {
updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
return if (playTimeNanos <= delayedTimeNanos) {
initialVelocity
} else {
originalVectorizedSpringSpec.getVelocityFromNanos(
playTimeNanos - delayedTimeNanos,
initialValue,
targetValue,
initialVelocity
)
}
}
override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long {
updateDelayedTimeNanosIfNeeded(initialValue, targetValue, initialVelocity)
return cachedOriginalDurationNanos + delayedTimeNanos
}
private fun updateDelayedTimeNanosIfNeeded(
initialValue: V,
targetValue: V,
initialVelocity: V
) {
if (
initialValue != cachedInitialValue ||
targetValue != cachedTargetValue ||
initialVelocity != cachedInitialVelocity
) {
cachedOriginalDurationNanos =
originalVectorizedSpringSpec.getDurationNanos(
initialValue,
targetValue,
initialVelocity
)
delayedTimeNanos = (cachedOriginalDurationNanos * delayedRatio).toLong()
}
}
}
/** The default settings of three pane motions. */
@ExperimentalMaterial3AdaptiveApi
object ThreePaneMotionDefaults {
/** The default [FiniteAnimationSpec] of pane position animations. */
val PanePositionAnimationSpec: FiniteAnimationSpec =
spring(
dampingRatio = 0.8f,
stiffness = 600f,
visibilityThreshold = IntOffset.VisibilityThreshold
)
/**
* The default [FiniteAnimationSpec] of pane position animations with a delay. It's by default
* used in the case when an enter pane will intersect with exit panes, we delay the entering
* animation to emphasize the entering transition.
*/
val PanePositionAnimationSpecDelayed: FiniteAnimationSpec =
DelayedSpringSpec(
dampingRatio = 0.8f,
stiffness = 600f,
delayedRatio = 0.1f,
visibilityThreshold = IntOffset.VisibilityThreshold
)
/** The default [FiniteAnimationSpec] of pane size animations. */
val PaneSizeAnimationSpec: FiniteAnimationSpec =
spring(
dampingRatio = 0.8f,
stiffness = 600f,
visibilityThreshold = IntSize.VisibilityThreshold
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy