[go: nahoru, domu]

Merge "Implement delayed sliding in when switching panes" into androidx-main
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
     )
 }