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

Commit 538a83b3 authored by Omar Miatello's avatar Omar Miatello Committed by Android (Google) Code Review
Browse files

Merge changes I753e3859,I7da1b608 into main

* changes:
  STL Added support for pointerType in Swipe definition
  STL introduce Content.findActionResultBestMatch(swipe)
parents 7c65d920 8a88fe39
Loading
Loading
Loading
Loading
+65 −105
Original line number Diff line number Diff line
@@ -36,12 +36,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
}

/**
@@ -96,7 +95,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) {
@@ -107,7 +106,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
@@ -124,11 +123,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) {
@@ -153,7 +148,7 @@ internal class DraggableHandlerImpl(
            return updateDragController(swipes, swipeAnimation)
        }

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

        swipes.updateSwipesResults(fromContent)
@@ -190,8 +185,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(),
@@ -200,10 +194,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 =
@@ -222,37 +226,14 @@ internal class DraggableHandlerImpl(
                        SwipeDirection.Resolved.Down
                    }
            },
            pointerCount = pointersDown,
        // If the number of pointers is not specified, 1 is assumed.
        pointerCount = pointersInfo?.pointersDown ?: 1,
        // Resolves the pointer type only if all pointers are of the same type.
        pointersType = pointersInfo?.pointersDownByType?.keys?.singleOrNull(),
        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,
@@ -498,24 +479,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
    }

@@ -569,11 +540,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) },
        )
    }

@@ -582,12 +555,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
@@ -595,17 +563,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
@@ -619,13 +581,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
@@ -636,13 +599,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
@@ -662,11 +623,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) {
@@ -704,11 +665,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) {
@@ -718,12 +679,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,
@@ -742,7 +702,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
            }
+115 −67
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
@@ -52,6 +53,7 @@ import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastSumBy
import com.android.compose.ui.util.SpaceVectorConverter
import kotlin.coroutines.cancellation.CancellationException
@@ -78,8 +80,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 +99,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 +126,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 +183,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 +217,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 +290,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 +440,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 +466,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 +497,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 +516,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 +531,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 +550,6 @@ internal class MultiPointerDraggableNode(
            onDragCancel(controller)
        }
    }
    }

    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
        pass: () -> PointerEventPass
@@ -655,11 +657,57 @@ 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.
 * @param pointersDownByType Provide a map of pointer types to the count of pointers of that type
 *   currently down/pressed.
 */
internal data class PointersInfo(
    val startedPosition: Offset?,
    val startedPosition: Offset,
    val pointersDown: Int,
    val isMouseWheel: Boolean,
    val pointersDownByType: Map<PointerType, Int>,
) {
    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,
        pointersDownByType =
            buildMap {
                lastPointerEvent.changes.fastForEach { change ->
                    if (!change.pressed) return@fastForEach
                    val newValue = (get(change.type) ?: 0) + 1
                    put(change.type, newValue)
                }
            },
    )
}
+4 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ 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.input.pointer.PointerType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
@@ -407,6 +408,7 @@ data object Back : UserAction() {
data class Swipe(
    val direction: SwipeDirection,
    val pointerCount: Int = 1,
    val pointersType: PointerType? = null,
    val fromSource: SwipeSource? = null,
) : UserAction() {
    companion object {
@@ -422,6 +424,7 @@ data class Swipe(
        return Resolved(
            direction = direction.resolve(layoutDirection),
            pointerCount = pointerCount,
            pointersType = pointersType,
            fromSource = fromSource?.resolve(layoutDirection),
        )
    }
@@ -431,6 +434,7 @@ data class Swipe(
        val direction: SwipeDirection.Resolved,
        val pointerCount: Int,
        val fromSource: SwipeSource.Resolved?,
        val pointersType: PointerType?,
    ) : UserAction.Resolved()
}

+48 −3

File changed.

Preview size limit exceeded, changes collapsed.

+57 −63

File changed.

Preview size limit exceeded, changes collapsed.

Loading