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

Commit a93c960d authored by omarmt's avatar omarmt
Browse files

Avoiding conflicts with multiple MultiPointerDraggables

Two adjustments were necessary to accomplish this:
- Initial event: During the Final phase, if the previous event has been
consumed, the event that enables the drag gesture to begin is captured.
- Dragging phase: Even the events that are not sent to onDrag are
consumed because they are part of the same gesture.

## Initial event
We are searching for an event that can be used as the starting point for
 the drag gesture.

Our options are:
- Initial: These events should never be consumed by the
MultiPointerDraggable since our ancestors can consume the gesture, but
we would eliminate this possibility for our descendants.
- Main: These events are consumed during the drag gesture, and they are
a good place to start if the previous event has not been consumed.
- Final: If the previous event has been consumed, we can wait for the
Main pass to finish. If none of our ancestors were interested in the
event, we can wait for an unconsumed event in the Final pass.

## Dragging phase
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.

Test: atest MultiPointerDraggableTest
Bug: 345434452
Flag: com.android.systemui.scene_container
Change-Id: I907b231f1fd9af4a9eb744a158a298cd694586c9
parent 1898c592
Loading
Loading
Loading
Loading
+42 −11
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import kotlin.coroutines.cancellation.CancellationException
@@ -237,8 +238,23 @@ internal class MultiPointerDraggableNode(
        onDragCancel: (controller: DragController) -> Unit,
        swipeDetector: SwipeDetector,
    ) {
        // Wait for a consumable event in [PointerEventPass.Main] pass
        val consumablePointer = awaitConsumableEvent().changes.first()
        val consumablePointer =
            awaitConsumableEvent {
                    // We are searching for an event that can be used as the starting point for the
                    // drag gesture. Our options are:
                    // - Initial: These events should never be consumed by the MultiPointerDraggable
                    //   since our ancestors can consume the gesture, but we would eliminate this
                    //   possibility for our descendants.
                    // - Main: These events are consumed during the drag gesture, and they are a
                    //   good place to start if the previous event has not been consumed.
                    // - Final: If the previous event has been consumed, we can wait for the Main
                    //   pass to finish. If none of our ancestors were interested in the event, we
                    //   can wait for an unconsumed event in the Final pass.
                    val previousConsumed = currentEvent.changes.fastAny { it.isConsumed }
                    if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main
                }
                .changes
                .first()

        var overSlop = 0f
        val drag =
@@ -305,6 +321,14 @@ internal class MultiPointerDraggableNode(
                            onDrag(controller, it, it.positionChange().toFloat())
                            it.consume()
                        },
                        onIgnoredEvent = {
                            // 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.
                            it.consume()
                        },
                    )
            } catch (t: Throwable) {
                onDragCancel(controller)
@@ -319,7 +343,9 @@ internal class MultiPointerDraggableNode(
        }
    }

    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(): PointerEvent {
    private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
        pass: () -> PointerEventPass,
    ): PointerEvent {
        fun canBeConsumed(changes: List<PointerInputChange>): Boolean {
            // All pointers must be:
            return changes.fastAll {
@@ -334,9 +360,7 @@ internal class MultiPointerDraggableNode(

        var event: PointerEvent
        do {
            // To allow the descendants with the opportunity to consume the event, we wait for it in
            // the Main pass.
            event = awaitPointerEvent()
            event = awaitPointerEvent(pass = pass())
        } while (!canBeConsumed(event.changes))

        // We found a consumable event in the Main pass
@@ -353,8 +377,10 @@ internal class MultiPointerDraggableNode(
    /**
     * Continues to read drag events until all pointers are up or the drag event is canceled. The
     * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the
     * result whether a change was detected from the drag function or not. [onDrag] is called
     * whenever the pointer moves and [hasDragged] returns non-zero.
     * result whether a change was detected from the drag function or not.
     *
     * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise,
     * [onIgnoredEvent] is called.
     *
     * @return true when gesture ended with all pointers up and false when the gesture was canceled.
     *
@@ -364,6 +390,7 @@ internal class MultiPointerDraggableNode(
        initialPointerId: PointerId,
        hasDragged: (PointerInputChange) -> Boolean,
        onDrag: (PointerInputChange) -> Unit,
        onIgnoredEvent: (PointerInputChange) -> Unit,
    ): Boolean {
        val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId }
        val isPointerUp = pointer?.pressed != true
@@ -372,7 +399,7 @@ internal class MultiPointerDraggableNode(
        }
        var pointerId = initialPointerId
        while (true) {
            val change = awaitDragOrUp(pointerId, hasDragged) ?: return false
            val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false

            if (change.isConsumed) {
                return false
@@ -392,16 +419,18 @@ internal class MultiPointerDraggableNode(
     * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger
     * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned.
     * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when
     * [hasDragged] returns `true`.
     * [hasDragged] returns `true`. Events that should not be captured are passed to
     * [onIgnoredEvent].
     *
     * `null` is returned if there was an error in the pointer input stream and the pointer that was
     * down was dropped before the 'up' was received.
     *
     * Note: Copied from DragGestureDetector.kt
     * Note: Inspired by DragGestureDetector.kt
     */
    private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
        initialPointerId: PointerId,
        hasDragged: (PointerInputChange) -> Boolean,
        onIgnoredEvent: (PointerInputChange) -> Unit,
    ): PointerInputChange? {
        var pointerId = initialPointerId
        while (true) {
@@ -417,6 +446,8 @@ internal class MultiPointerDraggableNode(
                }
            } else if (hasDragged(dragEvent)) {
                return dragEvent
            } else {
                onIgnoredEvent(dragEvent)
            }
        }
    }
+115 −0
Original line number Diff line number Diff line
@@ -348,6 +348,121 @@ class MultiPointerDraggableTest {
        assertThat(stopped).isTrue()
    }

    @Test
    fun multiPointerDuringAnotherGestureWaitAConsumableEventAfterMainPass() {
        val size = 200f
        val middle = Offset(size / 2f, size / 2f)

        var verticalStarted = false
        var verticalDragged = false
        var verticalStopped = false
        var horizontalStarted = false
        var horizontalDragged = false
        var horizontalStopped = false

        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            Box(
                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            verticalStarted = true
                            object : DragController {
                                override fun onDrag(delta: Float) {
                                    verticalDragged = true
                                }

                                override fun onStop(velocity: Float, canChangeScene: Boolean) {
                                    verticalStopped = true
                                }
                            }
                        },
                    )
                    .multiPointerDraggable(
                        orientation = Orientation.Horizontal,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            horizontalStarted = true
                            object : DragController {
                                override fun onDrag(delta: Float) {
                                    horizontalDragged = true
                                }

                                override fun onStop(velocity: Float, canChangeScene: Boolean) {
                                    horizontalStopped = true
                                }
                            }
                        },
                    )
            )
        }

        fun startDraggingDown() {
            rule.onRoot().performTouchInput {
                down(middle)
                moveBy(Offset(0f, touchSlop))
            }
        }

        fun startDraggingRight() {
            rule.onRoot().performTouchInput {
                down(middle)
                moveBy(Offset(touchSlop, 0f))
            }
        }

        fun stopDragging() {
            rule.onRoot().performTouchInput { up() }
        }

        fun continueDown() {
            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
        }

        fun continueRight() {
            rule.onRoot().performTouchInput { moveBy(Offset(touchSlop, 0f)) }
        }

        startDraggingDown()
        assertThat(verticalStarted).isTrue()
        assertThat(verticalDragged).isTrue()
        assertThat(verticalStopped).isFalse()

        // Ignore right swipe, do not interrupt the dragging gesture.
        continueRight()
        assertThat(horizontalStarted).isFalse()
        assertThat(horizontalDragged).isFalse()
        assertThat(horizontalStopped).isFalse()
        assertThat(verticalStopped).isFalse()

        stopDragging()
        assertThat(verticalStopped).isTrue()

        verticalStarted = false
        verticalDragged = false
        verticalStopped = false

        startDraggingRight()
        assertThat(horizontalStarted).isTrue()
        assertThat(horizontalDragged).isTrue()
        assertThat(horizontalStopped).isFalse()

        // Ignore down swipe, do not interrupt the dragging gesture.
        continueDown()
        assertThat(verticalStarted).isFalse()
        assertThat(verticalDragged).isFalse()
        assertThat(verticalStopped).isFalse()
        assertThat(horizontalStopped).isFalse()

        stopDragging()
        assertThat(horizontalStopped).isTrue()
    }

    @Test
    fun multiPointerSwipeDetectorInteraction() {
        val size = 200f