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

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

Merge changes I907b231f,I6a0ac353 into main

* changes:
  Avoiding conflicts with multiple MultiPointerDraggables
  Refactor detectDragGestures in MultiPointerDraggable
parents 64d7096b a93c960d
Loading
Loading
Loading
Loading
+119 −20
Original line number Diff line number Diff line
@@ -19,8 +19,6 @@ package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.gestures.verticalDrag
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -32,7 +30,9 @@ import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -46,6 +46,8 @@ 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
import kotlin.math.sign
@@ -236,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 =
@@ -297,18 +314,22 @@ internal class MultiPointerDraggableNode(
                onDrag(controller, drag, overSlop)

                successful =
                    when (orientation) {
                        Orientation.Horizontal ->
                            horizontalDrag(drag.id) {
                    drag(
                        initialPointerId = drag.id,
                        hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f },
                        onDrag = {
                            onDrag(controller, it, it.positionChange().toFloat())
                            it.consume()
                            }
                        Orientation.Vertical ->
                            verticalDrag(drag.id) {
                                onDrag(controller, it, it.positionChange().toFloat())
                        },
                        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)
                throw t
@@ -322,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 {
@@ -337,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
@@ -352,4 +373,82 @@ internal class MultiPointerDraggableNode(
            Orientation.Horizontal -> x
        }
    }

    /**
     * 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.
     *
     * 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.
     *
     * Note: Inspired by DragGestureDetector.kt
     */
    private suspend inline fun AwaitPointerEventScope.drag(
        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
        if (isPointerUp) {
            return false // The pointer has already been lifted, so the gesture is canceled
        }
        var pointerId = initialPointerId
        while (true) {
            val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false

            if (change.isConsumed) {
                return false
            }

            if (change.changedToUpIgnoreConsumed()) {
                return true
            }

            onDrag(change)
            pointerId = change.id
        }
    }

    /**
     * Waits for a single drag in one axis, final pointer up, or all pointers are up. When
     * [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`. 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: Inspired by DragGestureDetector.kt
     */
    private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
        initialPointerId: PointerId,
        hasDragged: (PointerInputChange) -> Boolean,
        onIgnoredEvent: (PointerInputChange) -> Unit,
    ): PointerInputChange? {
        var pointerId = initialPointerId
        while (true) {
            val event = awaitPointerEvent()
            val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null
            if (dragEvent.changedToUpIgnoreConsumed()) {
                val otherDown = event.changes.fastFirstOrNull { it.pressed }
                if (otherDown == null) {
                    // This is the last "up"
                    return dragEvent
                } else {
                    pointerId = otherDown.id
                }
            } 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