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

Commit f2825433 authored by omarmt's avatar omarmt
Browse files

STL introduce Content.findActionResultBestMatch(swipe)

This refactor lets us search for action results using more complex
conditions, like optionally specifying the pointer type (e.g., Mouse or
Touch). It also makes it easier to add other search conditions in the
future. We'll find gestures that perfectly match our requirements, or
use the closest match if needed.

Test: Just a refactor.
Bug: 371984715
Flag: com.android.systemui.scene_container
Change-Id: I7da1b608942c1065318911dffd1549055f8ccbbf
parent 70e721f9
Loading
Loading
Loading
Loading
+63 −105
Original line number Diff line number Diff line
@@ -35,12 +35,11 @@ internal typealias SuspendedValue<T> = suspend () -> T

internal interface DraggableHandler {
    /**
     * Start a drag in the given [startedPosition], with the given [overSlop] and number of
     * [pointersDown].
     * Start a drag with the given [pointersInfo] and [overSlop].
     *
     * The returned [DragController] should be used to continue or stop the drag.
     */
    fun onDragStarted(startedPosition: Offset?, overSlop: Float, pointersDown: Int): DragController
    fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController
}

/**
@@ -95,7 +94,7 @@ internal class DraggableHandlerImpl(
     * Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f,
     * indicating that the transition should be intercepted.
     */
    internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean {
    internal fun shouldImmediatelyIntercept(pointersInfo: PointersInfo?): Boolean {
        // We don't intercept the touch if we are not currently driving the transition.
        val dragController = dragController
        if (dragController?.isDrivingTransition != true) {
@@ -106,7 +105,7 @@ internal class DraggableHandlerImpl(

        // Only intercept the current transition if one of the 2 swipes results is also a transition
        // between the same pair of contents.
        val swipes = computeSwipes(startedPosition, pointersDown = 1)
        val swipes = computeSwipes(pointersInfo)
        val fromContent = layoutImpl.content(swipeAnimation.currentContent)
        val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent)
        val currentScene = layoutImpl.state.currentScene
@@ -123,11 +122,7 @@ internal class DraggableHandlerImpl(
                ))
    }

    override fun onDragStarted(
        startedPosition: Offset?,
        overSlop: Float,
        pointersDown: Int,
    ): DragController {
    override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController {
        if (overSlop == 0f) {
            val oldDragController = dragController
            check(oldDragController != null && oldDragController.isDrivingTransition) {
@@ -152,7 +147,7 @@ internal class DraggableHandlerImpl(
            return updateDragController(swipes, swipeAnimation)
        }

        val swipes = computeSwipes(startedPosition, pointersDown)
        val swipes = computeSwipes(pointersInfo)
        val fromContent = layoutImpl.contentForUserActions()

        swipes.updateSwipesResults(fromContent)
@@ -189,8 +184,7 @@ internal class DraggableHandlerImpl(
        return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
    }

    internal fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
        if (startedPosition == null) return null
    internal fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? {
        return layoutImpl.swipeSourceDetector.source(
            layoutSize = layoutImpl.lastSize,
            position = startedPosition.round(),
@@ -199,10 +193,20 @@ internal class DraggableHandlerImpl(
        )
    }

    internal fun resolveSwipe(
        pointersDown: Int,
        fromSource: SwipeSource.Resolved?,
    private fun computeSwipes(pointersInfo: PointersInfo?): Swipes {
        val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) }
        return Swipes(
            upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource),
            downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource),
        )
    }
}

private fun resolveSwipe(
    orientation: Orientation,
    isUpOrLeft: Boolean,
    pointersInfo: PointersInfo?,
    fromSource: SwipeSource.Resolved?,
): Swipe.Resolved {
    return Swipe.Resolved(
        direction =
@@ -221,37 +225,12 @@ internal class DraggableHandlerImpl(
                        SwipeDirection.Resolved.Down
                    }
            },
            pointerCount = pointersDown,
        // If the number of pointers is not specified, 1 is assumed.
        pointerCount = pointersInfo?.pointersDown ?: 1,
        fromSource = fromSource,
    )
}

    private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes {
        val fromSource = resolveSwipeSource(startedPosition)
        val upOrLeft = resolveSwipe(pointersDown, fromSource, isUpOrLeft = true)
        val downOrRight = resolveSwipe(pointersDown, fromSource, isUpOrLeft = false)
        return if (fromSource == null) {
            Swipes(
                upOrLeft = null,
                downOrRight = null,
                upOrLeftNoSource = upOrLeft,
                downOrRightNoSource = downOrRight,
            )
        } else {
            Swipes(
                upOrLeft = upOrLeft,
                downOrRight = downOrRight,
                upOrLeftNoSource = upOrLeft.copy(fromSource = null),
                downOrRightNoSource = downOrRight.copy(fromSource = null),
            )
        }
    }

    companion object {
        private const val TAG = "DraggableHandlerImpl"
    }
}

