diff options
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" + ) + } +} |