Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 97789b74 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Add TransitionBuilder.reversed()

This CL adds TransitionBuilder.reversed() to be able to explicitly
reverse some transformations. This can be used to partially share the
transition definition of Foo => Bar when defining Bar => Foo. This will
be used to make it possible to disable shared element animation in the
Shade => Lockscreen transition while keeping it enabled in the
Lockscreen => Shade transition.

Bug: 300867076
Test: atest TransitionDslTest
Change-Id: Iaddfdf5eab0d1990d54ac6081948ab9b4c763f66
parent e0765bb1
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -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) }
    }

+7 −0
Original line number Diff line number Diff line
@@ -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
+21 −5
Original line number Diff line number Diff line
@@ -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<*>) {
        val transformation =
            if (range != null) {
            transformations.add(RangedPropertyTransformation(transformation, range!!))
                RangedPropertyTransformation(transformation, range!!)
            } else {
            transformations.add(transformation)
                transformation
            }

        transformations.add(
            if (reversed) {
                transformation.reverse()
            } else {
                transformation
            }
        )
    }

    override fun fade(matcher: ElementMatcher) {
+10 −10
Original line number Diff line number Diff line
@@ -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.
@@ -52,13 +60,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.
     */
@@ -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
    }
}
+190 −0
Original line number Diff line number Diff line
/*
 * 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"
            )
    }
}