1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compose.animation.scene
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
21 import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.geometry.Offset
25 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
26 import androidx.compose.ui.input.pointer.PointerEvent
27 import androidx.compose.ui.input.pointer.PointerEventPass
28 import androidx.compose.ui.input.pointer.PointerId
29 import androidx.compose.ui.input.pointer.PointerInputChange
30 import androidx.compose.ui.input.pointer.PointerInputScope
31 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
32 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
33 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
34 import androidx.compose.ui.input.pointer.positionChange
35 import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
36 import androidx.compose.ui.input.pointer.util.VelocityTracker
37 import androidx.compose.ui.input.pointer.util.addPointerInputChange
38 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
39 import androidx.compose.ui.node.DelegatingNode
40 import androidx.compose.ui.node.ModifierNodeElement
41 import androidx.compose.ui.node.ObserverModifierNode
42 import androidx.compose.ui.node.PointerInputModifierNode
43 import androidx.compose.ui.node.currentValueOf
44 import androidx.compose.ui.node.observeReads
45 import androidx.compose.ui.platform.LocalViewConfiguration
46 import androidx.compose.ui.unit.IntSize
47 import androidx.compose.ui.unit.Velocity
48 import androidx.compose.ui.util.fastAll
49 import androidx.compose.ui.util.fastAny
50 import androidx.compose.ui.util.fastFirstOrNull
51 import androidx.compose.ui.util.fastForEach
52 import kotlin.coroutines.cancellation.CancellationException
53 import kotlin.math.sign
54 import kotlinx.coroutines.coroutineScope
55 import kotlinx.coroutines.isActive
56 
57 /**
58  * Make an element draggable in the given [orientation].
59  *
60  * The main difference with [multiPointerDraggable] and
61  * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
62  * of pointers that are down when the drag is started. If you don't need this information, you
63  * should use `draggable` instead.
64  *
65  * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
66  * pointer, then we count the number of distinct pointers that are down right before calling
67  * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
68  * dragged) and a second pointer is down and dragged. This is an implementation detail that might
69  * change in the future.
70  */
71 @Stable
72 internal fun Modifier.multiPointerDraggable(
73     orientation: Orientation,
74     enabled: () -> Boolean,
75     startDragImmediately: (startedPosition: Offset) -> Boolean,
76     onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
77     swipeDetector: SwipeDetector = DefaultSwipeDetector,
78 ): Modifier =
79     this.then(
80         MultiPointerDraggableElement(
81             orientation,
82             enabled,
83             startDragImmediately,
84             onDragStarted,
85             swipeDetector,
86         )
87     )
88 
89 private data class MultiPointerDraggableElement(
90     private val orientation: Orientation,
91     private val enabled: () -> Boolean,
92     private val startDragImmediately: (startedPosition: Offset) -> Boolean,
93     private val onDragStarted:
94         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
95     private val swipeDetector: SwipeDetector,
96 ) : ModifierNodeElement<MultiPointerDraggableNode>() {
97     override fun create(): MultiPointerDraggableNode =
98         MultiPointerDraggableNode(
99             orientation = orientation,
100             enabled = enabled,
101             startDragImmediately = startDragImmediately,
102             onDragStarted = onDragStarted,
103             swipeDetector = swipeDetector,
104         )
105 
106     override fun update(node: MultiPointerDraggableNode) {
107         node.orientation = orientation
108         node.enabled = enabled
109         node.startDragImmediately = startDragImmediately
110         node.onDragStarted = onDragStarted
111         node.swipeDetector = swipeDetector
112     }
113 }
114 
115 internal class MultiPointerDraggableNode(
116     orientation: Orientation,
117     enabled: () -> Boolean,
118     var startDragImmediately: (startedPosition: Offset) -> Boolean,
119     var onDragStarted:
120         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
121     var swipeDetector: SwipeDetector = DefaultSwipeDetector,
122 ) :
123     PointerInputModifierNode,
124     DelegatingNode(),
125     CompositionLocalConsumerModifierNode,
126     ObserverModifierNode {
<lambda>null127     private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
128     private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
129     private val velocityTracker = VelocityTracker()
130     private var previousEnabled: Boolean = false
131 
132     var enabled: () -> Boolean = enabled
133         set(value) {
134             // Reset the pointer input whenever enabled changed.
135             if (value != field) {
136                 field = value
137                 delegate.resetPointerInputHandler()
138             }
139         }
140 
141     var orientation: Orientation = orientation
142         set(value) {
143             // Reset the pointer input whenever orientation changed.
144             if (value != field) {
145                 field = value
146                 delegate.resetPointerInputHandler()
147             }
148         }
149 
onAttachnull150     override fun onAttach() {
151         previousEnabled = enabled()
152         onObservedReadsChanged()
153     }
154 
onObservedReadsChangednull155     override fun onObservedReadsChanged() {
156         observeReads {
157             val newEnabled = enabled()
158             if (newEnabled != previousEnabled) {
159                 delegate.resetPointerInputHandler()
160             }
161             previousEnabled = newEnabled
162         }
163     }
164 
onCancelPointerInputnull165     override fun onCancelPointerInput() = delegate.onCancelPointerInput()
166 
167     override fun onPointerEvent(
168         pointerEvent: PointerEvent,
169         pass: PointerEventPass,
170         bounds: IntSize
171     ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
172 
173     private suspend fun PointerInputScope.pointerInput() {
174         if (!enabled()) {
175             return
176         }
177 
178         coroutineScope {
179             awaitPointerEventScope {
180                 while (isActive) {
181                     try {
182                         detectDragGestures(
183                             orientation = orientation,
184                             startDragImmediately = startDragImmediately,
185                             onDragStart = { startedPosition, overSlop, pointersDown ->
186                                 velocityTracker.resetTracking()
187                                 onDragStarted(startedPosition, overSlop, pointersDown)
188                             },
189                             onDrag = { controller, change, amount ->
190                                 velocityTracker.addPointerInputChange(change)
191                                 controller.onDrag(amount)
192                             },
193                             onDragEnd = { controller ->
194                                 val viewConfiguration = currentValueOf(LocalViewConfiguration)
195                                 val maxVelocity =
196                                     viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) }
197                                 val velocity = velocityTracker.calculateVelocity(maxVelocity)
198                                 controller.onStop(
199                                     velocity =
200                                         when (orientation) {
201                                             Orientation.Horizontal -> velocity.x
202                                             Orientation.Vertical -> velocity.y
203                                         },
204                                     canChangeScene = true,
205                                 )
206                             },
207                             onDragCancel = { controller ->
208                                 controller.onStop(velocity = 0f, canChangeScene = true)
209                             },
210                             swipeDetector = swipeDetector
211                         )
212                     } catch (exception: CancellationException) {
213                         // If the coroutine scope is active, we can just restart the drag cycle.
214                         if (!isActive) {
215                             throw exception
216                         }
217                     }
218                 }
219             }
220         }
221     }
222 
223     /**
224      * Detect drag gestures in the given [orientation].
225      *
226      * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
227      * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
228      * 1) starting the gesture immediately without requiring a drag >= touch slope;
229      * 2) passing the number of pointers down to [onDragStart].
230      */
detectDragGesturesnull231     private suspend fun AwaitPointerEventScope.detectDragGestures(
232         orientation: Orientation,
233         startDragImmediately: (startedPosition: Offset) -> Boolean,
234         onDragStart:
235             (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
236         onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit,
237         onDragEnd: (controller: DragController) -> Unit,
238         onDragCancel: (controller: DragController) -> Unit,
239         swipeDetector: SwipeDetector,
240     ) {
241         val consumablePointer =
242             awaitConsumableEvent {
243                     // We are searching for an event that can be used as the starting point for the
244                     // drag gesture. Our options are:
245                     // - Initial: These events should never be consumed by the MultiPointerDraggable
246                     //   since our ancestors can consume the gesture, but we would eliminate this
247                     //   possibility for our descendants.
248                     // - Main: These events are consumed during the drag gesture, and they are a
249                     //   good place to start if the previous event has not been consumed.
250                     // - Final: If the previous event has been consumed, we can wait for the Main
251                     //   pass to finish. If none of our ancestors were interested in the event, we
252                     //   can wait for an unconsumed event in the Final pass.
253                     val previousConsumed = currentEvent.changes.fastAny { it.isConsumed }
254                     if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main
255                 }
256                 .changes
257                 .first()
258 
259         var overSlop = 0f
260         val drag =
261             if (startDragImmediately(consumablePointer.position)) {
262                 consumablePointer.consume()
263                 consumablePointer
264             } else {
265                 val onSlopReached = { change: PointerInputChange, over: Float ->
266                     if (swipeDetector.detectSwipe(change)) {
267                         change.consume()
268                         overSlop = over
269                     }
270                 }
271 
272                 // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it
273                 // is public.
274                 val drag =
275                     when (orientation) {
276                         Orientation.Horizontal ->
277                             awaitHorizontalTouchSlopOrCancellation(
278                                 consumablePointer.id,
279                                 onSlopReached
280                             )
281                         Orientation.Vertical ->
282                             awaitVerticalTouchSlopOrCancellation(
283                                 consumablePointer.id,
284                                 onSlopReached
285                             )
286                     }
287 
288                 // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
289                 // the touch slop. However, the overSlop we pass to onDragStarted() is used to
290                 // compute the direction we are dragging in, so overSlop should never be 0f unless
291                 // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned
292                 // true).
293                 if (drag != null && overSlop == 0f) {
294                     val delta = (drag.position - consumablePointer.position).toFloat()
295                     check(delta != 0f) { "delta is equal to 0" }
296                     overSlop = delta.sign
297                 }
298                 drag
299             }
300 
301         if (drag != null) {
302             // Count the number of pressed pointers.
303             val pressed = mutableSetOf<PointerId>()
304             currentEvent.changes.fastForEach { change ->
305                 if (change.pressed) {
306                     pressed.add(change.id)
307                 }
308             }
309 
310             val controller = onDragStart(drag.position, overSlop, pressed.size)
311 
312             val successful: Boolean
313             try {
314                 onDrag(controller, drag, overSlop)
315 
316                 successful =
317                     drag(
318                         initialPointerId = drag.id,
319                         hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f },
320                         onDrag = {
321                             onDrag(controller, it, it.positionChange().toFloat())
322                             it.consume()
323                         },
324                         onIgnoredEvent = {
325                             // We are still dragging an object, but this event is not of interest to
326                             // the caller.
327                             // This event will not trigger the onDrag event, but we will consume the
328                             // event to prevent another pointerInput from interrupting the current
329                             // gesture just because the event was ignored.
330                             it.consume()
331                         },
332                     )
333             } catch (t: Throwable) {
334                 onDragCancel(controller)
335                 throw t
336             }
337 
338             if (successful) {
339                 onDragEnd(controller)
340             } else {
341                 onDragCancel(controller)
342             }
343         }
344     }
345 
awaitConsumableEventnull346     private suspend fun AwaitPointerEventScope.awaitConsumableEvent(
347         pass: () -> PointerEventPass,
348     ): PointerEvent {
349         fun canBeConsumed(changes: List<PointerInputChange>): Boolean {
350             // All pointers must be:
351             return changes.fastAll {
352                 // A) recently pressed: even if the event has already been consumed, we can still
353                 // use the recently added finger event to determine whether to initiate dragging the
354                 // scene.
355                 it.changedToDownIgnoreConsumed() ||
356                     // B) unconsumed AND in a new position (on the current axis)
357                     it.positionChange().toFloat() != 0f
358             }
359         }
360 
361         var event: PointerEvent
362         do {
363             event = awaitPointerEvent(pass = pass())
364         } while (!canBeConsumed(event.changes))
365 
366         // We found a consumable event in the Main pass
367         return event
368     }
369 
toFloatnull370     private fun Offset.toFloat(): Float {
371         return when (orientation) {
372             Orientation.Vertical -> y
373             Orientation.Horizontal -> x
374         }
375     }
376 
377     /**
378      * Continues to read drag events until all pointers are up or the drag event is canceled. The
379      * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the
380      * result whether a change was detected from the drag function or not.
381      *
382      * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise,
383      * [onIgnoredEvent] is called.
384      *
385      * @return true when gesture ended with all pointers up and false when the gesture was canceled.
386      *
387      * Note: Inspired by DragGestureDetector.kt
388      */
dragnull389     private suspend inline fun AwaitPointerEventScope.drag(
390         initialPointerId: PointerId,
391         hasDragged: (PointerInputChange) -> Boolean,
392         onDrag: (PointerInputChange) -> Unit,
393         onIgnoredEvent: (PointerInputChange) -> Unit,
394     ): Boolean {
395         val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId }
396         val isPointerUp = pointer?.pressed != true
397         if (isPointerUp) {
398             return false // The pointer has already been lifted, so the gesture is canceled
399         }
400         var pointerId = initialPointerId
401         while (true) {
402             val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false
403 
404             if (change.isConsumed) {
405                 return false
406             }
407 
408             if (change.changedToUpIgnoreConsumed()) {
409                 return true
410             }
411 
412             onDrag(change)
413             pointerId = change.id
414         }
415     }
416 
417     /**
418      * Waits for a single drag in one axis, final pointer up, or all pointers are up. When
419      * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger
420      * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned.
421      * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when
422      * [hasDragged] returns `true`. Events that should not be captured are passed to
423      * [onIgnoredEvent].
424      *
425      * `null` is returned if there was an error in the pointer input stream and the pointer that was
426      * down was dropped before the 'up' was received.
427      *
428      * Note: Inspired by DragGestureDetector.kt
429      */
awaitDragOrUpnull430     private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
431         initialPointerId: PointerId,
432         hasDragged: (PointerInputChange) -> Boolean,
433         onIgnoredEvent: (PointerInputChange) -> Unit,
434     ): PointerInputChange? {
435         var pointerId = initialPointerId
436         while (true) {
437             val event = awaitPointerEvent()
438             val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null
439             if (dragEvent.changedToUpIgnoreConsumed()) {
440                 val otherDown = event.changes.fastFirstOrNull { it.pressed }
441                 if (otherDown == null) {
442                     // This is the last "up"
443                     return dragEvent
444                 } else {
445                     pointerId = otherDown.id
446                 }
447             } else if (hasDragged(dragEvent)) {
448                 return dragEvent
449             } else {
450                 onIgnoredEvent(dragEvent)
451             }
452         }
453     }
454 }
455