diff options
| author | 2024-06-11 13:20:01 +0000 | |
|---|---|---|
| committer | 2024-06-11 13:20:01 +0000 | |
| commit | 3e64fa40198f8ccfbbda36e8d79ddd00b53dfb15 (patch) | |
| tree | abf38f92d31ff58d11df91eff98df230cc5ce821 | |
| parent | 64d7096bd1f4d107383a9fc42e3d6f8a46aea664 (diff) | |
| parent | a93c960d61256882d74ed7b03f671d8bc2de9af2 (diff) | |
Merge changes I907b231f,I6a0ac353 into main
* changes:
Avoiding conflicts with multiple MultiPointerDraggables
Refactor detectDragGestures in MultiPointerDraggable
2 files changed, 234 insertions, 20 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 3cc8431cd87e..6001f1fd6db0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -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) { - onDrag(controller, it, it.positionChange().toFloat()) - it.consume() - } - Orientation.Vertical -> - verticalDrag(drag.id) { - onDrag(controller, it, it.positionChange().toFloat()) - it.consume() - } - } + drag( + initialPointerId = drag.id, + hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f }, + onDrag = { + 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) 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) + } + } + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index 4bb643f8b89e..1a0740b54892 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -349,6 +349,121 @@ class MultiPointerDraggableTest { } @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 val middle = Offset(size / 2f, size / 2f) |