Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +63 −21 Original line number Diff line number Diff line Loading @@ -17,12 +17,10 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffectResult import androidx.compose.runtime.DisposableEffectScope import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.unit.Dp Loading @@ -44,6 +42,20 @@ fun SceneScope.animateSharedIntAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Int value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedIntAsState( value: Int, debugName: String, canOverflow: Boolean = true, ): State<Int> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Float value. * Loading @@ -59,6 +71,20 @@ fun SceneScope.animateSharedFloatAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Float value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedFloatAsState( value: Float, debugName: String, canOverflow: Boolean = true, ): State<Float> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Dp value. * Loading @@ -74,6 +100,20 @@ fun SceneScope.animateSharedDpAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Dp value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedDpAsState( value: Dp, debugName: String, canOverflow: Boolean = true, ): State<Dp> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Color value. * Loading @@ -88,6 +128,19 @@ fun SceneScope.animateSharedColorAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false) } /** * Animate a shared Color value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedColorAsState( value: Color, debugName: String, ): State<Color> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow = false) } @Composable internal fun <T> animateSharedValueAsState( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -98,13 +151,15 @@ internal fun <T> animateSharedValueAsState( lerp: (T, T, Float) -> T, canOverflow: Boolean, ): State<T> { val sharedValue = remember(key) { Element.SharedValue(key, value) } if (value != sharedValue.value) { sharedValue.value = value val sharedValue = Snapshot.withoutReadObservation { element.sceneValues.getValue(scene.key).sharedValues.getOrPut(key) { Element.SharedValue(key, value) } as Element.SharedValue<T> } DisposableEffect(element, scene, sharedValue) { addSharedValueToElement(element, scene, sharedValue) if (value != sharedValue.value) { sharedValue.value = value } return remember(layoutImpl, element, sharedValue, lerp, canOverflow) { Loading @@ -112,19 +167,6 @@ internal fun <T> animateSharedValueAsState( } } private fun <T> DisposableEffectScope.addSharedValueToElement( element: Element, scene: Scene, sharedValue: Element.SharedValue<T>, ): DisposableEffectResult { val sceneValues = element.sceneValues[scene.key] ?: error("Element $element is not present in $scene") val sharedValues = sceneValues.sharedValues sharedValues[sharedValue.key] = sharedValue return onDispose { sharedValues.remove(sharedValue.key) } } private fun <T> computeValue( layoutImpl: SceneTransitionLayoutImpl, element: Element, Loading packages/SystemUI/compose/core/src/com/android/compose/animation/scene/MovableElement.kt +23 −3 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier Loading @@ -36,8 +37,6 @@ import androidx.compose.ui.unit.IntSize private const val TAG = "MovableElement" private object MovableElementScopeImpl : MovableElementScope @Composable internal fun MovableElement( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -51,6 +50,10 @@ internal fun MovableElement( // every time an element is added/removed from SceneTransitionLayoutImpl.elements, so we // disable read observation during the look-up in that map. val element = Snapshot.withoutReadObservation { layoutImpl.elements.getValue(key) } val movableElementScope = remember(layoutImpl, element, scene) { MovableElementScopeImpl(layoutImpl, element, scene) } // The [Picture] to which we save the last drawing commands of this element. This is // necessary because the content of this element might not be composed in this scene, in Loading @@ -77,7 +80,7 @@ internal fun MovableElement( } } ) { element.movableContent { MovableElementScopeImpl.content() } element.movableContent { movableElementScope.content() } } } else { // If we are not composed, we draw the previous drawing commands at the same size as the Loading Loading @@ -178,3 +181,20 @@ private fun shouldComposeMovableElement( isHighestScene } } private class MovableElementScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val element: Element, private val scene: Scene, ) : MovableElementScope { @Composable override fun <T> animateSharedValueAsState( value: T, debugName: String, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, ): State<T> { val key = remember { ValueKey(debugName) } return animateSharedValueAsState(layoutImpl, scene, element, key, value, lerp, canOverflow) } } packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +10 −1 Original line number Diff line number Diff line Loading @@ -160,7 +160,16 @@ interface SceneScope { // TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey // arguments to allow sharing values inside a movable element. @ElementDsl interface MovableElementScope @ElementDsl interface MovableElementScope { @Composable fun <T> animateSharedValueAsState( value: T, debugName: String, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, ): State<T> } /** An action performed by the user. */ sealed interface UserAction Loading packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt 0 → 100644 +218 −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.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.ui.util.lerp import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AnimatedSharedAsStateTest { @get:Rule val rule = createComposeRule() private data class Values( val int: Int, val float: Float, val dp: Dp, val color: Color, ) private fun lerp(start: Values, stop: Values, fraction: Float): Values { return Values( int = lerp(start.int, stop.int, fraction), float = lerp(start.float, stop.float, fraction), dp = lerp(start.dp, stop.dp, fraction), color = lerp(start.color, stop.color, fraction), ) } @Composable private fun SceneScope.Foo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { val key = TestElements.Foo Box(Modifier.element(key)) { val int by animateSharedIntAsState(targetValues.int, TestValues.Value1, key) val float by animateSharedFloatAsState(targetValues.float, TestValues.Value2, key) val dp by animateSharedDpAsState(targetValues.dp, TestValues.Value3, key) val color by animateSharedColorAsState(targetValues.color, TestValues.Value4, key) // Make sure we read the values during composition, so that we recompose and call // onCurrentValueChanged() with the latest values. val currentValues = Values(int, float, dp, color) SideEffect { onCurrentValueChanged(currentValues) } } } @Composable private fun SceneScope.MovableFoo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { val key = TestElements.Foo MovableElement(key = key, Modifier) { val int by animateSharedIntAsState(targetValues.int, debugName = TestValues.Value1.debugName) val float by animateSharedFloatAsState( targetValues.float, debugName = TestValues.Value2.debugName ) val dp by animateSharedDpAsState(targetValues.dp, debugName = TestValues.Value3.debugName) val color by animateSharedColorAsState( targetValues.color, debugName = TestValues.Value4.debugName ) // Make sure we read the values during composition, so that we recompose and call // onCurrentValueChanged() with the latest values. val currentValues = Values(int, float, dp, color) SideEffect { onCurrentValueChanged(currentValues) } } } @Test fun animateSharedValues() { val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) var lastValueInFrom = fromValues var lastValueInTo = toValues rule.testTransition( fromSceneContent = { Foo(targetValues = fromValues, onCurrentValueChanged = { lastValueInFrom = it }) }, toSceneContent = { Foo(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it }) }, transition = { // The transition lasts 64ms = 4 frames. spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { before { assertThat(lastValueInFrom).isEqualTo(fromValues) // to was not composed yet, so lastValueInTo was not set yet. assertThat(lastValueInTo).isEqualTo(toValues) } at(16) { // Given that we use Modifier.element() here, animateSharedXAsState is composed in // both scenes and values should be interpolated with the transition fraction. val expectedValues = lerp(fromValues, toValues, fraction = 0.25f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } at(32) { val expectedValues = lerp(fromValues, toValues, fraction = 0.5f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } at(48) { val expectedValues = lerp(fromValues, toValues, fraction = 0.75f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } after { assertThat(lastValueInFrom).isEqualTo(toValues) assertThat(lastValueInTo).isEqualTo(toValues) } } } @Test fun movableAnimateSharedValues() { val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) var lastValueInFrom = fromValues var lastValueInTo = toValues rule.testTransition( fromSceneContent = { MovableFoo( targetValues = fromValues, onCurrentValueChanged = { lastValueInFrom = it } ) }, toSceneContent = { MovableFoo(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it }) }, transition = { // The transition lasts 64ms = 4 frames. spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { before { assertThat(lastValueInFrom).isEqualTo(fromValues) // to was not composed yet, so lastValueInTo was not set yet. assertThat(lastValueInTo).isEqualTo(toValues) } at(16) { // Given that we use MovableElement here, animateSharedXAsState is composed only // once, in the highest scene (in this case, in toScene). assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f)) } at(32) { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f)) } at(48) { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) } after { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(toValues) } } } } packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt +3 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,9 @@ object TestElements { /** Value keys that can be reused by tests. */ object TestValues { val Value1 = ValueKey("Value1") val Value2 = ValueKey("Value2") val Value3 = ValueKey("Value3") val Value4 = ValueKey("Value4") } // We use a transition duration of 480ms here because it is a multiple of 16, the time of a frame in Loading Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +63 −21 Original line number Diff line number Diff line Loading @@ -17,12 +17,10 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffectResult import androidx.compose.runtime.DisposableEffectScope import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.unit.Dp Loading @@ -44,6 +42,20 @@ fun SceneScope.animateSharedIntAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Int value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedIntAsState( value: Int, debugName: String, canOverflow: Boolean = true, ): State<Int> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Float value. * Loading @@ -59,6 +71,20 @@ fun SceneScope.animateSharedFloatAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Float value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedFloatAsState( value: Float, debugName: String, canOverflow: Boolean = true, ): State<Float> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Dp value. * Loading @@ -74,6 +100,20 @@ fun SceneScope.animateSharedDpAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow) } /** * Animate a shared Dp value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedDpAsState( value: Dp, debugName: String, canOverflow: Boolean = true, ): State<Dp> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow) } /** * Animate a shared Color value. * Loading @@ -88,6 +128,19 @@ fun SceneScope.animateSharedColorAsState( return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false) } /** * Animate a shared Color value. * * @see MovableElementScope.animateSharedValueAsState */ @Composable fun MovableElementScope.animateSharedColorAsState( value: Color, debugName: String, ): State<Color> { return animateSharedValueAsState(value, debugName, ::lerp, canOverflow = false) } @Composable internal fun <T> animateSharedValueAsState( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -98,13 +151,15 @@ internal fun <T> animateSharedValueAsState( lerp: (T, T, Float) -> T, canOverflow: Boolean, ): State<T> { val sharedValue = remember(key) { Element.SharedValue(key, value) } if (value != sharedValue.value) { sharedValue.value = value val sharedValue = Snapshot.withoutReadObservation { element.sceneValues.getValue(scene.key).sharedValues.getOrPut(key) { Element.SharedValue(key, value) } as Element.SharedValue<T> } DisposableEffect(element, scene, sharedValue) { addSharedValueToElement(element, scene, sharedValue) if (value != sharedValue.value) { sharedValue.value = value } return remember(layoutImpl, element, sharedValue, lerp, canOverflow) { Loading @@ -112,19 +167,6 @@ internal fun <T> animateSharedValueAsState( } } private fun <T> DisposableEffectScope.addSharedValueToElement( element: Element, scene: Scene, sharedValue: Element.SharedValue<T>, ): DisposableEffectResult { val sceneValues = element.sceneValues[scene.key] ?: error("Element $element is not present in $scene") val sharedValues = sceneValues.sharedValues sharedValues[sharedValue.key] = sharedValue return onDispose { sharedValues.remove(sharedValue.key) } } private fun <T> computeValue( layoutImpl: SceneTransitionLayoutImpl, element: Element, Loading
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/MovableElement.kt +23 −3 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier Loading @@ -36,8 +37,6 @@ import androidx.compose.ui.unit.IntSize private const val TAG = "MovableElement" private object MovableElementScopeImpl : MovableElementScope @Composable internal fun MovableElement( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -51,6 +50,10 @@ internal fun MovableElement( // every time an element is added/removed from SceneTransitionLayoutImpl.elements, so we // disable read observation during the look-up in that map. val element = Snapshot.withoutReadObservation { layoutImpl.elements.getValue(key) } val movableElementScope = remember(layoutImpl, element, scene) { MovableElementScopeImpl(layoutImpl, element, scene) } // The [Picture] to which we save the last drawing commands of this element. This is // necessary because the content of this element might not be composed in this scene, in Loading @@ -77,7 +80,7 @@ internal fun MovableElement( } } ) { element.movableContent { MovableElementScopeImpl.content() } element.movableContent { movableElementScope.content() } } } else { // If we are not composed, we draw the previous drawing commands at the same size as the Loading Loading @@ -178,3 +181,20 @@ private fun shouldComposeMovableElement( isHighestScene } } private class MovableElementScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val element: Element, private val scene: Scene, ) : MovableElementScope { @Composable override fun <T> animateSharedValueAsState( value: T, debugName: String, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, ): State<T> { val key = remember { ValueKey(debugName) } return animateSharedValueAsState(layoutImpl, scene, element, key, value, lerp, canOverflow) } }
packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +10 −1 Original line number Diff line number Diff line Loading @@ -160,7 +160,16 @@ interface SceneScope { // TODO(b/291053742): Add animateSharedValueAsState(targetValue) without any ValueKey and ElementKey // arguments to allow sharing values inside a movable element. @ElementDsl interface MovableElementScope @ElementDsl interface MovableElementScope { @Composable fun <T> animateSharedValueAsState( value: T, debugName: String, lerp: (start: T, stop: T, fraction: Float) -> T, canOverflow: Boolean, ): State<T> } /** An action performed by the user. */ sealed interface UserAction Loading
packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt 0 → 100644 +218 −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.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.ui.util.lerp import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AnimatedSharedAsStateTest { @get:Rule val rule = createComposeRule() private data class Values( val int: Int, val float: Float, val dp: Dp, val color: Color, ) private fun lerp(start: Values, stop: Values, fraction: Float): Values { return Values( int = lerp(start.int, stop.int, fraction), float = lerp(start.float, stop.float, fraction), dp = lerp(start.dp, stop.dp, fraction), color = lerp(start.color, stop.color, fraction), ) } @Composable private fun SceneScope.Foo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { val key = TestElements.Foo Box(Modifier.element(key)) { val int by animateSharedIntAsState(targetValues.int, TestValues.Value1, key) val float by animateSharedFloatAsState(targetValues.float, TestValues.Value2, key) val dp by animateSharedDpAsState(targetValues.dp, TestValues.Value3, key) val color by animateSharedColorAsState(targetValues.color, TestValues.Value4, key) // Make sure we read the values during composition, so that we recompose and call // onCurrentValueChanged() with the latest values. val currentValues = Values(int, float, dp, color) SideEffect { onCurrentValueChanged(currentValues) } } } @Composable private fun SceneScope.MovableFoo( targetValues: Values, onCurrentValueChanged: (Values) -> Unit, ) { val key = TestElements.Foo MovableElement(key = key, Modifier) { val int by animateSharedIntAsState(targetValues.int, debugName = TestValues.Value1.debugName) val float by animateSharedFloatAsState( targetValues.float, debugName = TestValues.Value2.debugName ) val dp by animateSharedDpAsState(targetValues.dp, debugName = TestValues.Value3.debugName) val color by animateSharedColorAsState( targetValues.color, debugName = TestValues.Value4.debugName ) // Make sure we read the values during composition, so that we recompose and call // onCurrentValueChanged() with the latest values. val currentValues = Values(int, float, dp, color) SideEffect { onCurrentValueChanged(currentValues) } } } @Test fun animateSharedValues() { val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) var lastValueInFrom = fromValues var lastValueInTo = toValues rule.testTransition( fromSceneContent = { Foo(targetValues = fromValues, onCurrentValueChanged = { lastValueInFrom = it }) }, toSceneContent = { Foo(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it }) }, transition = { // The transition lasts 64ms = 4 frames. spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { before { assertThat(lastValueInFrom).isEqualTo(fromValues) // to was not composed yet, so lastValueInTo was not set yet. assertThat(lastValueInTo).isEqualTo(toValues) } at(16) { // Given that we use Modifier.element() here, animateSharedXAsState is composed in // both scenes and values should be interpolated with the transition fraction. val expectedValues = lerp(fromValues, toValues, fraction = 0.25f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } at(32) { val expectedValues = lerp(fromValues, toValues, fraction = 0.5f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } at(48) { val expectedValues = lerp(fromValues, toValues, fraction = 0.75f) assertThat(lastValueInFrom).isEqualTo(expectedValues) assertThat(lastValueInTo).isEqualTo(expectedValues) } after { assertThat(lastValueInFrom).isEqualTo(toValues) assertThat(lastValueInTo).isEqualTo(toValues) } } } @Test fun movableAnimateSharedValues() { val fromValues = Values(int = 0, float = 0f, dp = 0.dp, color = Color.Red) val toValues = Values(int = 100, float = 100f, dp = 100.dp, color = Color.Blue) var lastValueInFrom = fromValues var lastValueInTo = toValues rule.testTransition( fromSceneContent = { MovableFoo( targetValues = fromValues, onCurrentValueChanged = { lastValueInFrom = it } ) }, toSceneContent = { MovableFoo(targetValues = toValues, onCurrentValueChanged = { lastValueInTo = it }) }, transition = { // The transition lasts 64ms = 4 frames. spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { before { assertThat(lastValueInFrom).isEqualTo(fromValues) // to was not composed yet, so lastValueInTo was not set yet. assertThat(lastValueInTo).isEqualTo(toValues) } at(16) { // Given that we use MovableElement here, animateSharedXAsState is composed only // once, in the highest scene (in this case, in toScene). assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.25f)) } at(32) { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.5f)) } at(48) { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) } after { assertThat(lastValueInFrom).isEqualTo(fromValues) assertThat(lastValueInTo).isEqualTo(toValues) } } } }
packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt +3 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,9 @@ object TestElements { /** Value keys that can be reused by tests. */ object TestValues { val Value1 = ValueKey("Value1") val Value2 = ValueKey("Value2") val Value3 = ValueKey("Value3") val Value4 = ValueKey("Value4") } // We use a transition duration of 480ms here because it is a multiple of 16, the time of a frame in Loading