Implement delayed sliding in when switching panes
Test: unit test
Change-Id: I1a38e35fdec953696478b42dcd8eb0170289214d
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
index 4710f5b..51f89e5 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotionTest.kt
@@ -16,6 +16,10 @@
package androidx.compose.material3.adaptive.layout
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VectorizedAnimationSpec
+import androidx.compose.animation.core.spring
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -277,6 +281,61 @@
)
assertThat(motions).isEqualTo(ThreePaneMotion.NoMotion)
}
+
+ @Test
+ fun delayedSpring_identicalWithOriginPlusDelay() {
+ val delayedRatio = 0.5f
+
+ val originalSpec =
+ spring(
+ dampingRatio = 0.7f,
+ stiffness = 500f,
+ visibilityThreshold = 0.1f
+ ).vectorize(Float.VectorConverter)
+
+ val delayedSpec =
+ DelayedSpringSpec(
+ dampingRatio = 0.7f,
+ stiffness = 500f,
+ visibilityThreshold = 0.1f,
+ delayedRatio = delayedRatio,
+ ).vectorize(Float.VectorConverter)
+
+ val originalDurationNanos = originalSpec.getDurationNanos()
+ val delayedNanos = (originalDurationNanos * delayedRatio).toLong()
+
+ fun assertValuesAt(playTimeNanos: Long) {
+ assertValuesAreEqual(
+ originalSpec.getValueFromNanos(playTimeNanos),
+ delayedSpec.getValueFromNanos(playTimeNanos + delayedNanos)
+ )
+ }
+
+ assertValuesAt(0)
+ assertValuesAt((originalDurationNanos * 0.2).toLong())
+ assertValuesAt((originalDurationNanos * 0.35).toLong())
+ assertValuesAt((originalDurationNanos * 0.6).toLong())
+ assertValuesAt((originalDurationNanos * 0.85).toLong())
+ assertValuesAt(originalDurationNanos)
+ }
+
+ private fun VectorizedAnimationSpec<AnimationVector1D>.getDurationNanos(): Long =
+ getDurationNanos(InitialValue, TargetValue, InitialVelocity)
+
+ private fun VectorizedAnimationSpec<AnimationVector1D>.getValueFromNanos(
+ playTimeNanos: Long
+ ): Float = getValueFromNanos(playTimeNanos, InitialValue, TargetValue, InitialVelocity).value
+
+ private fun assertValuesAreEqual(value1: Float, value2: Float) {
+ assertThat(value1 - value2).isWithin(Tolerance)
+ }
+
+ companion object {
+ private val InitialValue = AnimationVector1D(0f)
+ private val TargetValue = AnimationVector1D(1f)
+ private val InitialVelocity = AnimationVector1D(0f)
+ private const val Tolerance = 0.001f
+ }
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
index ba986e4..3395ad3 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneMotion.kt
@@ -18,8 +18,12 @@
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorizedFiniteAnimationSpec
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
@@ -179,6 +183,90 @@
}
}
+internal class DelayedSpringSpec<T>(
+ dampingRatio: Float = Spring.DampingRatioNoBouncy,
+ stiffness: Float = Spring.StiffnessMedium,
+ private val delayedRatio: Float,
+ visibilityThreshold: T? = null
+) : FiniteAnimationSpec<T> {
+ private val originalSpringSpec = spring(dampingRatio, stiffness, visibilityThreshold)
+ override fun <V : AnimationVector> vectorize(
+ converter: TwoWayConverter<T, V>
+ ): VectorizedFiniteAnimationSpec<V> =
+ DelayedVectorizedSpringSpec(originalSpringSpec.vectorize(converter), delayedRatio)
+}
+
+private class DelayedVectorizedSpringSpec<V : AnimationVector>(
+ val originalVectorizedSpringSpec: VectorizedFiniteAnimationSpec<V>,
+ val delayedRatio: Float,
+) : VectorizedFiniteAnimationSpec<V> {
+ 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()
+ }
+ }
+}
+
@ExperimentalMaterial3AdaptiveApi
internal object ThreePaneMotionDefaults {
/**
@@ -192,8 +280,18 @@
visibilityThreshold = IntOffset.VisibilityThreshold
)
+ val PaneSpringSpecDelayed: DelayedSpringSpec<IntOffset> =
+ DelayedSpringSpec(
+ dampingRatio = 0.8f,
+ stiffness = 600f,
+ delayedRatio = 0.1f,
+ visibilityThreshold = IntOffset.VisibilityThreshold
+ )
+
private val slideInFromLeft = slideInHorizontally(PaneSpringSpec) { -it }
+ private val slideInFromLeftDelayed = slideInHorizontally(PaneSpringSpecDelayed) { -it }
private val slideInFromRight = slideInHorizontally(PaneSpringSpec) { it }
+ private val slideInFromRightDelayed = slideInHorizontally(PaneSpringSpecDelayed) { it }
private val slideOutToLeft = slideOutHorizontally(PaneSpringSpec) { -it }
private val slideOutToRight = slideOutHorizontally(PaneSpringSpec) { it }
@@ -219,9 +317,9 @@
val switchLeftTwoPanesMotion = ThreePaneMotion(
PaneSpringSpec,
- slideInFromLeft,
+ slideInFromLeftDelayed,
slideOutToLeft,
- slideInFromLeft,
+ slideInFromLeftDelayed,
slideOutToLeft,
EnterTransition.None,
ExitTransition.None
@@ -231,9 +329,9 @@
PaneSpringSpec,
EnterTransition.None,
ExitTransition.None,
- slideInFromRight,
+ slideInFromRightDelayed,
slideOutToRight,
- slideInFromRight,
+ slideInFromRightDelayed,
slideOutToRight
)
}