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

Commit b6fa1294 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Remove HoistedSceneTransitionLayoutState

Bug: 351315500
Test: atest PlatformComposeSceneTransitionLayoutTests
Flag: com.android.systemui.compose_bouncer
Change-Id: I134a98ed135a93c26e8d76d463eb27b02f03981d
parent b3a5f329
Loading
Loading
Loading
Loading
+17 −7
Original line number Diff line number Diff line
@@ -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,
    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,
+18 −7
Original line number Diff line number Diff line
@@ -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,
        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
+0 −51
Original line number Diff line number Diff line
@@ -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].
+0 −107
Original line number Diff line number Diff line
@@ -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,
+27 −25
Original line number Diff line number Diff line
@@ -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) {
Loading