summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt6
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt7
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt26
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt20
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt190
5 files changed, 232 insertions, 17 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
index f4e39023edfe..f567e5c3be07 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.snap
import androidx.compose.ui.geometry.Offset
@@ -35,11 +36,12 @@ import com.android.compose.ui.util.fastMap
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions(
- private val transitionSpecs: List<TransitionSpec>,
+ @get:VisibleForTesting val transitionSpecs: List<TransitionSpec>,
) {
private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
- internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ @VisibleForTesting
+ fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
index fb12b90d7d3e..b465c57949ce 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -127,6 +127,13 @@ interface TransitionBuilder : PropertyTransformationBuilder {
* the result.
*/
fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
+
+ /**
+ * Adds the transformations in [builder] but in reversed order. This allows you to partially
+ * reuse the definition of the transition from scene `Foo` to scene `Bar` inside the definition
+ * of the transition from scene `Bar` to scene `Foo`.
+ */
+ fun reversed(builder: TransitionBuilder.() -> Unit)
}
@TransitionDsl
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 48d5638e8b4e..9f08cbaf873b 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -80,6 +80,7 @@ internal class TransitionBuilderImpl : TransitionBuilder {
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
private var range: TransformationRange? = null
+ private var reversed = false
private val durationMillis: Int by lazy {
val spec = spec
if (spec !is DurationBasedAnimationSpec) {
@@ -93,6 +94,12 @@ internal class TransitionBuilderImpl : TransitionBuilder {
transformations.add(PunchHole(matcher, bounds, shape))
}
+ override fun reversed(builder: TransitionBuilder.() -> Unit) {
+ reversed = true
+ builder()
+ reversed = false
+ }
+
override fun fractionRange(
start: Float?,
end: Float?,
@@ -122,11 +129,20 @@ internal class TransitionBuilderImpl : TransitionBuilder {
}
private fun transformation(transformation: PropertyTransformation<*>) {
- if (range != null) {
- transformations.add(RangedPropertyTransformation(transformation, range!!))
- } else {
- transformations.add(transformation)
- }
+ val transformation =
+ if (range != null) {
+ RangedPropertyTransformation(transformation, range!!)
+ } else {
+ transformation
+ }
+
+ transformations.add(
+ if (reversed) {
+ transformation.reverse()
+ } else {
+ transformation
+ }
+ )
}
override fun fade(matcher: ElementMatcher) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
index ce6749da2711..ffb9ee96898b 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -30,6 +30,14 @@ sealed interface Transformation {
*/
val matcher: ElementMatcher
+ /**
+ * The range during which the transformation is applied. If it is `null`, then the
+ * transformation will be applied throughout the whole scene transition.
+ */
+ // TODO(b/240432457): Move this back to PropertyTransformation.
+ val range: TransformationRange?
+ get() = null
+
/*
* Reverse this transformation. This is called when we use Transition(from = A, to = B) when
* animating from B to A and there is no Transition(from = B, to = A) defined.
@@ -53,13 +61,6 @@ internal interface ModifierTransformation : Transformation {
/** A transformation that changes the value of an element property, like its size or offset. */
internal sealed interface PropertyTransformation<T> : Transformation {
/**
- * The range during which the transformation is applied. If it is `null`, then the
- * transformation will be applied throughout the whole scene transition.
- */
- val range: TransformationRange?
- get() = null
-
- /**
* Transform [value], i.e. the value of the transformed property without this transformation.
*/
// TODO(b/290184746): Figure out a public API for custom transformations that don't have access
@@ -92,8 +93,7 @@ internal class RangedPropertyTransformation<T>(
}
/** The progress-based range of a [PropertyTransformation]. */
-data class TransformationRange
-private constructor(
+data class TransformationRange(
val start: Float,
val end: Float,
) {
@@ -133,6 +133,6 @@ private constructor(
}
companion object {
- private const val BoundUnspecified = Float.MIN_VALUE
+ const val BoundUnspecified = Float.MIN_VALUE
}
}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
new file mode 100644
index 000000000000..fa94b25028a2
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -0,0 +1,190 @@
+/*
+ * 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
+
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.tween
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationRange
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TransitionDslTest {
+ @Test
+ fun emptyTransitions() {
+ val transitions = transitions {}
+ assertThat(transitions.transitionSpecs).isEmpty()
+ }
+
+ @Test
+ fun manyTransitions() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB)
+ from(TestScenes.SceneB, to = TestScenes.SceneC)
+ from(TestScenes.SceneC, to = TestScenes.SceneA)
+ }
+ assertThat(transitions.transitionSpecs).hasSize(3)
+ }
+
+ @Test
+ fun toFromBuilders() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB)
+ from(TestScenes.SceneB)
+ to(TestScenes.SceneC)
+ }
+
+ assertThat(transitions.transitionSpecs)
+ .comparingElementsUsing(
+ Correspondence.transforming<TransitionSpec, Pair<SceneKey?, SceneKey?>>(
+ { it?.from to it?.to },
+ "has (from, to) equal to"
+ )
+ )
+ .containsExactly(
+ TestScenes.SceneA to TestScenes.SceneB,
+ TestScenes.SceneB to null,
+ null to TestScenes.SceneC,
+ )
+ }
+
+ @Test
+ fun defaultTransitionSpec() {
+ val transitions = transitions { from(TestScenes.SceneA, to = TestScenes.SceneB) }
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.spec).isInstanceOf(SpringSpec::class.java)
+ }
+
+ @Test
+ fun customTransitionSpec() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) { spec = tween(durationMillis = 42) }
+ }
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.spec).isInstanceOf(TweenSpec::class.java)
+ assertThat((transition.spec as TweenSpec).durationMillis).isEqualTo(42)
+ }
+
+ @Test
+ fun defaultRange() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
+ }
+
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.transformations.size).isEqualTo(1)
+ assertThat(transition.transformations.single().range).isEqualTo(null)
+ }
+
+ @Test
+ fun fractionRange() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) }
+ fractionRange(start = 0.2f) { fade(TestElements.Foo) }
+ fractionRange(end = 0.9f) { fade(TestElements.Foo) }
+ }
+ }
+
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.transformations)
+ .comparingElementsUsing(TRANSFORMATION_RANGE)
+ .containsExactly(
+ TransformationRange(start = 0.1f, end = 0.8f),
+ TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified),
+ TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f),
+ )
+ }
+
+ @Test
+ fun timestampRange() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ spec = tween(500)
+
+ timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) }
+ timestampRange(startMillis = 200) { fade(TestElements.Foo) }
+ timestampRange(endMillis = 400) { fade(TestElements.Foo) }
+ }
+ }
+
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.transformations)
+ .comparingElementsUsing(TRANSFORMATION_RANGE)
+ .containsExactly(
+ TransformationRange(start = 100 / 500f, end = 300 / 500f),
+ TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified),
+ TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f),
+ )
+ }
+
+ @Test
+ fun reversed() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ spec = tween(500)
+ reversed {
+ fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) }
+ timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) }
+ }
+ }
+ }
+
+ val transition = transitions.transitionSpecs.single()
+ assertThat(transition.transformations)
+ .comparingElementsUsing(TRANSFORMATION_RANGE)
+ .containsExactly(
+ TransformationRange(start = 1f - 0.8f, end = 1f - 0.1f),
+ TransformationRange(start = 1f - 300 / 500f, end = 1f - 100 / 500f),
+ )
+ }
+
+ @Test
+ fun defaultReversed() {
+ val transitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ spec = tween(500)
+ fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) }
+ timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) }
+ }
+ }
+
+ // Fetch the transition from B to A, which will automatically reverse the transition from A
+ // to B we defined.
+ val transition =
+ transitions.transitionSpec(from = TestScenes.SceneB, to = TestScenes.SceneA)
+ assertThat(transition.transformations)
+ .comparingElementsUsing(TRANSFORMATION_RANGE)
+ .containsExactly(
+ TransformationRange(start = 1f - 0.8f, end = 1f - 0.1f),
+ TransformationRange(start = 1f - 300 / 500f, end = 1f - 100 / 500f),
+ )
+ }
+
+ companion object {
+ private val TRANSFORMATION_RANGE =
+ Correspondence.transforming<Transformation, TransformationRange?>(
+ { it?.range },
+ "has range equal to"
+ )
+ }
+}