Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +24 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.annotation.FrequentlyChangingValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow Loading Loading @@ -56,6 +57,8 @@ import kotlin.math.roundToInt */ @Stable interface AnimatedState<T> : State<T> { @get:FrequentlyChangingValue override val value: T /** * Return a [State] that can be read during composition. * Loading Loading @@ -337,7 +340,14 @@ internal fun <T> animateSharedValueAsState( } return remember(layoutImpl, content, element, canOverflow) { AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow) AnimatedStateImpl<T, Any>( layoutImpl, content, element, key, canOverflow, fallbackValue = value, ) } } Loading @@ -358,13 +368,20 @@ private fun maybePruneMaps( } } private fun <T, Delta> sharedValueOrNull( layoutImpl: SceneTransitionLayoutImpl, key: ValueKey, element: ElementKey?, ): SharedValue<T, Delta>? { return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> } } private fun <T, Delta> sharedValue( layoutImpl: SceneTransitionLayoutImpl, key: ValueKey, element: ElementKey?, ): SharedValue<T, Delta> { return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> } ?: error(valueReadTooEarlyMessage(key)) return sharedValueOrNull(layoutImpl, key, element) ?: error(valueReadTooEarlyMessage(key)) } private fun valueReadTooEarlyMessage(key: ValueKey) = Loading Loading @@ -401,12 +418,14 @@ private class AnimatedStateImpl<T, Delta>( private val element: ElementKey?, private val key: ValueKey, private val canOverflow: Boolean, private val fallbackValue: T, // needed because of b/432799675. ) : AnimatedState<T> { override val value: T get() = value() private fun value(): T { val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element) val sharedValue = sharedValueOrNull<T, Delta>(layoutImpl, key, element) ?: return fallbackValue val transition = transition(sharedValue) val value: T = valueOrNull(sharedValue, transition) Loading @@ -414,7 +433,7 @@ private class AnimatedStateImpl<T, Delta>( // scene value, but we have to because code of removed nodes can still run if they // are placed with a graphics layer. ?: sharedValue[content] ?: error(valueReadTooEarlyMessage(key)) ?: return fallbackValue val interruptedValue = computeInterruptedValue(sharedValue, transition, value) sharedValue.lastValue = interruptedValue return interruptedValue Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +44 −12 Original line number Diff line number Diff line Loading @@ -26,10 +26,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.lerp import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Dp Loading @@ -49,7 +52,6 @@ import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -286,17 +288,6 @@ class AnimatedSharedAsStateTest { } } @Test fun readingAnimatedStateValueDuringCompositionThrows() { assertThrows(IllegalStateException::class.java) { rule.testTransition( fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value }, toSceneContent = {}, transition = {}, ) {} } } @Test fun readingAnimatedStateValueDuringCompositionIsStillPossible() { @Composable Loading Loading @@ -631,4 +622,45 @@ class AnimatedSharedAsStateTest { assertThat(targetValues[SceneE]?.value).isEqualTo(null) // not composed assertThat(targetValues[SceneF]?.value).isEqualTo(null) // not composed } @Test // Regression test for b/432799675. fun animatedSharedValueInGraphicsLayer() { val lastGraphicsLayerRotationPerContent = mutableMapOf<ContentKey, Float>() @Composable fun ContentScope.SharedFoo(rotation: Float, modifier: Modifier = Modifier) { val contentKey = this.contentKey ElementWithValues(TestElements.Foo, modifier) { val animatedRotation by animateElementFloatAsState(rotation, TestValues.Value1) Box( Modifier.graphicsLayer { rotationZ = animatedRotation.also { lastGraphicsLayerRotationPerContent[contentKey] = it } } ) } } rule.setContent { val scope = rememberCoroutineScope() val state = remember { MutableSceneTransitionLayoutStateForTests(SceneA).apply { startTransitionImmediately( scope, transition(SceneA, SceneB, progress = { 0.5f }), ) } } SceneTransitionLayout(state) { scene(SceneA) { SharedFoo(rotation = 0f, Modifier.fillMaxSize()) } scene(SceneB) { SharedFoo(rotation = 180f, Modifier.fillMaxSize(0.5f)) } } } // SharedFoo is only placed in sceneB, the scene with highest zIndex. assertThat(lastGraphicsLayerRotationPerContent[SceneB]).isWithin(0.1f).of(90f) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +24 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.annotation.FrequentlyChangingValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow Loading Loading @@ -56,6 +57,8 @@ import kotlin.math.roundToInt */ @Stable interface AnimatedState<T> : State<T> { @get:FrequentlyChangingValue override val value: T /** * Return a [State] that can be read during composition. * Loading Loading @@ -337,7 +340,14 @@ internal fun <T> animateSharedValueAsState( } return remember(layoutImpl, content, element, canOverflow) { AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow) AnimatedStateImpl<T, Any>( layoutImpl, content, element, key, canOverflow, fallbackValue = value, ) } } Loading @@ -358,13 +368,20 @@ private fun maybePruneMaps( } } private fun <T, Delta> sharedValueOrNull( layoutImpl: SceneTransitionLayoutImpl, key: ValueKey, element: ElementKey?, ): SharedValue<T, Delta>? { return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> } } private fun <T, Delta> sharedValue( layoutImpl: SceneTransitionLayoutImpl, key: ValueKey, element: ElementKey?, ): SharedValue<T, Delta> { return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> } ?: error(valueReadTooEarlyMessage(key)) return sharedValueOrNull(layoutImpl, key, element) ?: error(valueReadTooEarlyMessage(key)) } private fun valueReadTooEarlyMessage(key: ValueKey) = Loading Loading @@ -401,12 +418,14 @@ private class AnimatedStateImpl<T, Delta>( private val element: ElementKey?, private val key: ValueKey, private val canOverflow: Boolean, private val fallbackValue: T, // needed because of b/432799675. ) : AnimatedState<T> { override val value: T get() = value() private fun value(): T { val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element) val sharedValue = sharedValueOrNull<T, Delta>(layoutImpl, key, element) ?: return fallbackValue val transition = transition(sharedValue) val value: T = valueOrNull(sharedValue, transition) Loading @@ -414,7 +433,7 @@ private class AnimatedStateImpl<T, Delta>( // scene value, but we have to because code of removed nodes can still run if they // are placed with a graphics layer. ?: sharedValue[content] ?: error(valueReadTooEarlyMessage(key)) ?: return fallbackValue val interruptedValue = computeInterruptedValue(sharedValue, transition, value) sharedValue.lastValue = interruptedValue return interruptedValue Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +44 −12 Original line number Diff line number Diff line Loading @@ -26,10 +26,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.lerp import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Dp Loading @@ -49,7 +52,6 @@ import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -286,17 +288,6 @@ class AnimatedSharedAsStateTest { } } @Test fun readingAnimatedStateValueDuringCompositionThrows() { assertThrows(IllegalStateException::class.java) { rule.testTransition( fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value }, toSceneContent = {}, transition = {}, ) {} } } @Test fun readingAnimatedStateValueDuringCompositionIsStillPossible() { @Composable Loading Loading @@ -631,4 +622,45 @@ class AnimatedSharedAsStateTest { assertThat(targetValues[SceneE]?.value).isEqualTo(null) // not composed assertThat(targetValues[SceneF]?.value).isEqualTo(null) // not composed } @Test // Regression test for b/432799675. fun animatedSharedValueInGraphicsLayer() { val lastGraphicsLayerRotationPerContent = mutableMapOf<ContentKey, Float>() @Composable fun ContentScope.SharedFoo(rotation: Float, modifier: Modifier = Modifier) { val contentKey = this.contentKey ElementWithValues(TestElements.Foo, modifier) { val animatedRotation by animateElementFloatAsState(rotation, TestValues.Value1) Box( Modifier.graphicsLayer { rotationZ = animatedRotation.also { lastGraphicsLayerRotationPerContent[contentKey] = it } } ) } } rule.setContent { val scope = rememberCoroutineScope() val state = remember { MutableSceneTransitionLayoutStateForTests(SceneA).apply { startTransitionImmediately( scope, transition(SceneA, SceneB, progress = { 0.5f }), ) } } SceneTransitionLayout(state) { scene(SceneA) { SharedFoo(rotation = 0f, Modifier.fillMaxSize()) } scene(SceneB) { SharedFoo(rotation = 180f, Modifier.fillMaxSize(0.5f)) } } } // SharedFoo is only placed in sceneB, the scene with highest zIndex. assertThat(lastGraphicsLayerRotationPerContent[SceneB]).isWithin(0.1f).of(90f) } }