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

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

Merge changes from topic "stl-dynamic-distance" into main

* changes:
  Move UserActionDistance to TransitionSpec
  Make it possible to compute the swipe distance lazily
parents 0491bba4 3f9cba65
Loading
Loading
Loading
Loading
+0 −22
Original line number Diff line number Diff line
@@ -16,9 +16,6 @@

package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Edge as ComposeAwareEdge
import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
@@ -26,14 +23,12 @@ import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey
import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction
import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance
import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.TransitionKey
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.shared.model.UserActionDistance
import com.android.systemui.scene.shared.model.UserActionResult

// TODO(b/293899074): remove this file once we can use the types from SceneTransitionLayout.
@@ -82,22 +77,5 @@ fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult {
    return ComposeAwareUserActionResult(
        toScene = composeUnaware.toScene.asComposeAware(),
        transitionKey = composeUnaware.transitionKey?.asComposeAware(),
        distance = composeUnaware.distance?.asComposeAware(),
    )
}

fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance {
    val composeUnware = this
    return object : ComposeAwareUserActionDistance {
        override fun Density.absoluteDistance(
            fromSceneSize: IntSize,
            orientation: Orientation,
        ): Float {
            return composeUnware.absoluteDistance(
                fromSceneWidth = fromSceneSize.width,
                fromSceneHeight = fromSceneSize.height,
                isHorizontal = orientation == Orientation.Horizontal,
            )
        }
    }
}
+101 −34
Original line number Diff line number Diff line
@@ -27,7 +27,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
@@ -56,14 +55,17 @@ internal class SceneGestureHandler(
        if (isDrivingTransition || force) {
            layoutState.startTransition(newTransition, newTransition.key)

            // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
            // layoutState.startTransition() is called, because it computes the
            // layoutState.transformationSpec().
            // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be
            // called right after layoutState.startTransition() is called, because it computes the
            // current layoutState.transformationSpec().
            val transformationSpec = layoutState.transformationSpec
            newTransition.transformationSpec = transformationSpec
            newTransition.swipeSpec =
                layoutState.transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
                transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
        } else {
            // We were not driving the transition and we don't force the update, so the spec won't
            // be used and it doesn't matter which one we set here.
            // We were not driving the transition and we don't force the update, so the specs won't
            // be used and it doesn't matter which ones we set here.
            newTransition.transformationSpec = TransformationSpec.Empty
            newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
        }

@@ -285,16 +287,21 @@ internal class SceneGestureHandler(
    ): Pair<Scene, Float> {
        val toScene = swipeTransition._toScene
        val fromScene = swipeTransition._fromScene
        val absoluteDistance = swipeTransition.distance.absoluteValue
        val distance = swipeTransition.distance()

        // If the swipe was not committed, don't do anything.
        if (swipeTransition._currentScene != toScene) {
        // If the swipe was not committed or if the swipe distance is not computed yet, don't do
        // anything.
        if (
            swipeTransition._currentScene != toScene ||
                distance == SwipeTransition.DistanceUnspecified
        ) {
            return fromScene to 0f
        }

        // If the offset is past the distance then let's change fromScene so that the user can swipe
        // to the next screen or go back to the previous one.
        val offset = swipeTransition.dragOffset
        val absoluteDistance = distance.absoluteValue
        return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) {
            toScene to absoluteDistance
        } else if (
@@ -347,10 +354,11 @@ internal class SceneGestureHandler(

            // Compute the destination scene (and therefore offset) to settle in.
            val offset = swipeTransition.dragOffset
            val distance = swipeTransition.distance
            val distance = swipeTransition.distance()
            var targetScene: Scene
            var targetOffset: Float
            if (
                distance != SwipeTransition.DistanceUnspecified &&
                    shouldCommitSwipe(
                        offset,
                        distance,
@@ -372,7 +380,15 @@ internal class SceneGestureHandler(
                // We wanted to change to a new scene but we are not allowed to, so we animate back
                // to the current scene.
                targetScene = swipeTransition._currentScene
                targetOffset = if (targetScene == fromScene) 0f else distance
                targetOffset =
                    if (targetScene == fromScene) {
                        0f
                    } else {
                        check(distance != SwipeTransition.DistanceUnspecified) {
                            "distance is equal to ${SwipeTransition.DistanceUnspecified}"
                        }
                        distance
                    }
            }

            animateTo(targetScene = targetScene, targetOffset = targetOffset)
@@ -459,22 +475,20 @@ private fun SwipeTransition(
): SwipeTransition {
    val upOrLeftResult = swipes.upOrLeftResult
    val downOrRightResult = swipes.downOrRightResult
    val userActionDistance = result.distance ?: DefaultSwipeDistance
    val absoluteDistance =
        with(userActionDistance) {
            layoutImpl.density.absoluteDistance(fromScene.targetSize, orientation)
    val isUpOrLeft =
        when (result) {
            upOrLeftResult -> true
            downOrRightResult -> false
            else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
        }

    return SwipeTransition(
        key = result.transitionKey,
        _fromScene = fromScene,
        _toScene = layoutImpl.scene(result.toScene),
        distance =
            when (result) {
                upOrLeftResult -> -absoluteDistance
                downOrRightResult -> absoluteDistance
                else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
            },
        userActionDistanceScope = layoutImpl.userActionDistanceScope,
        orientation = orientation,
        isUpOrLeft = isUpOrLeft,
    )
}

@@ -482,11 +496,9 @@ private class SwipeTransition(
    val key: TransitionKey?,
    val _fromScene: Scene,
    val _toScene: Scene,
    /**
     * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
     * or to the left of [toScene]
     */
    val distance: Float,
    private val userActionDistanceScope: UserActionDistanceScope,
    private val orientation: Orientation,
    private val isUpOrLeft: Boolean,
) : TransitionState.Transition(_fromScene.key, _toScene.key) {
    var _currentScene by mutableStateOf(_fromScene)
    override val currentScene: SceneKey
@@ -494,7 +506,16 @@ private class SwipeTransition(

    override val progress: Float
        get() {
            // Important: If we are going to return early because distance is equal to 0, we should
            // still make sure we read the offset before returning so that the calling code still
            // subscribes to the offset value.
            val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset

            val distance = distance()
            if (distance == DistanceUnspecified) {
                return 0f
            }

            return offset / distance
        }

@@ -518,9 +539,50 @@ private class SwipeTransition(
    /** Job to check that there is at most one offset animation in progress. */
    private var offsetAnimationJob: Job? = null

    /**
     * The [TransformationSpecImpl] associated to this transition.
     *
     * Note: This is lateinit because this [SwipeTransition] is needed by
     * [BaseSceneTransitionLayoutState] to compute the [TransitionSpec], and it will be set right
     * after [BaseSceneTransitionLayoutState.startTransition] is called with this transition.
     */
    lateinit var transformationSpec: TransformationSpecImpl

    /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
    lateinit var swipeSpec: SpringSpec<Float>

    private var lastDistance = DistanceUnspecified

    /**
     * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
     * or to the left of [toScene].
     *
     * Note that this distance can be equal to [DistanceUnspecified] during the first frame of a
     * transition when the distance depends on the size or position of an element that is composed
     * in the scene we are going to.
     */
    fun distance(): Float {
        if (lastDistance != DistanceUnspecified) {
            return lastDistance
        }

        val absoluteDistance =
            with(transformationSpec.distance ?: DefaultSwipeDistance) {
                userActionDistanceScope.absoluteDistance(
                    _fromScene.targetSize,
                    orientation,
                )
            }

        if (absoluteDistance <= 0f) {
            return DistanceUnspecified
        }

        val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance
        lastDistance = distance
        return distance
    }

    /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
    private fun startOffsetAnimation(job: () -> Job) {
        cancelOffsetAnimation()
@@ -563,6 +625,7 @@ private class SwipeTransition(
        }
        isAnimatingOffset = true

        val animationSpec = transformationSpec
        offsetAnimatable.animateTo(
            targetValue = targetOffset,
            animationSpec = swipeSpec,
@@ -571,10 +634,14 @@ private class SwipeTransition(

        finishOffsetAnimation()
    }

    companion object {
        const val DistanceUnspecified = 0f
    }
}

private object DefaultSwipeDistance : UserActionDistance {
    override fun Density.absoluteDistance(
    override fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation,
    ): Float {
+36 −19
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
@@ -394,36 +395,52 @@ class UserActionResult(
    /** The scene we should be transitioning to during the [UserAction]. */
    val toScene: SceneKey,

    /**
     * The distance the action takes to animate from 0% to 100%.
     *
     * If `null`, a default distance will be used that depends on the [UserAction] performed.
     */
    val distance: UserActionDistance? = null,

    /** The key of the transition that should be used. */
    val transitionKey: TransitionKey? = null,
) {
    constructor(
        toScene: SceneKey,
        distance: Dp,
        transitionKey: TransitionKey? = null,
    ) : this(toScene, FixedDistance(distance), transitionKey)
}
)

interface UserActionDistance {
    /**
     * Return the **absolute** distance of the user action given the size of the scene we are
     * animating from and the [orientation].
     *
     * Note: This function will be called for each drag event until it returns a value > 0f. This
     * for instance allows you to return 0f or a negative value until the first layout pass of a
     * scene, so that you can use the size and position of elements in the scene we are
     * transitioning to when computing this absolute distance.
     */
    fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
    fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation
    ): Float
}

/** The user action has a fixed [absoluteDistance]. */
private class FixedDistance(private val distance: Dp) : UserActionDistance {
    override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float {
        return distance.toPx()
interface UserActionDistanceScope : Density {
    /**
     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
     * when idle, or `null` if the element is not composed and measured in that scene (yet).
     */
    fun ElementKey.targetSize(scene: SceneKey): IntSize?

    /**
     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
     */
    fun ElementKey.targetOffset(scene: SceneKey): Offset?

    /**
     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
     * the scene was never composed.
     */
    fun SceneKey.targetSize(): IntSize?
}

/** The user action has a fixed [absoluteDistance]. */
class FixedDistance(private val distance: Dp) : UserActionDistance {
    override fun UserActionDistanceScope.absoluteDistance(
        fromSceneSize: IntSize,
        orientation: Orientation,
    ): Float = distance.toPx()
}

/**
+9 −0
Original line number Diff line number Diff line
@@ -96,9 +96,18 @@ internal class SceneTransitionLayoutImpl(
                ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SnapshotStateMap<SceneKey, *>>>()
                    .also { _sharedValues = it }

    // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed.
    private val horizontalGestureHandler: SceneGestureHandler
    private val verticalGestureHandler: SceneGestureHandler

    private var _userActionDistanceScope: UserActionDistanceScope? = null
    internal val userActionDistanceScope: UserActionDistanceScope
        get() =
            _userActionDistanceScope
                ?: UserActionDistanceScopeImpl(layoutImpl = this).also {
                    _userActionDistanceScope = it
                }

    init {
        updateScenes(builder)

+11 −0
Original line number Diff line number Diff line
@@ -163,6 +163,14 @@ interface TransformationSpec {
     */
    val swipeSpec: SpringSpec<Float>?

    /**
     * The distance it takes for this transition to animate from 0% to 100% when it is driven by a
     * [UserAction].
     *
     * If `null`, a default distance will be used that depends on the [UserAction] performed.
     */
    val distance: UserActionDistance?

    /** The list of [Transformation] applied to elements during this transition. */
    val transformations: List<Transformation>

@@ -171,6 +179,7 @@ interface TransformationSpec {
            TransformationSpecImpl(
                progressSpec = snap(),
                swipeSpec = null,
                distance = null,
                transformations = emptyList(),
            )
        internal val EmptyProvider = { Empty }
@@ -193,6 +202,7 @@ internal class TransitionSpecImpl(
                TransformationSpecImpl(
                    progressSpec = reverse.progressSpec,
                    swipeSpec = reverse.swipeSpec,
                    distance = reverse.distance,
                    transformations = reverse.transformations.map { it.reversed() }
                )
            }
@@ -209,6 +219,7 @@ internal class TransitionSpecImpl(
internal class TransformationSpecImpl(
    override val progressSpec: AnimationSpec<Float>,
    override val swipeSpec: SpringSpec<Float>?,
    override val distance: UserActionDistance?,
    override val transformations: List<Transformation>,
) : TransformationSpec {
    private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
Loading