Merge "Add easing parameter for fractionRange and timestampRange" into main
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index a30b780..79b3856 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -17,6 +17,8 @@
package com.android.compose.animation.scene
import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
@@ -140,6 +142,7 @@
fun fractionRange(
start: Float? = null,
end: Float? = null,
+ easing: Easing = LinearEasing,
builder: PropertyTransformationBuilder.() -> Unit,
)
}
@@ -182,6 +185,7 @@
fun timestampRange(
startMillis: Int? = null,
endMillis: Int? = null,
+ easing: Easing = LinearEasing,
builder: PropertyTransformationBuilder.() -> Unit,
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 6515cb8..a63b19a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DurationBasedAnimationSpec
+import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.VectorConverter
@@ -163,9 +164,10 @@
override fun fractionRange(
start: Float?,
end: Float?,
+ easing: Easing,
builder: PropertyTransformationBuilder.() -> Unit
) {
- range = TransformationRange(start, end)
+ range = TransformationRange(start, end, easing)
builder()
range = null
}
@@ -251,6 +253,7 @@
override fun timestampRange(
startMillis: Int?,
endMillis: Int?,
+ easing: Easing,
builder: PropertyTransformationBuilder.() -> Unit
) {
if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) {
@@ -263,7 +266,7 @@
val start = startMillis?.let { it.toFloat() / durationMillis }
val end = endMillis?.let { it.toFloat() / durationMillis }
- fractionRange(start, end, builder)
+ fractionRange(start, end, easing, builder)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 77ec891..eda8ede 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -16,6 +16,8 @@
package com.android.compose.animation.scene.transformation
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
import androidx.compose.ui.util.fastCoerceIn
@@ -90,11 +92,13 @@
data class TransformationRange(
val start: Float,
val end: Float,
+ val easing: Easing,
) {
constructor(
start: Float? = null,
- end: Float? = null
- ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified)
+ end: Float? = null,
+ easing: Easing = LinearEasing,
+ ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing)
init {
require(!start.isSpecified() || (start in 0f..1f))
@@ -103,17 +107,20 @@
}
/** Reverse this range. */
- fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start))
+ fun reversed() =
+ TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing)
/** Get the progress of this range given the global [transitionProgress]. */
fun progress(transitionProgress: Float): Float {
- return when {
- start.isSpecified() && end.isSpecified() ->
- ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f)
- !start.isSpecified() && !end.isSpecified() -> transitionProgress
- end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f)
- else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f)
- }
+ val progress =
+ when {
+ start.isSpecified() && end.isSpecified() ->
+ ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f)
+ !start.isSpecified() && !end.isSpecified() -> transitionProgress
+ end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f)
+ else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f)
+ }
+ return easing.transform(progress)
}
private fun Float.isSpecified() = this != BoundUnspecified
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index 68240b5..bed6cef 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.spring
@@ -107,6 +108,13 @@
fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) }
fractionRange(start = 0.2f) { fade(TestElements.Foo) }
fractionRange(end = 0.9f) { fade(TestElements.Foo) }
+ fractionRange(
+ start = 0.1f,
+ end = 0.8f,
+ easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ ) {
+ fade(TestElements.Foo)
+ }
}
}
@@ -118,6 +126,11 @@
TransformationRange(start = 0.1f, end = 0.8f),
TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified),
TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f),
+ TransformationRange(
+ start = 0.1f,
+ end = 0.8f,
+ CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ ),
)
}
@@ -130,6 +143,13 @@
timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) }
timestampRange(startMillis = 200) { fade(TestElements.Foo) }
timestampRange(endMillis = 400) { fade(TestElements.Foo) }
+ timestampRange(
+ startMillis = 100,
+ endMillis = 300,
+ easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ ) {
+ fade(TestElements.Foo)
+ }
}
}
@@ -141,6 +161,11 @@
TransformationRange(start = 100 / 500f, end = 300 / 500f),
TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified),
TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f),
+ TransformationRange(
+ start = 100 / 500f,
+ end = 300 / 500f,
+ easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ ),
)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt
new file mode 100644
index 0000000..07901f2
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 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 com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EasingTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testFractionRangeEasing() {
+ val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) },
+ toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ fractionRange(easing = easing) {
+ scaleSize(TestElements.Foo, width = 0f, height = 0f)
+ scaleSize(TestElements.Bar, width = 0f, height = 0f)
+ }
+ },
+ ) {
+ // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the
+ // transition so it starts at 200dp x 50dp.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) {
+ onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp)
+ }
+ at(16) {
+ // 25% linear progress is mapped to 68.5% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp)
+ }
+ at(32) {
+ // 50% linear progress is mapped to 89.5% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp)
+ }
+ at(48) {
+ // 75% linear progress is mapped to 97.8% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp)
+ }
+ after {
+ onElement(TestElements.Foo).assertDoesNotExist()
+ onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp)
+ }
+ }
+ }
+
+ @Test
+ fun testTimestampRangeEasing() {
+ val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) },
+ toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ timestampRange(easing = easing) {
+ scaleSize(TestElements.Foo, width = 0f, height = 0f)
+ scaleSize(TestElements.Bar, width = 0f, height = 0f)
+ }
+ },
+ ) {
+ // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the
+ // transition so it starts at 200dp x 50dp.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) {
+ onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp)
+ }
+ at(16) {
+ // 25% linear progress is mapped to 68.5% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp)
+ }
+ at(32) {
+ // 50% linear progress is mapped to 89.5% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp)
+ }
+ at(48) {
+ // 75% linear progress is mapped to 97.8% eased progress
+ onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp)
+ onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp)
+ }
+ after {
+ onElement(TestElements.Foo).assertDoesNotExist()
+ onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp)
+ }
+ }
+ }
+}