/** @param swipes The [Swipes] associated to the current gesture. */
private class DragControllerImpl(
    private val draggableHandler: DraggableHandlerImpl,
@@ -497,24 +476,14 @@ private class DragControllerImpl(
}

/** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
internal class Swipes(
    val upOrLeft: Swipe.Resolved?,
    val downOrRight: Swipe.Resolved?,
    val upOrLeftNoSource: Swipe.Resolved?,
    val downOrRightNoSource: Swipe.Resolved?,
) {
internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resolved) {
    /** The [UserActionResult] associated to up and down swipes. */
    var upOrLeftResult: UserActionResult? = null
    var downOrRightResult: UserActionResult? = null

    fun computeSwipesResults(fromContent: Content): Pair<UserActionResult?, UserActionResult?> {
        val userActions = fromContent.userActions
        fun result(swipe: Swipe.Resolved?): UserActionResult? {
            return userActions[swipe ?: return null]
        }

        val upOrLeftResult = result(upOrLeft) ?: result(upOrLeftNoSource)
        val downOrRightResult = result(downOrRight) ?: result(downOrRightNoSource)
        val upOrLeftResult = fromContent.findActionResultBestMatch(swipe = upOrLeft)
        val downOrRightResult = fromContent.findActionResultBestMatch(swipe = downOrRight)
        return upOrLeftResult to downOrRightResult
    }

@@ -568,11 +537,13 @@ internal class NestedScrollHandlerImpl(

    val connection: PriorityNestedScrollConnection = nestedScrollConnection()

    private fun PointersInfo.resolveSwipe(isUpOrLeft: Boolean): Swipe.Resolved {
        return draggableHandler.resolveSwipe(
            pointersDown = pointersDown,
            fromSource = draggableHandler.resolveSwipeSource(startedPosition),
    private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved {
        return resolveSwipe(
            orientation = draggableHandler.orientation,
            isUpOrLeft = isUpOrLeft,
            pointersInfo = pointersInfo,
            fromSource =
                pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) },
        )
    }

@@ -581,12 +552,7 @@ internal class NestedScrollHandlerImpl(
        // moving on to the next scene.
        var canChangeScene = false

        var _lastPointersInfo: PointersInfo? = null
        fun pointersInfo(): PointersInfo {
            return checkNotNull(_lastPointersInfo) {
                "PointersInfo should be initialized before the transition begins."
            }
        }
        var lastPointersInfo: PointersInfo? = null

        fun hasNextScene(amount: Float): Boolean {
            val transitionState = layoutState.transitionState
@@ -594,17 +560,11 @@ internal class NestedScrollHandlerImpl(
            val fromScene = layoutImpl.scene(scene)
            val resolvedSwipe =
                when {
                    amount < 0f -> pointersInfo().resolveSwipe(isUpOrLeft = true)
                    amount > 0f -> pointersInfo().resolveSwipe(isUpOrLeft = false)
                    amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo)
                    amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo)
                    else -> null
                }
            val nextScene =
                resolvedSwipe?.let {
                    fromScene.userActions[it]
                        ?: if (it.fromSource != null) {
                            fromScene.userActions[it.copy(fromSource = null)]
                        } else null
                }
            val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) }
            if (nextScene != null) return true

            if (transitionState !is TransitionState.Idle) return false
@@ -618,13 +578,14 @@ internal class NestedScrollHandlerImpl(
        return PriorityNestedScrollConnection(
            orientation = orientation,
            canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
                val pointersInfo = pointersInfoOwner.pointersInfo()
                canChangeScene =
                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f

                val canInterceptSwipeTransition =
                    canChangeScene &&
                        offsetAvailable != 0f &&
                        draggableHandler.shouldImmediatelyIntercept(startedPosition = null)
                        draggableHandler.shouldImmediatelyIntercept(pointersInfo)
                if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false

                val threshold = layoutImpl.transitionInterceptionThreshold
@@ -635,13 +596,11 @@ internal class NestedScrollHandlerImpl(
                    return@PriorityNestedScrollConnection false
                }

                val pointersInfo = pointersInfoOwner.pointersInfo()

                if (pointersInfo.isMouseWheel) {
                if (pointersInfo?.isMouseWheel == true) {
                    // Do not support mouse wheel interactions
                    return@PriorityNestedScrollConnection false
                }
                _lastPointersInfo = pointersInfo
                lastPointersInfo = pointersInfo

                // If the current swipe transition is *not* closed to 0f or 1f, then we want the
                // scroll events to intercept the current transition to continue the scene
@@ -661,11 +620,11 @@ internal class NestedScrollHandlerImpl(
                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f

                val pointersInfo = pointersInfoOwner.pointersInfo()
                if (pointersInfo.isMouseWheel) {
                if (pointersInfo?.isMouseWheel == true) {
                    // Do not support mouse wheel interactions
                    return@PriorityNestedScrollConnection false
                }
                _lastPointersInfo = pointersInfo
                lastPointersInfo = pointersInfo

                val canStart =
                    when (behavior) {
@@ -703,11 +662,11 @@ internal class NestedScrollHandlerImpl(
                canChangeScene = false

                val pointersInfo = pointersInfoOwner.pointersInfo()
                if (pointersInfo.isMouseWheel) {
                if (pointersInfo?.isMouseWheel == true) {
                    // Do not support mouse wheel interactions
                    return@PriorityNestedScrollConnection false
                }
                _lastPointersInfo = pointersInfo
                lastPointersInfo = pointersInfo

                val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
                if (canStart) {
@@ -717,12 +676,11 @@ internal class NestedScrollHandlerImpl(
                canStart
            },
            onStart = { firstScroll ->
                val pointersInfo = pointersInfo()
                val pointersInfo = lastPointersInfo
                scrollController(
                    dragController =
                        draggableHandler.onDragStarted(
                            pointersDown = pointersInfo.pointersDown,
                            startedPosition = pointersInfo.startedPosition,
                            pointersInfo = pointersInfo,
                            overSlop = if (isIntercepting) 0f else firstScroll,
                        ),
                    canChangeScene = canChangeScene,
@@ -741,7 +699,7 @@ private fun scrollController(
    return object : ScrollController {
        override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
            val pointersInfo = pointersInfoOwner.pointersInfo()
            if (pointersInfo.isMouseWheel) {
            if (pointersInfo?.isMouseWheel == true) {
                // Do not support mouse wheel interactions
                return 0f
            }
+102 −67
Original line number Diff line number Diff line
@@ -78,8 +78,8 @@ import kotlinx.coroutines.launch
@Stable
internal fun Modifier.multiPointerDraggable(
    orientation: Orientation,
    startDragImmediately: (startedPosition: Offset) -> Boolean,
    onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
    onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
    onFirstPointerDown: () -> Unit = {},
    swipeDetector: SwipeDetector = DefaultSwipeDetector,
    dispatcher: NestedScrollDispatcher,
@@ -97,9 +97,8 @@ internal fun Modifier.multiPointerDraggable(

private data class MultiPointerDraggableElement(
    private val orientation: Orientation,
    private val startDragImmediately: (startedPosition: Offset) -> Boolean,
    private val onDragStarted:
        (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
    private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
    private val onFirstPointerDown: () -> Unit,
    private val swipeDetector: SwipeDetector,
    private val dispatcher: NestedScrollDispatcher,
@@ -125,9 +124,8 @@ private data class MultiPointerDraggableElement(

internal class MultiPointerDraggableNode(
    orientation: Orientation,
    var startDragImmediately: (startedPosition: Offset) -> Boolean,
    var onDragStarted:
        (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
    var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
    var onFirstPointerDown: () -> Unit,
    swipeDetector: SwipeDetector = DefaultSwipeDetector,
    private val dispatcher: NestedScrollDispatcher,
@@ -183,17 +181,22 @@ internal class MultiPointerDraggableNode(
        pointerInput.onPointerEvent(pointerEvent, pass, bounds)
    }

    private var lastPointerEvent: PointerEvent? = null
    private var startedPosition: Offset? = null
    private var pointersDown: Int = 0
    private var isMouseWheel: Boolean = false

    internal fun pointersInfo(): PointersInfo {
        return PointersInfo(
    internal fun pointersInfo(): PointersInfo? {
        val startedPosition = startedPosition
        val lastPointerEvent = lastPointerEvent
        if (startedPosition == null || lastPointerEvent == null) {
            // This may be null, i.e. when the user uses TalkBack
            return null
        }

        return PointersInfo(
            startedPosition = startedPosition,
            // We could have 0 pointers during fling or for other reasons.
            pointersDown = pointersDown.coerceAtLeast(1),
            isMouseWheel = isMouseWheel,
            pointersDown = pointersDown,
            lastPointerEvent = lastPointerEvent,
        )
    }

@@ -212,8 +215,8 @@ internal class MultiPointerDraggableNode(
                if (pointerEvent.type == PointerEventType.Enter) continue

                val changes = pointerEvent.changes
                lastPointerEvent = pointerEvent
                pointersDown = changes.countDown()
                isMouseWheel = pointerEvent.type == PointerEventType.Scroll

                when {
                    // There are no more pointers down.
@@ -285,8 +288,8 @@ internal class MultiPointerDraggableNode(
                    detectDragGestures(
                        orientation = orientation,
                        startDragImmediately = startDragImmediately,
                        onDragStart = { startedPosition, overSlop, pointersDown ->
                            onDragStarted(startedPosition, overSlop, pointersDown)
                        onDragStart = { pointersInfo, overSlop ->
                            onDragStarted(pointersInfo, overSlop)
                        },
                        onDrag = { controller, amount ->
                            dispatchScrollEvents(
@@ -435,9 +438,8 @@ internal class MultiPointerDraggableNode(
     */
    private suspend fun AwaitPointerEventScope.detectDragGestures(
        orientation: Orientation,
        startDragImmediately: (startedPosition: Offset) -> Boolean,
        onDragStart:
            (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
        startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
        onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
        onDrag: (controller: DragController, dragAmount: Float) -> Unit,
        onDragEnd: (controller: DragController) -> Unit,
        onDragCancel: (controller: DragController) -> Unit,
@@ -462,8 +464,13 @@ internal class MultiPointerDraggableNode(
                .first()

        var overSlop = 0f
        var lastPointersInfo =
            checkNotNull(pointersInfo()) {
                "We should have pointers down, last event: $currentEvent"
            }

        val drag =
            if (startDragImmediately(consumablePointer.position)) {
            if (startDragImmediately(lastPointersInfo)) {
                consumablePointer.consume()
                consumablePointer
            } else {
@@ -488,14 +495,18 @@ internal class MultiPointerDraggableNode(
                                consumablePointer.id,
                                onSlopReached,
                            )
                    }
                    } ?: return

                lastPointersInfo =
                    checkNotNull(pointersInfo()) {
                        "We should have pointers down, last event: $currentEvent"
                    }
                // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
                // the touch slop. However, the overSlop we pass to onDragStarted() is used to
                // compute the direction we are dragging in, so overSlop should never be 0f unless
                // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned
                // true).
                if (drag != null && overSlop == 0f) {
                if (overSlop == 0f) {
                    val delta = (drag.position - consumablePointer.position).toFloat()
                    check(delta != 0f) { "delta is equal to 0" }
                    overSlop = delta.sign
@@ -503,17 +514,7 @@ internal class MultiPointerDraggableNode(
                drag
            }

        if (drag != null) {
            val controller =
                onDragStart(
                    // The startedPosition is the starting position when a gesture begins (when the
                    // first pointer touches the screen), not the point where we begin dragging.
                    // For example, this could be different if one of our children intercepts the
                    // gesture first and then we do.
                    requireNotNull(startedPosition),
                    overSlop,
                    pointersDown,
                )
        val controller = onDragStart(lastPointersInfo, overSlop)

        val successful: Boolean
        try {
@@ -528,8 +529,8 @@ internal class MultiPointerDraggableNode(
                        it.consume()
                    },
                    onIgnoredEvent = {
                            // We are still dragging an object, but this event is not of interest to
                            // the caller.
                        // We are still dragging an object, but this event is not of interest to the
                        // caller.
                        // This event will not trigger the onDrag event, but we will consume the
                        // event to prevent another pointerInput from interrupting the current
                        // gesture just because the event was ignored.
@@ -547,7 +548,6 @@ internal class MultiPointerDraggableNode(
            onDragCancel(controller)
        }
    }
    }

    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
        pass: () -> PointerEventPass
@@ -655,11 +655,46 @@ internal class MultiPointerDraggableNode(
}

internal fun interface PointersInfoOwner {
    fun pointersInfo(): PointersInfo
    /**
     * Provides information about the pointers interacting with this composable.
     *
     * @return A [PointersInfo] object containing details about the pointers, including the starting
     *   position and the number of pointers down, or `null` if there are no pointers down.
     */
    fun pointersInfo(): PointersInfo?
}

/**
 * Holds information about pointer interactions within a composable.
 *
 * This class stores details such as the starting position of a gesture, the number of pointers
 * down, and whether the last pointer event was a mouse wheel scroll.
 *
 * @param startedPosition The starting position of the gesture. This is the position where the first
 *   pointer touched the screen, not necessarily the point where dragging begins. This may be
 *   different from the initial touch position if a child composable intercepts the gesture before
 *   this one.
 * @param pointersDown The number of pointers currently down.
 * @param isMouseWheel Indicates whether the last pointer event was a mouse wheel scroll.
 */
internal data class PointersInfo(
    val startedPosition: Offset?,
    val startedPosition: Offset,
    val pointersDown: Int,
    val isMouseWheel: Boolean,
) {
    init {
        check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" }
    }
}

private fun PointersInfo(
    startedPosition: Offset,
    pointersDown: Int,
    lastPointerEvent: PointerEvent,
): PointersInfo {
    return PointersInfo(
        startedPosition = startedPosition,
        pointersDown = pointersDown,
        isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll,
    )
}
+36 −3
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.input.pointer.PointerEvent
@@ -65,6 +64,40 @@ private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
    return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
}

/**
 * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
 * Prioritizes actions with matching [Swipe.Resolved.fromSource].
 *
 * @param swipe The swipe to match against.
 * @return The best matching [UserActionResult], or `null` if no match is found.
 */
internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
    var bestMatch: UserActionResult? = null
    userActions.forEach { (actionSwipe, actionResult) ->
        if (
            actionSwipe !is Swipe.Resolved ||
                // The direction must match.
                actionSwipe.direction != swipe.direction ||
                // The number of pointers down must match.
                actionSwipe.pointerCount != swipe.pointerCount ||
                // The action requires a specific fromSource.
                (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource)
        ) {
            // This action is not eligible.
            return@forEach
        }

        // Prioritize actions with a matching fromSource.
        if (actionSwipe.fromSource == swipe.fromSource) {
            return actionResult
        }

        // Otherwise, keep track of the best eligible action.
        bestMatch = actionResult
    }
    return bestMatch
}

private data class SwipeToSceneElement(
    val draggableHandler: DraggableHandlerImpl,
    val swipeDetector: SwipeDetector,
@@ -155,10 +188,10 @@ private class SwipeToSceneNode(

    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()

    private fun startDragImmediately(startedPosition: Offset): Boolean {
    private fun startDragImmediately(pointersInfo: PointersInfo): Boolean {
        // Immediately start the drag if the user can't swipe in the other direction and the gesture
        // handler can intercept it.
        return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(startedPosition)
        return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersInfo)
    }

    private fun canOppositeSwipe(): Boolean {
+55 −63

File changed.

Preview size limit exceeded, changes collapsed.

+9 −9

File changed.

Preview size limit exceeded, changes collapsed.