summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author omarmt <omarmt@google.com> 2024-06-06 16:28:21 +0000
committer omarmt <omarmt@google.com> 2024-06-07 16:08:35 +0000
commita93c960d61256882d74ed7b03f671d8bc2de9af2 (patch)
tree13334fa9c1e67693b1aac64677a29485cbb5f568
parent1898c59269d4d8ce7a03d7131638e597e1cf5a1a (diff)
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
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt53
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt115
2 files changed, 157 insertions, 11 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 780af77c20c7..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
@@ -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)
}
}
}
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)