diff options
3 files changed, 120 insertions, 38 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index 5d1a7c5c840f..c89dd5b7d3ec 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -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,39 +268,58 @@ 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 fromValue = sceneValue(transition.fromScene) - val toValue = sceneValue(transition.toScene) - 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. - fromValue - } else { - // In the case of bouncing, if the value remains constant during the overscroll, - // we should use the value of the scene we are bouncing around. - if (!canOverflow && transition is TransitionState.HasOverscrollProperties) { - val bouncingScene = transition.bouncingScene - if (bouncingScene != null) { - return sceneValue(bouncingScene) - } - } + 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 progress = - if (canOverflow) transition.progress - else transition.progress.fastCoerceIn(0f, 1f) - lerp(fromValue, toValue, progress) + val fromValue = sceneValue(transition.fromScene) + val toValue = sceneValue(transition.toScene) + 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. + fromValue + } else { + // In the case of bouncing, if the value remains constant during the overscroll, + // we should use the value of the scene we are bouncing around. + if (!canOverflow && transition is TransitionState.HasOverscrollProperties) { + val bouncingScene = transition.bouncingScene + if (bouncingScene != null) { + return sceneValue(bouncingScene) } - } else fromValue ?: toValue + } + + val progress = + if (canOverflow) transition.progress else transition.progress.fastCoerceIn(0f, 1f) + lerp(fromValue, toValue, progress) + } + } 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) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index a5b6d2486168..44affd968513 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -457,7 +457,7 @@ internal abstract class BaseSceneTransitionLayoutState( */ internal fun startTransition( transition: TransitionState.Transition, - transitionKey: TransitionKey?, + transitionKey: TransitionKey? = null, chain: Boolean = true, ) { checkThread() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt index e8854cf0de60..6e8b208ea9e8 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt @@ -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) + } } |