Support for multiple gestures at once
To ensure that there is only one source in control of the scene at a
time (using the takePriority method).
In this case, the drag and stop methods called by other sources will be
ignored.
For example, this could happen when using the draggable modifier (with
startDragImmediately set to true) and nestedScroll.
In this case, the sequence of events received by our
SceneGestureHandler is as follows:
- draggable: start
- draggable: drag
- nestedScroll: start
- nestedScroll: drag
- draggable: stop // <-- this event should be ignored
- nestedScroll: drag
- nestedScroll: drag
- nestedScroll: drag
- ...
- nestedScroll: stop
Test: atest SceneGestureHandlerTest
Bug: 291025415
Change-Id: I74fdc27e6b7786cd425254f84df07615afa4e6a1
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index d70a248..2dc53ab 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -157,6 +157,8 @@
*/
private val positionalThreshold = with(layoutImpl.density) { 56.dp.toPx() }
+ internal var gestureWithPriority: Any? = null
+
internal fun onDragStarted() {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
@@ -525,15 +527,21 @@
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
+ gestureHandler.gestureWithPriority = this
gestureHandler.onDragStarted()
}
override fun onDelta(pixels: Float) {
- gestureHandler.onDrag(delta = pixels)
+ if (gestureHandler.gestureWithPriority == this) {
+ gestureHandler.onDrag(delta = pixels)
+ }
}
override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
- gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
+ if (gestureHandler.gestureWithPriority == this) {
+ gestureHandler.gestureWithPriority = null
+ gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
+ }
}
}
@@ -615,10 +623,15 @@
},
canContinueScroll = { priorityScene == gestureHandler.swipeTransitionToScene.key },
onStart = {
+ gestureHandler.gestureWithPriority = this
priorityScene = nextScene
gestureHandler.onDragStarted()
},
onScroll = { offsetAvailable ->
+ if (gestureHandler.gestureWithPriority != this) {
+ return@PriorityNestedScrollConnection Offset.Zero
+ }
+
val amount = offsetAvailable.toAmount()
// TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
@@ -628,6 +641,10 @@
amount.toOffset()
},
onStop = { velocityAvailable ->
+ if (gestureHandler.gestureWithPriority != this) {
+ return@PriorityNestedScrollConnection Velocity.Zero
+ }
+
priorityScene = null
gestureHandler.onDragStopped(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 9b9e70d..6791a85 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -312,4 +312,52 @@
advanceUntilIdle()
assertScene(currentScene = SceneA, isIdle = true)
}
+
+ @Test
+ fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest {
+ draggable.onDelta(deltaInPixels10)
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+ @Test
+ fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
+ draggable.onDragStopped(coroutineScope, velocityThreshold)
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+
+ @Test
+ fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
+ nestedScroll.onPreFling(Velocity(0f, velocityThreshold))
+ assertScene(currentScene = SceneA, isIdle = true)
+ }
+
+ @Test
+ fun startNestedScrollWhileDragging() = runGestureTest {
+ draggable.onDragStarted(coroutineScope, Offset.Zero)
+ assertScene(currentScene = SceneA, isIdle = false)
+ val transition = transitionState as Transition
+
+ draggable.onDelta(deltaInPixels10)
+ assertThat(transition.progress).isEqualTo(0.1f)
+
+ // now we can intercept the scroll events
+ nestedScrollEvents(available = offsetY10)
+ assertThat(transition.progress).isEqualTo(0.2f)
+
+ // this should be ignored, we are scrolling now!
+ draggable.onDragStopped(coroutineScope, velocityThreshold)
+ assertScene(currentScene = SceneA, isIdle = false)
+
+ nestedScrollEvents(available = offsetY10)
+ assertThat(transition.progress).isEqualTo(0.3f)
+
+ nestedScrollEvents(available = offsetY10)
+ assertThat(transition.progress).isEqualTo(0.4f)
+
+ nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
+ assertScene(currentScene = SceneC, isIdle = false)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertScene(currentScene = SceneC, isIdle = true)
+ }
}