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

Commit 148b7af1 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make STL swipe distances configurable

This CL changes the userActions map from a Map<UserAction, SceneKey> to
Map<UserAction, UserActionResult>. In addition to the target SceneKey,
the UserActionResult also exposes a UserActionDistance that can be used
to specify the distance of the user action.

This CL makes sure that usages of the previous API still work by making
SceneKey implement UserActionResult.

Bug: 321932826
Test: atest SwipeToSceneTest
Flag: N/A
Change-Id: I6b3832f82a72d3d1dca8cef1c58ccb87b8ec2220
parent 1d576236
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ sealed class Key(val debugName: String, val identity: Any) {
class SceneKey(
    name: String,
    identity: Any = Object(),
) : Key(name, identity) {
) : Key(name, identity), UserActionResult {
    @VisibleForTesting
    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
    // access internal members.
@@ -53,6 +53,10 @@ class SceneKey(
    /** The unique [ElementKey] identifying this scene's root element. */
    val rootElementKey = ElementKey(name, identity)

    // Implementation of [UserActionResult].
    override val toScene: SceneKey = this
    override val distance: UserActionDistance? = null

    override fun toString(): String {
        return "SceneKey(debugName=$debugName)"
    }
+1 −1
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ internal class Scene(
    val key: SceneKey,
    layoutImpl: SceneTransitionLayoutImpl,
    content: @Composable SceneScope.() -> Unit,
    actions: Map<UserAction, SceneKey>,
    actions: Map<UserAction, UserActionResult>,
    zIndex: Float,
) {
    internal val scope = SceneScopeImpl(layoutImpl, this)
+77 −50
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ 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
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -75,20 +77,22 @@ internal class SceneGestureHandler(

    internal var currentSource: Any? = null

    /** The [UserAction]s associated to the current swipe. */
    private var actionUpOrLeft: UserAction? = null
    private var actionDownOrRight: UserAction? = null
    private var actionUpOrLeftNoEdge: UserAction? = null
    private var actionDownOrRightNoEdge: UserAction? = null
    private var upOrLeftScene: SceneKey? = null
    private var downOrRightScene: SceneKey? = null
    /** The [Swipe]s associated to the current gesture. */
    private var upOrLeftSwipe: Swipe? = null
    private var downOrRightSwipe: Swipe? = null
    private var upOrLeftNoEdgeSwipe: Swipe? = null
    private var downOrRightNoEdgeSwipe: Swipe? = null

    /** The [UserActionResult] associated to up and down swipes. */
    private var upOrLeftResult: UserActionResult? = null
    private var downOrRightResult: UserActionResult? = null

    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) {
        if (isDrivingTransition) {
            // This [transition] was already driving the animation: simply take over it.
            // Stop animating and start from where the current offset.
            swipeTransition.cancelOffsetAnimation()
            updateTargetScenes(swipeTransition._fromScene)
            updateTargetResults(swipeTransition._fromScene)
            return
        }

@@ -146,29 +150,23 @@ internal class SceneGestureHandler(
            )

        if (fromEdge == null) {
            actionUpOrLeft = null
            actionDownOrRight = null
            actionUpOrLeftNoEdge = upOrLeft
            actionDownOrRightNoEdge = downOrRight
            upOrLeftSwipe = null
            downOrRightSwipe = null
            upOrLeftNoEdgeSwipe = upOrLeft
            downOrRightNoEdgeSwipe = downOrRight
        } else {
            actionUpOrLeft = upOrLeft
            actionDownOrRight = downOrRight
            actionUpOrLeftNoEdge = upOrLeft.copy(fromEdge = null)
            actionDownOrRightNoEdge = downOrRight.copy(fromEdge = null)
            upOrLeftSwipe = upOrLeft
            downOrRightSwipe = downOrRight
            upOrLeftNoEdgeSwipe = upOrLeft.copy(fromEdge = null)
            downOrRightNoEdgeSwipe = downOrRight.copy(fromEdge = null)
        }
    }

    /**
     * Use the layout size in the swipe orientation for swipe distance.
     *
     * TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
     *   will also have to make sure that we correctly handle overscroll.
     */
    private fun Scene.getAbsoluteDistance(): Float {
        return when (orientation) {
            Orientation.Horizontal -> targetSize.width
            Orientation.Vertical -> targetSize.height
        }.toFloat()
    private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float {
        val targetSize = this.targetSize
        return with(distance ?: DefaultSwipeDistance) {
            layoutImpl.density.absoluteDistance(targetSize, orientation)
        }
    }

    internal fun onDrag(delta: Float) {
@@ -200,9 +198,15 @@ internal class SceneGestureHandler(
        }
    }

    private fun updateTargetScenes(fromScene: Scene) {
        upOrLeftScene = fromScene.upOrLeft()
        downOrRightScene = fromScene.downOrRight()
    private fun updateTargetResults(fromScene: Scene) {
        val userActions = fromScene.userActions
        fun sceneToSwipePair(swipe: Swipe?): UserActionResult? {
            return userActions[swipe ?: return null]
        }

        upOrLeftResult = sceneToSwipePair(upOrLeftSwipe) ?: sceneToSwipePair(upOrLeftNoEdgeSwipe)
        downOrRightResult =
            sceneToSwipePair(downOrRightSwipe) ?: sceneToSwipePair(downOrRightNoEdgeSwipe)
    }

    /**
@@ -229,9 +233,9 @@ internal class SceneGestureHandler(
        // 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
        return if (offset <= -absoluteDistance && upOrLeftScene == toScene.key) {
        return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) {
            Pair(toScene, absoluteDistance)
        } else if (offset >= absoluteDistance && downOrRightScene == toScene.key) {
        } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) {
            Pair(toScene, -absoluteDistance)
        } else {
            Pair(fromScene, 0f)
@@ -253,22 +257,32 @@ internal class SceneGestureHandler(
     *   drag into the null direction this function will return the opposite direction, assuming
     *   that the users intention is to start the drag into the other direction eventually. If
     *   [directionOffset] is 0f and both direction are available, it will default to
     *   [upOrLeftScene].
     *   [upOrLeftResult].
     */
    private inline fun findTargetSceneAndDistance(
        fromScene: Scene,
        directionOffset: Float,
        updateScenes: Boolean,
    ): Pair<Scene, Float>? {
        if (updateScenes) updateTargetScenes(fromScene)
        val absoluteDistance = fromScene.getAbsoluteDistance()
        if (updateScenes) updateTargetResults(fromScene)

        // Compute the target scene depending on the current offset.
        return when {
            upOrLeftScene == null && downOrRightScene == null -> null
            (directionOffset < 0f && upOrLeftScene != null) || downOrRightScene == null ->
                Pair(layoutImpl.scene(upOrLeftScene!!), -absoluteDistance)
            else -> Pair(layoutImpl.scene(downOrRightScene!!), absoluteDistance)
            upOrLeftResult == null && downOrRightResult == null -> null
            (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
                upOrLeftResult?.let { result ->
                    Pair(
                        layoutImpl.scene(result.toScene),
                        -fromScene.getAbsoluteDistance(result.distance)
                    )
                }
            else ->
                downOrRightResult?.let { result ->
                    Pair(
                        layoutImpl.scene(result.toScene),
                        fromScene.getAbsoluteDistance(result.distance)
                    )
                }
        }
    }

@@ -280,22 +294,23 @@ internal class SceneGestureHandler(
        fromScene: Scene,
        directionOffset: Float,
    ): Pair<Scene, Float>? {
        val absoluteDistance = fromScene.getAbsoluteDistance()
        return when {
            directionOffset > 0f ->
                upOrLeftScene?.let { Pair(layoutImpl.scene(it), -absoluteDistance) }
                upOrLeftResult?.let { result ->
                    Pair(
                        layoutImpl.scene(result.toScene),
                        -fromScene.getAbsoluteDistance(result.distance),
                    )
                }
            directionOffset < 0f ->
                downOrRightScene?.let { Pair(layoutImpl.scene(it), absoluteDistance) }
            else -> null
                downOrRightResult?.let { result ->
                    Pair(
                        layoutImpl.scene(result.toScene),
                        fromScene.getAbsoluteDistance(result.distance),
                    )
                }
            else -> null
        }

    private fun Scene.upOrLeft(): SceneKey? {
        return userActions[actionUpOrLeft] ?: userActions[actionUpOrLeftNoEdge]
    }

    private fun Scene.downOrRight(): SceneKey? {
        return userActions[actionDownOrRight] ?: userActions[actionDownOrRightNoEdge]
    }

    internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
@@ -515,6 +530,18 @@ internal class SceneGestureHandler(
    companion object {
        private const val TAG = "SceneGestureHandler"
    }

    private object DefaultSwipeDistance : UserActionDistance {
        override fun Density.absoluteDistance(
            fromSceneSize: IntSize,
            orientation: Orientation,
        ): Float {
            return when (orientation) {
                Orientation.Horizontal -> fromSceneSize.width
                Orientation.Vertical -> fromSceneSize.height
            }.toFloat()
        }
    }
}

private class SceneDraggableHandler(
+66 −1
Original line number Diff line number Diff line
@@ -27,6 +27,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize

/**
 * [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -113,7 +116,7 @@ interface SceneTransitionLayoutScope {
     */
    fun scene(
        key: SceneKey,
        userActions: Map<UserAction, SceneKey> = emptyMap(),
        userActions: Map<UserAction, UserActionResult> = emptyMap(),
        content: @Composable SceneScope.() -> Unit,
    )
}
@@ -352,6 +355,68 @@ enum class SwipeDirection(val orientation: Orientation) {
    Right(Orientation.Horizontal),
}

/**
 * The result of performing a [UserAction].
 *
 * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to
 * easily create a [UserActionResult] with a fixed distance:
 * ```
 * SceneTransitionLayout(...) {
 *     scene(
 *         Scenes.Foo,
 *         userActions =
 *             mapOf(
 *                 Swipe.Right to Scene.Bar,
 *                 Swipe.Down to Scene.Doe withDistance 100.dp,
 *             )
 *         )
 *     ) { ... }
 * }
 * ```
 */
interface 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?
}

interface UserActionDistance {
    /**
     * Return the **absolute** distance of the user action given the size of the scene we are
     * animating from and the [orientation].
     */
    fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
}

/**
 * A utility function to make it possible to define user actions with a distance using the syntax
 * `Swipe.Up to Scene.foo withDistance 100.dp`
 */
infix fun Pair<UserAction, SceneKey>.withDistance(
    distance: Dp
): Pair<UserAction, UserActionResult> {
    val scene = second
    val distance = FixedDistance(distance)
    return first to
        object : UserActionResult {
            override val toScene: SceneKey = scene
            override val distance: UserActionDistance = distance
        }
}

/** 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()
    }
}

/**
 * An internal version of [SceneTransitionLayout] to be used for tests.
 *
+5 −3
Original line number Diff line number Diff line
@@ -140,7 +140,7 @@ internal class SceneTransitionLayoutImpl(
        object : SceneTransitionLayoutScope {
                override fun scene(
                    key: SceneKey,
                    userActions: Map<UserAction, SceneKey>,
                    userActions: Map<UserAction, UserActionResult>,
                    content: @Composable SceneScope.() -> Unit,
                ) {
                    scenesToRemove.remove(key)
@@ -229,8 +229,10 @@ internal class SceneTransitionLayoutImpl(
                // Handle back events.
                // TODO(b/290184746): Make sure that this works with SystemUI once we use
                // SceneTransitionLayout in Flexiglass.
                scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
                    BackHandler { with(state) { coroutineScope.onChangeScene(backScene) } }
                scene(state.transitionState.currentScene).userActions[Back]?.let { result ->
                    // TODO(b/290184746): Handle predictive back and use result.distance if
                    // specified.
                    BackHandler { with(state) { coroutineScope.onChangeScene(result.toScene) } }
                }

                Box {
Loading