diff options
10 files changed, 144 insertions, 298 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index b8f9ca82f072..f655ac1d207b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -83,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.PlatformButton import com.android.compose.animation.Easings import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout @@ -516,13 +517,22 @@ private fun FoldAware( val currentSceneKey = if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey - SceneTransitionLayout( - currentScene = currentSceneKey, - onChangeScene = {}, - transitions = SceneTransitions, - modifier = modifier, - enableInterruptions = false, - ) { + val state = remember { + MutableSceneTransitionLayoutState( + currentSceneKey, + SceneTransitions, + enableInterruptions = false, + ) + } + + // Update state whenever currentSceneKey has changed. + LaunchedEffect(state, currentSceneKey) { + if (currentSceneKey != state.transitionState.currentScene) { + state.setTargetScene(currentSceneKey, coroutineScope = this) + } + } + + SceneTransitionLayout(state, modifier = modifier) { scene(SceneKeys.ContiguousSceneKey) { FoldableScene( aboveFold = aboveFold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 067315381773..0cd4b6816a61 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -33,6 +34,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.modifiers.thenIf @@ -78,13 +80,22 @@ constructor( WeatherClockScenes.splitShadeLargeClockScene } - SceneTransitionLayout( - modifier = modifier, - currentScene = currentScene, - onChangeScene = {}, - transitions = ClockTransition.defaultClockTransitions, - enableInterruptions = false, - ) { + val state = remember { + MutableSceneTransitionLayoutState( + currentScene, + ClockTransition.defaultClockTransitions, + enableInterruptions = false, + ) + } + + // Update state whenever currentSceneKey has changed. + LaunchedEffect(state, currentScene) { + if (currentScene != state.transitionState.currentScene) { + state.setTargetScene(currentScene, coroutineScope = this) + } + } + + SceneTransitionLayout(state, modifier) { scene(splitShadeLargeClockScene) { LargeClockWithSmartSpace( shouldOffSetClockToOneHalf = !hasCustomPositionUpdatedAnimation diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 7c8fce8f297d..45758c53d69a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.unit.IntSize * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. - * @see updateSceneTransitionLayoutState */ @Composable fun SceneTransitionLayout( @@ -70,56 +69,6 @@ fun SceneTransitionLayout( ) } -/** - * [SceneTransitionLayout] is a container that automatically animates its content whenever - * [currentScene] changes, using the transitions defined in [transitions]. - * - * Note: You should use [androidx.compose.animation.AnimatedContent] instead of - * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if - * you need support for swipe gestures, shared elements or transitions defined declaratively outside - * UI code. - * - * @param currentScene the current scene - * @param onChangeScene a mutator that should set [currentScene] to the given scene when called. - * This is called when the user commits a transition to a new scene because of a [UserAction], for - * instance by triggering back navigation or by swiping to a new scene. - * @param transitions the definition of the transitions used to animate a change of scene. - * @param swipeSourceDetector the source detector used to detect which source a swipe is started - * from, if any. - * @param transitionInterceptionThreshold used during a scene transition. For the scene to be - * intercepted, the progress value must be above the threshold, and below (1 - threshold). - * @param scenes the configuration of the different scenes of this layout. - */ -@Composable -fun SceneTransitionLayout( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - transitions: SceneTransitions, - modifier: Modifier = Modifier, - swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, - swipeDetector: SwipeDetector = DefaultSwipeDetector, - @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, - scenes: SceneTransitionLayoutScope.() -> Unit, -) { - val state = - updateSceneTransitionLayoutState( - currentScene, - onChangeScene, - transitions, - enableInterruptions = enableInterruptions, - ) - - SceneTransitionLayout( - state, - modifier, - swipeSourceDetector, - swipeDetector, - transitionInterceptionThreshold, - scenes, - ) -} - interface SceneTransitionLayoutScope { /** * Add a scene to this layout, identified by [key]. 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 5b4fbf036083..56c8752eb53c 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 @@ -22,13 +22,9 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastFilter @@ -38,14 +34,12 @@ import com.android.compose.animation.scene.transition.link.StateLink import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch /** * The state of a [SceneTransitionLayout]. * * @see MutableSceneTransitionLayoutState - * @see updateSceneTransitionLayoutState */ @Stable sealed interface SceneTransitionLayoutState { @@ -152,55 +146,6 @@ fun MutableSceneTransitionLayoutState( ) } -/** - * Sets up a [SceneTransitionLayoutState] and keeps it synced with [currentScene], [onChangeScene] - * and [transitions]. New transitions will automatically be started whenever [currentScene] is - * changed. - * - * @param currentScene the current scene - * @param onChangeScene a mutator that should set [currentScene] to the given scene when called. - * This is called when the user commits a transition to a new scene because of a [UserAction], for - * instance by triggering back navigation or by swiping to a new scene. - * @param transitions the definition of the transitions used to animate a change of scene. - * @param canChangeScene whether we can transition to the given scene. This is called when the user - * commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns - * `true`, then [onChangeScene] will be called right afterwards with the same [SceneKey]. If it - * returns `false`, the user action will be cancelled and we will animate back to the current - * scene. - * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other - * [SceneTransitionLayoutState]s. - */ -@Composable -fun updateSceneTransitionLayoutState( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - transitions: SceneTransitions = SceneTransitions.Empty, - canChangeScene: (SceneKey) -> Boolean = { true }, - stateLinks: List<StateLink> = emptyList(), - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, -): SceneTransitionLayoutState { - return remember { - HoistedSceneTransitionLayoutState( - currentScene, - transitions, - onChangeScene, - canChangeScene, - stateLinks, - enableInterruptions, - ) - } - .apply { - update( - currentScene, - onChangeScene, - canChangeScene, - transitions, - stateLinks, - enableInterruptions, - ) - } -} - @Stable sealed interface TransitionState { /** @@ -729,58 +674,6 @@ internal abstract class BaseSceneTransitionLayoutState( } } -/** - * A [SceneTransitionLayout] whose current scene/source of truth is hoisted (its current value comes - * from outside). - */ -internal class HoistedSceneTransitionLayoutState( - initialScene: SceneKey, - override var transitions: SceneTransitions, - private var changeScene: (SceneKey) -> Unit, - private var canChangeScene: (SceneKey) -> Boolean, - stateLinks: List<StateLink> = emptyList(), - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, -) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) { - private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED) - - override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene) - - override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene.invoke(scene) - - @Composable - fun update( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - canChangeScene: (SceneKey) -> Boolean, - transitions: SceneTransitions, - stateLinks: List<StateLink>, - enableInterruptions: Boolean, - ) { - SideEffect { - this.changeScene = onChangeScene - this.canChangeScene = canChangeScene - this.transitions = transitions - this.stateLinks = stateLinks - this.enableInterruptions = enableInterruptions - - targetSceneChannel.trySend(currentScene) - } - - LaunchedEffect(targetSceneChannel) { - for (newKey in targetSceneChannel) { - // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame - // late. - val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey - animateToScene( - layoutState = this@HoistedSceneTransitionLayoutState, - target = newKey, - transitionKey = null, - ) - } - } - } -} - /** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */ internal class MutableSceneTransitionLayoutStateImpl( initialScene: SceneKey, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 2de6faaccca2..76d488818949 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -203,26 +203,28 @@ class ElementTest { val elementSize = 50.dp val elementOffset = 20.dp - lateinit var changeScene: (SceneKey) -> Unit - - rule.testTransition( - from = SceneA, - to = SceneB, - transitionLayout = { currentScene, onChangeScene -> - changeScene = onChangeScene - - SceneTransitionLayout( - currentScene, - onChangeScene, + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, transitions { from(SceneA, to = SceneB) { spec = tween } from(SceneB, to = SceneC) { spec = tween } }, - // Disable interruptions so that the current transition is directly removed when - // starting a new one. + // Disable interruptions so that the current transition is directly removed + // when starting a new one. enableInterruptions = false, - ) { + ) + } + + lateinit var coroutineScope: CoroutineScope + rule.testTransition( + state = state, + to = SceneB, + transitionLayout = { state -> + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.size(layoutSize)) { // Transformed element @@ -243,7 +245,7 @@ class ElementTest { onElement(TestElements.Bar).assertExists() // Start transition from SceneB to SceneC - changeScene(SceneC) + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } } at(3 * frameDuration) { onElement(TestElements.Bar).assertIsNotDisplayed() } @@ -340,18 +342,16 @@ class ElementTest { @Test fun elementIsReusedBetweenScenes() { - var currentScene by mutableStateOf(SceneA) + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var sceneCState by mutableStateOf(0) val key = TestElements.Foo var nullableLayoutImpl: SceneTransitionLayoutImpl? = null + lateinit var coroutineScope: CoroutineScope rule.setContent { + coroutineScope = rememberCoroutineScope() SceneTransitionLayoutForTesting( - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it } - ), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { /* Nothing */ } @@ -375,7 +375,7 @@ class ElementTest { assertThat(layoutImpl.elements).isEmpty() // Scene B: element is in the map. - currentScene = SceneB + rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) } rule.waitForIdle() assertThat(layoutImpl.elements.keys).containsExactly(key) @@ -383,7 +383,7 @@ class ElementTest { assertThat(element.sceneStates.keys).containsExactly(SceneB) // Scene C, state 0: the same element is reused. - currentScene = SceneC + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } sceneCState = 0 rule.waitForIdle() @@ -472,12 +472,13 @@ class ElementTest { @Test fun elementModifierSupportsUpdates() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } var key by mutableStateOf(TestElements.Foo) var nullableLayoutImpl: SceneTransitionLayoutImpl? = null rule.setContent { SceneTransitionLayoutForTesting( - state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { Box(Modifier.element(key)) } @@ -521,11 +522,12 @@ class ElementTest { rule.waitUntil(timeoutMillis = 10_000) { animationFinished } } + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } rule.setContent { scrollScope = rememberCoroutineScope() SceneTransitionLayoutForTesting( - state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}), + state = state, onLayoutImpl = { nullableLayoutImpl = it }, ) { scene(SceneA) { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt index 55431354b693..f717301dba38 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt @@ -40,7 +40,13 @@ class ObservableTransitionStateTest { @Test fun testObservableTransitionState() = runTest { - lateinit var state: SceneTransitionLayoutState + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + EmptyTestTransitions, + ) + } // Collect the current observable state into [observableState]. // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be @@ -63,16 +69,9 @@ class ObservableTransitionStateTest { } rule.testTransition( - from = SceneA, + state = state, to = SceneB, - transitionLayout = { currentScene, onChangeScene -> - state = - updateSceneTransitionLayoutState( - currentScene, - onChangeScene, - EmptyTestTransitions - ) - + transitionLayout = { SceneTransitionLayout(state = state) { scene(SceneA) {} scene(SceneB) {} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 1c8efb82fd24..422306b94845 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -31,6 +31,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.ui.Alignment import androidx.compose.ui.Modifier @@ -58,6 +60,7 @@ import com.android.compose.test.assertSizeIsEqualTo import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -69,8 +72,13 @@ class SceneTransitionLayoutTest { private val LayoutSize = 300.dp } - private var currentScene by mutableStateOf(SceneA) - private lateinit var layoutState: SceneTransitionLayoutState + private lateinit var coroutineScope: CoroutineScope + private lateinit var layoutState: MutableSceneTransitionLayoutState + private var currentScene: SceneKey + get() = layoutState.transitionState.currentScene + set(value) { + rule.runOnUiThread { layoutState.setTargetScene(value, coroutineScope) } + } // We use createAndroidComposeRule() here and not createComposeRule() because we need an // activity for testBack(). @@ -79,13 +87,14 @@ class SceneTransitionLayoutTest { /** The content under test. */ @Composable private fun TestContent(enableInterruptions: Boolean = true) { - layoutState = - updateSceneTransitionLayoutState( - currentScene, - { currentScene = it }, + coroutineScope = rememberCoroutineScope() + layoutState = remember { + MutableSceneTransitionLayoutState( + SceneA, EmptyTestTransitions, enableInterruptions = enableInterruptions, ) + } SceneTransitionLayout( state = layoutState, @@ -218,23 +227,15 @@ class SceneTransitionLayoutTest { // We will advance the clock manually. rule.mainClock.autoAdvance = false - // Change the current scene. Until composition is triggered, this won't change the layout - // state. + // Change the current scene. currentScene = SceneB - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - - // On the next frame, we will recompose because currentScene changed, which will start the - // transition (i.e. it will change the transitionState to be a Transition) in a - // LaunchedEffect. - rule.mainClock.advanceTimeByFrame() val transition = assertThat(layoutState.transitionState).isTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(transition).hasProgress(0f) // Then, on the next frame, the animator we started gets its initial value and clock - // starting time. We are now at progress = 0f. + // starting time. We are still at progress = 0f. rule.mainClock.advanceTimeByFrame() assertThat(transition).hasProgress(0f) @@ -275,12 +276,9 @@ class SceneTransitionLayoutTest { // Pause animations to test the state mid-transition. rule.mainClock.autoAdvance = false - // Go to scene B and let the animation start. See [testLayoutState()] and - // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock - // by 2 frames to be at the start of the animation. + // Go to scene B and let the animation start. currentScene = SceneB rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() // Advance to the middle of the animation. rule.mainClock.advanceTimeBy(TestTransitionDuration / 2) @@ -311,7 +309,6 @@ class SceneTransitionLayoutTest { // Animate to scene C, let the animation start then go to the middle of the transition. currentScene = SceneC rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeBy(TestTransitionDuration / 2) // In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The @@ -409,24 +406,24 @@ class SceneTransitionLayoutTest { fun multipleTransitionsWillComposeMultipleScenes() { val duration = 10 * 16L - var currentScene: SceneKey by mutableStateOf(SceneA) - lateinit var state: SceneTransitionLayoutState - rule.setContent { - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it }, - transitions = - transitions { - from(SceneA, to = SceneB) { - spec = tween(duration.toInt(), easing = LinearEasing) - } - from(SceneB, to = SceneC) { - spec = tween(duration.toInt(), easing = LinearEasing) - } + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + transitions { + from(SceneA, to = SceneB) { + spec = tween(duration.toInt(), easing = LinearEasing) + } + from(SceneB, to = SceneC) { + spec = tween(duration.toInt(), easing = LinearEasing) } + } ) + } + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.testTag("aRoot").fillMaxSize()) } scene(SceneB) { Box(Modifier.testTag("bRoot").fillMaxSize()) } @@ -444,12 +441,11 @@ class SceneTransitionLayoutTest { rule.mainClock.autoAdvance = false // Start A => B and go to the middle of the transition. - currentScene = SceneB + rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) } - // We need to tick 2 frames after changing [currentScene] before the animation actually + // We need to tick 1 frames after changing [currentScene] before the animation actually // starts. rule.mainClock.advanceTimeByFrame() - rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeBy(duration / 2) rule.waitForIdle() @@ -462,8 +458,7 @@ class SceneTransitionLayoutTest { rule.onNodeWithTag("cRoot").assertDoesNotExist() // Start B => C. - currentScene = SceneC - rule.mainClock.advanceTimeByFrame() + rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) } rule.mainClock.advanceTimeByFrame() rule.waitForIdle() @@ -517,12 +512,7 @@ class SceneTransitionLayoutTest { assertThrows(IllegalStateException::class.java) { rule.setContent { SceneTransitionLayout( - state = - updateSceneTransitionLayoutState( - currentScene = currentScene, - onChangeScene = { currentScene = it }, - transitions = EmptyTestTransitions - ), + state = remember { MutableSceneTransitionLayoutState(SceneA) }, modifier = Modifier.size(LayoutSize), ) { // from SceneA to SceneA diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt index de46f7209c84..fbd557f3cbb3 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt @@ -27,12 +27,6 @@ fun TestSceneScope( content: @Composable SceneScope.() -> Unit, ) { val currentScene = remember { SceneKey("current") } - SceneTransitionLayout( - currentScene, - onChangeScene = { /* do nothing */}, - transitions = remember { transitions {} }, - modifier, - ) { - scene(currentScene, content = content) - } + val state = remember { MutableSceneTransitionLayoutState(currentScene) } + SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index 6724851dbec5..a37d78ef8a71 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -19,13 +19,14 @@ package com.android.compose.animation.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.junit4.ComposeContentTestRule +import kotlinx.coroutines.CoroutineScope import platform.test.motion.MotionTestRule import platform.test.motion.RecordedMotion import platform.test.motion.compose.ComposeRecordingSpec @@ -95,20 +96,24 @@ fun ComposeContentTestRule.testTransition( builder: TransitionTestBuilder.() -> Unit, ) { testTransition( - from = fromScene, + state = + runOnUiThread { + MutableSceneTransitionLayoutState( + fromScene, + transitions { from(fromScene, to = toScene, builder = transition) } + ) + }, to = toScene, - transitionLayout = { currentScene, onChangeScene -> + transitionLayout = { state -> SceneTransitionLayout( - currentScene, - onChangeScene, - transitions { from(fromScene, to = toScene, builder = transition) }, + state, layoutModifier, ) { scene(fromScene, content = fromSceneContent) scene(toScene, content = toSceneContent) } }, - builder, + builder = builder, ) } @@ -172,21 +177,19 @@ fun MotionTestRule<ComposeToolkit>.recordTransition( ) } -/** - * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different - * points in time. - */ +/** Test the transition from [state] to [to]. */ fun ComposeContentTestRule.testTransition( - from: SceneKey, + state: MutableSceneTransitionLayoutState, to: SceneKey, - transitionLayout: - @Composable - ( - currentScene: SceneKey, - onChangeScene: (SceneKey) -> Unit, - ) -> Unit, + transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit, builder: TransitionTestBuilder.() -> Unit, ) { + val currentScene = state.transitionState.currentScene + check(currentScene != to) { + "The 'to' scene (${to.debugName}) should be different from the state current scene " + + "(${currentScene.debugName})" + } + val test = transitionTest(builder) val assertionScope = object : TransitionTestAssertionScope { @@ -198,8 +201,11 @@ fun ComposeContentTestRule.testTransition( } } - var currentScene by mutableStateOf(from) - setContent { transitionLayout(currentScene, { currentScene = it }) } + lateinit var coroutineScope: CoroutineScope + setContent { + coroutineScope = rememberCoroutineScope() + transitionLayout(state) + } // Wait for the UI to be idle then test the before state. waitForIdle() @@ -209,14 +215,8 @@ fun ComposeContentTestRule.testTransition( mainClock.autoAdvance = false // Change the current scene. - currentScene = to - - // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will - // change the transitionState to be a Transition) in a LaunchedEffect. - mainClock.advanceTimeByFrame() - - // Advance by another frame so that the animator we started gets its initial value and clock - // starting time. We are now at progress = 0f. + runOnUiThread { state.setTargetScene(to, coroutineScope) } + waitForIdle() mainClock.advanceTimeByFrame() waitForIdle() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 608e25a82eff..c1bc0b22fed6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -21,6 +21,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.constraintlayout.widget.ConstraintSet @@ -29,9 +30,9 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayout -import com.android.compose.animation.scene.transitions import com.android.internal.jank.InteractionJankMonitor import com.android.keyguard.KeyguardStatusView import com.android.keyguard.KeyguardStatusViewController @@ -112,7 +113,6 @@ constructor( private var rootViewHandle: DisposableHandle? = null private var indicationAreaHandle: DisposableHandle? = null - private val sceneKey = SceneKey("root-view-scene-key") var keyguardStatusViewController: KeyguardStatusViewController? = null get() { @@ -229,12 +229,10 @@ constructor( setContent { // STL is used solely to provide a SceneScope to enable us to invoke SceneScope // composables. - SceneTransitionLayout( - currentScene = sceneKey, - onChangeScene = {}, - transitions = transitions {}, - ) { - scene(sceneKey) { + val currentScene = remember { SceneKey("root-view-scene-key") } + val state = remember { MutableSceneTransitionLayoutState(currentScene) } + SceneTransitionLayout(state) { + scene(currentScene) { with( LockscreenContent( viewModel = viewModel, |