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

Commit 4165bbe5 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Extract ContentState out of TransitionState (1/2)

This CL is a pure extraction of TransitionState into a ContentState
class, meant to be subclassed by OverlayState in follow-up CLs.

Bug: 353679003
Test: atest PlatformComposeSceneTransitionLayoutTests
Flag: com.android.systemui.scene_container
Change-Id: I1a0776412bf9335a91d3ced3af36ff5748f28ccd
parent 411a4c14
Loading
Loading
Loading
Loading
+6 −5
Original line number Diff line number Diff line
@@ -31,8 +31,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.content.Scene
import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.ContentState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
@@ -609,7 +610,7 @@ private class SwipeTransition(
    var lastDistance: Float = DistanceUnspecified,
) :
    TransitionState.Transition(_fromScene.key, _toScene.key, replacedTransition),
    TransitionState.HasOverscrollProperties {
    ContentState.HasOverscrollProperties {
    var _currentScene by mutableStateOf(_fromScene)
    override val currentScene: SceneKey
        get() = _currentScene.key
@@ -646,7 +647,7 @@ private class SwipeTransition(

    override val isInitiatedByUserInput = true

    override var bouncingScene: SceneKey? = null
    override var bouncingContent: SceneKey? = null

    /** The current offset caused by the drag gesture. */
    var dragOffset by mutableFloatStateOf(0f)
@@ -765,7 +766,7 @@ private class SwipeTransition(
                                animationSpec = swipeSpec,
                                initialVelocity = initialVelocity,
                            ) {
                                if (bouncingScene == null) {
                                if (bouncingContent == null) {
                                    val isBouncing =
                                        if (isTargetGreater) {
                                            if (startedWhenOvercrollingTargetScene) {
@@ -782,7 +783,7 @@ private class SwipeTransition(
                                        }

                                    if (isBouncing) {
                                        bouncingScene = targetScene
                                        bouncingContent = targetScene

                                        // Immediately stop this transition if we are bouncing on a
                                        // scene that does not bounce.
+2 −1
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastLastOrNull
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
@@ -1075,7 +1076,7 @@ private inline fun <T> computeValue(
    }

    val currentScene = currentContentState.content
    if (transition is TransitionState.HasOverscrollProperties) {
    if (transition is ContentState.HasOverscrollProperties) {
        val overscroll = transition.currentOverscrollSpec
        if (overscroll?.scene == currentScene) {
            val elementSpec =
+2 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import com.android.compose.animation.scene.content.state.ContentState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transition.link.LinkedTransition
import com.android.compose.animation.scene.transition.link.StateLink
@@ -239,7 +240,7 @@ internal class MutableSceneTransitionLayoutStateImpl(
        // Compute the [TransformationSpec] when the transition starts.
        val fromScene = transition.fromScene
        val toScene = transition.toScene
        val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
        val orientation = (transition as? ContentState.HasOverscrollProperties)?.orientation

        // Update the transition specs.
        transition.transformationSpec =
+0 −1
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified

/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
+65 −76
Original line number Diff line number Diff line
@@ -21,10 +21,10 @@ import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.OverscrollScope
import com.android.compose.animation.scene.OverscrollSpecImpl
import com.android.compose.animation.scene.ProgressVisibilityThreshold
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
@@ -32,33 +32,18 @@ import com.android.compose.animation.scene.TransitionKey
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/** The state associated to one or more contents. */
@Stable
sealed interface TransitionState {
    /**
     * The current effective scene. If a new transition was triggered, it would start from this
     * scene.
     *
     * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
     * gesture starts, but then if the user flings their finger and commits the transition to scene
     * B, then [currentScene] becomes scene B even if the transition is not finished yet and is
     * still animating to settle to scene B.
     */
    val currentScene: SceneKey

    /** No transition/animation is currently running. */
    data class Idle(override val currentScene: SceneKey) : TransitionState

    /** There is a transition animating between two scenes. */
    abstract class Transition(
        /** The scene this transition is starting from. Can't be the same as toScene */
        val fromScene: SceneKey,

        /** The scene this transition is going to. Can't be the same as fromScene */
        val toScene: SceneKey,

        /** The transition that `this` transition is replacing, if any. */
        internal val replacedTransition: Transition? = null,
    ) : TransitionState {
sealed interface ContentState<out T : ContentKey> {
    /** The [content] is idle, it does not animate. */
    sealed class Idle<T : ContentKey>(val content: T) : ContentState<T>

    /** The content is transitioning with another content. */
    sealed class Transition<out T : ContentKey>(
        val fromContent: T,
        val toContent: T,
        internal val replacedTransition: Transition<T>?,
    ) : ContentState<T> {
        /**
         * The key of this transition. This should usually be null, but it can be specified to use a
         * specific set of transformations associated to this transition.
@@ -75,24 +60,24 @@ sealed interface TransitionState {
        /** The current velocity of [progress], in progress units. */
        abstract val progressVelocity: Float

        /** Whether the transition was triggered by user input rather than being programmatic. */
        abstract val isInitiatedByUserInput: Boolean

        /** Whether user input is currently driving the transition. */
        abstract val isUserInputOngoing: Boolean

        /**
         * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
         * also be less than `0` or greater than `1` when using transitions with a spring
         * AnimationSpec or when flinging quickly during a swipe gesture.
         */
        open val previewProgress: Float = 0f
        internal open val previewProgress: Float = 0f

        /** The current velocity of [previewProgress], in progress units. */
        open val previewProgressVelocity: Float = 0f
        internal open val previewProgressVelocity: Float = 0f

        /** Whether the transition is currently in the preview stage */
        open val isInPreviewStage: Boolean = false

        /** Whether the transition was triggered by user input rather than being programmatic. */
        abstract val isInitiatedByUserInput: Boolean

        /** Whether user input is currently driving the transition. */
        abstract val isUserInputOngoing: Boolean
        internal open val isInPreviewStage: Boolean = false

        /**
         * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
@@ -111,29 +96,14 @@ sealed interface TransitionState {
            get() {
                if (this !is HasOverscrollProperties) return null
                val progress = progress
                val bouncingScene = bouncingScene
                val bouncingContent = bouncingContent
                return when {
                    progress < 0f || bouncingScene == fromScene -> fromOverscrollSpec
                    progress > 1f || bouncingScene == toScene -> toOverscrollSpec
                    progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
                    progress > 1f || bouncingContent == toContent -> toOverscrollSpec
                    else -> null
                }
            }

        /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
        internal fun isWithinProgressRange(progress: Float): Boolean {
            // If the properties are missing we assume that every [Transition] can overscroll
            if (this !is HasOverscrollProperties) return true
            // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
            val specForCurrentScene =
                when {
                    progress <= 0f -> fromOverscrollSpec
                    progress >= 1f -> toOverscrollSpec
                    else -> null
                } ?: return true

            return specForCurrentScene.transformationSpec.transformations.isNotEmpty()
        }

        /**
         * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
         * jump of values when this transitions interrupts another one.
@@ -141,37 +111,40 @@ sealed interface TransitionState {
        private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null

        init {
            check(fromScene != toScene)
            check(fromContent != toContent)
            check(
                replacedTransition == null ||
                    (replacedTransition.fromScene == fromScene &&
                        replacedTransition.toScene == toScene)
                    (replacedTransition.fromContent == fromContent &&
                        replacedTransition.toContent == toContent)
            )
        }

        /**
         * Force this transition to finish and animate to [currentScene], so that this transition
         * progress will settle to either 0% (if [currentScene] == [fromScene]) or 100% (if
         * [currentScene] == [toScene]) in a finite amount of time.
         * Force this transition to finish and animate to an [Idle] state.
         *
         * Important: Once this is called, the effective state of the transition should remain
         * unchanged. For instance, in the case of a [TransitionState.Transition], its
         * [currentScene][TransitionState.Transition.currentScene] should never change once [finish]
         * is called.
         *
         * @return the [Job] that animates the progress to [currentScene]. It can be used to wait
         *   until the animation is complete or cancel it to snap to [currentScene]. Calling
         *   [finish] multiple times will return the same [Job].
         * @return the [Job] that animates to the idle state. It can be used to wait until the
         *   animation is complete or cancel it to snap the animation. Calling [finish] multiple
         *   times will return the same [Job].
         */
        abstract fun finish(): Job

        /**
         * Whether we are transitioning. If [from] or [to] is empty, we will also check that they
         * match the scenes we are animating from and/or to.
         * match the contents we are animating from and/or to.
         */
        fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
            return (from == null || fromScene == from) && (to == null || toScene == to)
        fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean {
            return (from == null || fromContent == from) && (to == null || toContent == to)
        }

        /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */
        fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean {
            return isTransitioning(from = scene, to = other) ||
                isTransitioning(from = other, to = scene)
        /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
        fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
            return isTransitioning(from = content, to = other) ||
                isTransitioning(from = other, to = content)
        }

        internal fun updateOverscrollSpecs(
@@ -182,6 +155,21 @@ sealed interface TransitionState {
            toOverscrollSpec = toSpec
        }

        /** Returns if the [progress] value of this transition can go beyond range `[0; 1]` */
        internal fun isWithinProgressRange(progress: Float): Boolean {
            // If the properties are missing we assume that every [Transition] can overscroll
            if (this !is HasOverscrollProperties) return true
            // [OverscrollSpec] for the current scene, even if it hasn't started overscrolling yet.
            val specForCurrentScene =
                when {
                    progress <= 0f -> fromOverscrollSpec
                    progress >= 1f -> toOverscrollSpec
                    else -> null
                } ?: return true

            return specForCurrentScene.transformationSpec.transformations.isNotEmpty()
        }

        internal open fun interruptionProgress(
            layoutImpl: SceneTransitionLayoutImpl,
        ): Float {
@@ -216,14 +204,14 @@ sealed interface TransitionState {

    interface HasOverscrollProperties {
        /**
         * The position of the [Transition.toScene].
         * The position of the [Transition.toContent].
         *
         * Used to understand the direction of the overscroll.
         */
        val isUpOrLeft: Boolean

        /**
         * The relative orientation between [Transition.fromScene] and [Transition.toScene].
         * The relative orientation between [Transition.fromContent] and [Transition.toContent].
         *
         * Used to understand the orientation of the overscroll.
         */
@@ -231,15 +219,16 @@ sealed interface TransitionState {

        /**
         * Scope which can be used in the Overscroll DSL to define a transformation based on the
         * distance between [Transition.fromScene] and [Transition.toScene].
         * distance between [Transition.fromContent] and [Transition.toContent].
         */
        val overscrollScope: OverscrollScope

        /**
         * The scene around which the transition is currently bouncing. When not `null`, this
         * transition is currently oscillating around this scene and will soon settle to that scene.
         * The content (scene or overlay) around which the transition is currently bouncing. When
         * not `null`, this transition is currently oscillating around this content and will soon
         * settle to that content.
         */
        val bouncingScene: SceneKey?
        val bouncingContent: ContentKey?

        companion object {
            const val DistanceUnspecified = 0f
Loading