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

Commit fe0ec045 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge changes from topic "remove-hoisted-stlstate" into main

* changes:
  Interrupted predictive back does not call canChangeScene
  Move (predictive) back tests into PredictiveBackHandlerTest
  Remove HoistedSceneTransitionLayoutState
parents fe2f8c35 5ad010b4
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
+7 −10
Original line number Diff line number Diff line
@@ -56,13 +56,7 @@ internal fun PredictiveBackHandler(
            progress.collect { backEvent -> transition.dragProgress = backEvent.progress }

            // Back gesture successful.
            transition.animateTo(
                if (state.canChangeScene(targetSceneForBack)) {
                    targetSceneForBack
                } else {
                    fromScene
                }
            )
            transition.animateTo(targetSceneForBack)
        } catch (e: CancellationException) {
            // Back gesture cancelled.
            transition.animateTo(fromScene)
@@ -105,12 +99,15 @@ private class PredictiveBackTransition(
            return it
        }

        if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) {
            currentScene = scene
        }

        val targetProgress =
            when (scene) {
            when (currentScene) {
                fromScene -> 0f
                toScene -> 1f
                else -> error("scene $scene should be either $fromScene or $toScene")
                else -> error("scene $currentScene should be either $fromScene or $toScene")
            }

        val animatable = Animatable(dragProgress).also { progressAnimatable = it }
+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,
Loading