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

Commit 4747534b authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make animated values support multiple transitions

Bug: 290930950
Test: AnimatedSharedAsStateTest
Flag: com.android.systemui.scene_container
Change-Id: Id4e5ff33701f6f6e3dfdf6e175c759eebbb108f5
parent 183ee498
Loading
Loading
Loading
Loading
+51 −31
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.lerp

/**
@@ -267,15 +268,17 @@ private fun <T> valueOrNull(
    val sceneToValueMap = sceneToValueMap<T>(layoutImpl, key, element)
    fun sceneValue(scene: SceneKey): T? = sceneToValueMap[scene]

    return when (val transition = layoutImpl.state.transitionState) {
        is TransitionState.Idle -> sceneValue(transition.currentScene)
        is TransitionState.Transition -> {
            // Note: no need to check for transition ready here given that all target values are
            // defined during composition, we should already have the correct values to interpolate
            // between here.
    val transition =
        transition(layoutImpl, element, sceneToValueMap)
            ?: return sceneValue(layoutImpl.state.transitionState.currentScene)
                // TODO(b/311600838): Remove this. We should not have to fallback to the current
                // scene value, but we have to because code of removed nodes can still run if they
                // are placed with a graphics layer.
                ?: sceneValue(scene)

    val fromValue = sceneValue(transition.fromScene)
    val toValue = sceneValue(transition.toScene)
            if (fromValue != null && toValue != null) {
    return if (fromValue != null && toValue != null) {
        if (fromValue == toValue) {
            // Optimization: avoid reading progress if the values are the same, so we don't
            // relayout/redraw for nothing.
@@ -291,15 +294,32 @@ private fun <T> valueOrNull(
            }

            val progress =
                        if (canOverflow) transition.progress
                        else transition.progress.fastCoerceIn(0f, 1f)
                if (canOverflow) transition.progress else transition.progress.fastCoerceIn(0f, 1f)
            lerp(fromValue, toValue, progress)
        }
            } else fromValue ?: toValue
    } else
        fromValue
            ?: toValue
            // TODO(b/311600838): Remove this. We should not have to fallback to the current scene
            // value, but we have to because code of removed nodes can still run if they are placed
            // with a graphics layer.
            ?: sceneValue(scene)
}

private fun transition(
    layoutImpl: SceneTransitionLayoutImpl,
    element: ElementKey?,
    sceneToValueMap: Map<SceneKey, *>,
): TransitionState.Transition? {
    return if (element != null) {
        layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
            layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                transition.fromScene in sceneStates || transition.toScene in sceneStates
            }
        }
    } else {
        layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
            transition.fromScene in sceneToValueMap || transition.toScene in sceneToValueMap
        }
    }
    // TODO(b/311600838): Remove this. We should not have to fallback to the current scene value,
    // but we have to because code of removed nodes can still run if they are placed with a graphics
    // layer.
    ?: sceneValue(scene)
}
+1 −1
Original line number Diff line number Diff line
@@ -457,7 +457,7 @@ internal abstract class BaseSceneTransitionLayoutState(
     */
    internal fun startTransition(
        transition: TransitionState.Transition,
        transitionKey: TransitionKey?,
        transitionKey: TransitionKey? = null,
        chain: Boolean = true,
    ) {
        checkThread()
+68 −6
Original line number Diff line number Diff line
@@ -32,7 +32,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.util.lerp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.TestScenes.SceneD
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
@@ -130,8 +135,8 @@ class AnimatedSharedAsStateTest {
                // The transition lasts 64ms = 4 frames.
                spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
            },
            fromScene = TestScenes.SceneA,
            toScene = TestScenes.SceneB,
            fromScene = SceneA,
            toScene = SceneB,
        ) {
            before {
                assertThat(lastValueInFrom).isEqualTo(fromValues)
@@ -189,8 +194,8 @@ class AnimatedSharedAsStateTest {
                // The transition lasts 64ms = 4 frames.
                spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
            },
            fromScene = TestScenes.SceneA,
            toScene = TestScenes.SceneB,
            fromScene = SceneA,
            toScene = SceneB,
        ) {
            before {
                assertThat(lastValueInFrom).isEqualTo(fromValues)
@@ -243,8 +248,8 @@ class AnimatedSharedAsStateTest {
                // The transition lasts 64ms = 4 frames.
                spec = tween(durationMillis = 16 * 4, easing = LinearEasing)
            },
            fromScene = TestScenes.SceneA,
            toScene = TestScenes.SceneB,
            fromScene = SceneA,
            toScene = SceneB,
        ) {
            before {
                assertThat(lastValueInFrom).isEqualTo(fromValues)
@@ -381,4 +386,61 @@ class AnimatedSharedAsStateTest {
            }
        }
    }

    @Test
    fun animatedValueIsUsingLastTransition() = runTest {
        val state =
            rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA, transitions {}) }

        val foo = ValueKey("foo")
        val bar = ValueKey("bar")
        val lastValues = mutableMapOf<ValueKey, MutableMap<SceneKey, Float>>()

        @Composable
        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
            val animatedValue = animateSceneFloatAsState(value, key)
            LaunchedEffect(animatedValue) {
                snapshotFlow { animatedValue.value }
                    .collect { lastValues.getOrPut(key) { mutableMapOf() }[sceneKey] = it }
            }
        }

        rule.setContent {
            SceneTransitionLayout(state) {
                // foo goes from 0f to 100f in A => B.
                scene(SceneA) { animateFloat(0f, foo) }
                scene(SceneB) { animateFloat(100f, foo) }

                // bar goes from 0f to 10f in C => D.
                scene(SceneC) { animateFloat(0f, bar) }
                scene(SceneD) { animateFloat(10f, bar) }
            }
        }

        rule.runOnUiThread {
            // A => B is at 30%.
            state.startTransition(
                transition(
                    from = SceneA,
                    to = SceneB,
                    progress = { 0.3f },
                    onFinish = neverFinish(),
                )
            )

            // C => D is at 70%.
            state.startTransition(transition(from = SceneC, to = SceneD, progress = { 0.7f }))
        }
        rule.waitForIdle()

        assertThat(lastValues[foo]?.get(SceneA)).isWithin(0.001f).of(30f)
        assertThat(lastValues[foo]?.get(SceneB)).isWithin(0.001f).of(30f)
        assertThat(lastValues[foo]?.get(SceneC)).isNull()
        assertThat(lastValues[foo]?.get(SceneD)).isNull()

        assertThat(lastValues[bar]?.get(SceneA)).isNull()
        assertThat(lastValues[bar]?.get(SceneB)).isNull()
        assertThat(lastValues[bar]?.get(SceneC)).isWithin(0.001f).of(7f)
        assertThat(lastValues[bar]?.get(SceneD)).isWithin(0.001f).of(7f)
    }
}