1 /*
<lambda>null2  * Copyright 2019 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 androidx.compose.foundation.gestures
18 
19 import androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.MutatePriority
21 import androidx.compose.foundation.MutatorMutex
22 import androidx.compose.foundation.gestures.DragEvent.DragCancelled
23 import androidx.compose.foundation.gestures.DragEvent.DragDelta
24 import androidx.compose.foundation.gestures.DragEvent.DragStarted
25 import androidx.compose.foundation.gestures.DragEvent.DragStopped
26 import androidx.compose.foundation.interaction.DragInteraction
27 import androidx.compose.foundation.interaction.MutableInteractionSource
28 import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.Stable
31 import androidx.compose.runtime.remember
32 import androidx.compose.runtime.rememberUpdatedState
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.geometry.Offset
35 import androidx.compose.ui.input.pointer.PointerEvent
36 import androidx.compose.ui.input.pointer.PointerEventPass
37 import androidx.compose.ui.input.pointer.PointerInputChange
38 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
39 import androidx.compose.ui.input.pointer.pointerInput
40 import androidx.compose.ui.input.pointer.util.VelocityTracker
41 import androidx.compose.ui.input.pointer.util.addPointerInputChange
42 import androidx.compose.ui.node.DelegatingNode
43 import androidx.compose.ui.node.ModifierNodeElement
44 import androidx.compose.ui.node.PointerInputModifierNode
45 import androidx.compose.ui.platform.InspectorInfo
46 import androidx.compose.ui.unit.IntSize
47 import androidx.compose.ui.unit.Velocity
48 import kotlin.coroutines.cancellation.CancellationException
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.CoroutineStart
51 import kotlinx.coroutines.channels.Channel
52 import kotlinx.coroutines.coroutineScope
53 import kotlinx.coroutines.isActive
54 import kotlinx.coroutines.launch
55 
56 /**
57  * State of [draggable]. Allows for a granular control of how deltas are consumed by the user as
58  * well as to write custom drag methods using [drag] suspend function.
59  */
60 @JvmDefaultWithCompatibility
61 interface DraggableState {
62     /**
63      * Call this function to take control of drag logic.
64      *
65      * All actions that change the logical drag position must be performed within a [drag] block
66      * (even if they don't call any other methods on this object) in order to guarantee that mutual
67      * exclusion is enforced.
68      *
69      * If [drag] is called from elsewhere with the [dragPriority] higher or equal to ongoing drag,
70      * ongoing drag will be canceled.
71      *
72      * @param dragPriority of the drag operation
73      * @param block to perform drag in
74      */
75     suspend fun drag(
76         dragPriority: MutatePriority = MutatePriority.Default,
77         block: suspend DragScope.() -> Unit
78     )
79 
80     /**
81      * Dispatch drag delta in pixels avoiding all drag related priority mechanisms.
82      *
83      * **NOTE:** unlike [drag], dispatching any delta with this method will bypass scrolling of any
84      * priority. This method will also ignore `reverseDirection` and other parameters set in
85      * [draggable].
86      *
87      * This method is used internally for low level operations, allowing implementers of
88      * [DraggableState] influence the consumption as suits them, e.g. introduce nested scrolling.
89      * Manually dispatching delta via this method will likely result in a bad user experience, you
90      * must prefer [drag] method over this one.
91      *
92      * @param delta amount of scroll dispatched in the nested drag process
93      */
94     fun dispatchRawDelta(delta: Float)
95 }
96 
97 /** Scope used for suspending drag blocks */
98 interface DragScope {
99     /** Attempts to drag by [pixels] px. */
dragBynull100     fun dragBy(pixels: Float)
101 }
102 
103 /**
104  * Default implementation of [DraggableState] interface that allows to pass a simple action that
105  * will be invoked when the drag occurs.
106  *
107  * This is the simplest way to set up a [draggable] modifier. When constructing this
108  * [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever drag
109  * happens (by gesture input or a custom [DraggableState.drag] call) with the delta in pixels.
110  *
111  * If you are creating [DraggableState] in composition, consider using [rememberDraggableState].
112  *
113  * @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels.
114  */
115 fun DraggableState(onDelta: (Float) -> Unit): DraggableState = DefaultDraggableState(onDelta)
116 
117 /**
118  * Create and remember default implementation of [DraggableState] interface that allows to pass a
119  * simple action that will be invoked when the drag occurs.
120  *
121  * This is the simplest way to set up a [draggable] modifier. When constructing this
122  * [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever drag
123  * happens (by gesture input or a custom [DraggableState.drag] call) with the delta in pixels.
124  *
125  * @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels.
126  */
127 @Composable
128 fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState {
129     val onDeltaState = rememberUpdatedState(onDelta)
130     return remember { DraggableState { onDeltaState.value.invoke(it) } }
131 }
132 
133 /**
134  * Configure touch dragging for the UI element in a single [Orientation]. The drag distance reported
135  * to [DraggableState], allowing users to react on the drag delta and update their state.
136  *
137  * The common usecase for this component is when you need to be able to drag something inside the
138  * component on the screen and represent this state via one float value
139  *
140  * If you need to control the whole dragging flow, consider using [pointerInput] instead with the
141  * helper functions like [detectDragGestures].
142  *
143  * If you want to enable dragging in 2 dimensions, consider using [draggable2D].
144  *
145  * If you are implementing scroll/fling behavior, consider using [scrollable].
146  *
147  * @sample androidx.compose.foundation.samples.DraggableSample
148  * @param state [DraggableState] state of the draggable. Defines how drag events will be interpreted
149  *   by the user land logic.
150  * @param orientation orientation of the drag
151  * @param enabled whether or not drag is enabled
152  * @param interactionSource [MutableInteractionSource] that will be used to emit
153  *   [DragInteraction.Start] when this draggable is being dragged.
154  * @param startDragImmediately when set to true, draggable will start dragging immediately and
155  *   prevent other gesture detectors from reacting to "down" events (in order to block composed
156  *   press-based gestures). This is intended to allow end users to "catch" an animating widget by
157  *   pressing on it. It's useful to set it when value you're dragging is settling / animating.
158  * @param onDragStarted callback that will be invoked when drag is about to start at the starting
159  *   position, allowing user to suspend and perform preparation for drag, if desired. This suspend
160  *   function is invoked with the draggable scope, allowing for async processing, if desired. Note
161  *   that the scope used here is the one provided by the draggable node, for long running work that
162  *   needs to outlast the modifier being in the composition you should use a scope that fits the
163  *   lifecycle needed.
164  * @param onDragStopped callback that will be invoked when drag is finished, allowing the user to
165  *   react on velocity and process it. This suspend function is invoked with the draggable scope,
166  *   allowing for async processing, if desired. Note that the scope used here is the one provided by
167  *   the draggable node, for long running work that needs to outlast the modifier being in the
168  *   composition you should use a scope that fits the lifecycle needed.
169  * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave
170  *   like bottom to top and left to right will behave like right to left.
171  */
172 @Stable
draggablenull173 fun Modifier.draggable(
174     state: DraggableState,
175     orientation: Orientation,
176     enabled: Boolean = true,
177     interactionSource: MutableInteractionSource? = null,
178     startDragImmediately: Boolean = false,
179     onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted,
180     onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped,
181     reverseDirection: Boolean = false
182 ): Modifier =
183     this then
184         DraggableElement(
185             state = state,
186             orientation = orientation,
187             enabled = enabled,
188             interactionSource = interactionSource,
189             startDragImmediately = startDragImmediately,
190             onDragStarted = onDragStarted,
191             onDragStopped = onDragStopped,
192             reverseDirection = reverseDirection
193         )
194 
195 internal class DraggableElement(
196     private val state: DraggableState,
197     private val orientation: Orientation,
198     private val enabled: Boolean,
199     private val interactionSource: MutableInteractionSource?,
200     private val startDragImmediately: Boolean,
201     private val onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
202     private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
203     private val reverseDirection: Boolean
204 ) : ModifierNodeElement<DraggableNode>() {
205     override fun create(): DraggableNode =
206         DraggableNode(
207             state,
208             CanDrag,
209             orientation,
210             enabled,
211             interactionSource,
212             startDragImmediately,
213             onDragStarted,
214             onDragStopped,
215             reverseDirection
216         )
217 
218     override fun update(node: DraggableNode) {
219         node.update(
220             state,
221             CanDrag,
222             orientation,
223             enabled,
224             interactionSource,
225             startDragImmediately,
226             onDragStarted,
227             onDragStopped,
228             reverseDirection
229         )
230     }
231 
232     override fun equals(other: Any?): Boolean {
233         if (this === other) return true
234         if (other === null) return false
235         if (this::class != other::class) return false
236 
237         other as DraggableElement
238 
239         if (state != other.state) return false
240         if (orientation != other.orientation) return false
241         if (enabled != other.enabled) return false
242         if (interactionSource != other.interactionSource) return false
243         if (startDragImmediately != other.startDragImmediately) return false
244         if (onDragStarted != other.onDragStarted) return false
245         if (onDragStopped != other.onDragStopped) return false
246         if (reverseDirection != other.reverseDirection) return false
247 
248         return true
249     }
250 
251     override fun hashCode(): Int {
252         var result = state.hashCode()
253         result = 31 * result + orientation.hashCode()
254         result = 31 * result + enabled.hashCode()
255         result = 31 * result + (interactionSource?.hashCode() ?: 0)
256         result = 31 * result + startDragImmediately.hashCode()
257         result = 31 * result + onDragStarted.hashCode()
258         result = 31 * result + onDragStopped.hashCode()
259         result = 31 * result + reverseDirection.hashCode()
260         return result
261     }
262 
263     override fun InspectorInfo.inspectableProperties() {
264         name = "draggable"
265         properties["orientation"] = orientation
266         properties["enabled"] = enabled
267         properties["reverseDirection"] = reverseDirection
268         properties["interactionSource"] = interactionSource
269         properties["startDragImmediately"] = startDragImmediately
270         properties["onDragStarted"] = onDragStarted
271         properties["onDragStopped"] = onDragStopped
272         properties["state"] = state
273     }
274 
275     companion object {
276         val CanDrag: (PointerInputChange) -> Boolean = { true }
277     }
278 }
279 
280 internal class DraggableNode(
281     private var state: DraggableState,
282     canDrag: (PointerInputChange) -> Boolean,
283     private var orientation: Orientation,
284     enabled: Boolean,
285     interactionSource: MutableInteractionSource?,
286     private var startDragImmediately: Boolean,
287     private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
288     private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
289     private var reverseDirection: Boolean
290 ) :
291     DragGestureNode(
292         canDrag = canDrag,
293         enabled = enabled,
294         interactionSource = interactionSource,
295         orientationLock = orientation
296     ) {
297 
dragnull298     override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
299         state.drag(MutatePriority.UserInput) {
300             forEachDelta { dragDelta ->
301                 dragBy(dragDelta.delta.reverseIfNeeded().toFloat(orientation))
302             }
303         }
304     }
305 
onDragStartednull306     override fun onDragStarted(startedPosition: Offset) {
307         if (!isAttached || onDragStarted == NoOpOnDragStarted) return
308         coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
309             this@DraggableNode.onDragStarted(this, startedPosition)
310         }
311     }
312 
onDragStoppednull313     override fun onDragStopped(velocity: Velocity) {
314         if (!isAttached || onDragStopped == NoOpOnDragStopped) return
315         coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
316             this@DraggableNode.onDragStopped(this, velocity.reverseIfNeeded().toFloat(orientation))
317         }
318     }
319 
startDragImmediatelynull320     override fun startDragImmediately(): Boolean = startDragImmediately
321 
322     fun update(
323         state: DraggableState,
324         canDrag: (PointerInputChange) -> Boolean,
325         orientation: Orientation,
326         enabled: Boolean,
327         interactionSource: MutableInteractionSource?,
328         startDragImmediately: Boolean,
329         onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
330         onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
331         reverseDirection: Boolean
332     ) {
333         var resetPointerInputHandling = false
334         if (this.state != state) {
335             this.state = state
336             resetPointerInputHandling = true
337         }
338         if (this.orientation != orientation) {
339             this.orientation = orientation
340             resetPointerInputHandling = true
341         }
342         if (this.reverseDirection != reverseDirection) {
343             this.reverseDirection = reverseDirection
344             resetPointerInputHandling = true
345         }
346 
347         this.onDragStarted = onDragStarted
348         this.onDragStopped = onDragStopped
349         this.startDragImmediately = startDragImmediately
350 
351         update(canDrag, enabled, interactionSource, orientation, resetPointerInputHandling)
352     }
353 
reverseIfNeedednull354     private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
355 
356     private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f
357 }
358 
359 /** A node that performs drag gesture recognition and event propagation. */
360 internal abstract class DragGestureNode(
361     canDrag: (PointerInputChange) -> Boolean,
362     enabled: Boolean,
363     interactionSource: MutableInteractionSource?,
364     private var orientationLock: Orientation?
365 ) : DelegatingNode(), PointerInputModifierNode {
366 
367     protected var canDrag = canDrag
368         private set
369 
370     protected var enabled = enabled
371         private set
372 
373     protected var interactionSource = interactionSource
374         private set
375 
376     // Use wrapper lambdas here to make sure that if these properties are updated while we suspend,
377     // we point to the new reference when we invoke them. startDragImmediately is a lambda since we
378     // need the most recent value passed to it from Scrollable.
379     private val _canDrag: (PointerInputChange) -> Boolean = { this.canDrag(it) }
380     private var channel: Channel<DragEvent>? = null
381     private var dragInteraction: DragInteraction.Start? = null
382     private var isListeningForEvents = false
383 
384     /**
385      * Responsible for the dragging behavior between the start and the end of the drag. It
386      * continually invokes `forEachDelta` to process incoming events. In return, `forEachDelta`
387      * calls `dragBy` method to process each individual delta.
388      */
389     abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
390 
391     /**
392      * Passes the action needed when a drag starts. This gives the ability to pass the desired
393      * behavior from other nodes implementing AbstractDraggableNode
394      */
395     abstract fun onDragStarted(startedPosition: Offset)
396 
397     /**
398      * Passes the action needed when a drag stops. This gives the ability to pass the desired
399      * behavior from other nodes implementing AbstractDraggableNode
400      */
401     abstract fun onDragStopped(velocity: Velocity)
402 
403     /**
404      * If touch slop recognition should be skipped. If this is true, this node will start
405      * recognizing drag events immediately without waiting for touch slop.
406      */
407     abstract fun startDragImmediately(): Boolean
408 
409     private fun startListeningForEvents() {
410         isListeningForEvents = true
411 
412         /**
413          * To preserve the original behavior we had (before the Modifier.Node migration) we need to
414          * scope the DragStopped and DragCancel methods to the node's coroutine scope instead of
415          * using the one provided by the pointer input modifier, this is to ensure that even when
416          * the pointer input scope is reset we will continue any coroutine scope scope that we
417          * started from these methods while the pointer input scope was active.
418          */
419         coroutineScope.launch {
420             while (isActive) {
421                 var event = channel?.receive()
422                 if (event !is DragStarted) continue
423                 processDragStart(event)
424                 try {
425                     drag { processDelta ->
426                         while (event !is DragStopped && event !is DragCancelled) {
427                             (event as? DragDelta)?.let(processDelta)
428                             event = channel?.receive()
429                         }
430                     }
431                     if (event is DragStopped) {
432                         processDragStop(event as DragStopped)
433                     } else if (event is DragCancelled) {
434                         processDragCancel()
435                     }
436                 } catch (c: CancellationException) {
437                     processDragCancel()
438                 }
439             }
440         }
441     }
442 
443     private var pointerInputNode: SuspendingPointerInputModifierNode? = null
444 
445     override fun onDetach() {
446         isListeningForEvents = false
447         disposeInteractionSource()
448     }
449 
450     override fun onPointerEvent(
451         pointerEvent: PointerEvent,
452         pass: PointerEventPass,
453         bounds: IntSize
454     ) {
455         if (enabled && pointerInputNode == null) {
456             pointerInputNode = delegate(initializePointerInputNode())
457         }
458         pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
459     }
460 
461     @OptIn(ExperimentalFoundationApi::class)
462     private fun initializePointerInputNode(): SuspendingPointerInputModifierNode {
463         return SuspendingPointerInputModifierNode {
464             // re-create tracker when pointer input block restarts. This lazily creates the tracker
465             // only when it is need.
466             val velocityTracker = VelocityTracker()
467 
468             val onDragStart:
469                 (
470                     down: PointerInputChange,
471                     slopTriggerChange: PointerInputChange,
472                     postSlopOffset: Offset
473                 ) -> Unit =
474                 { down, slopTriggerChange, postSlopOffset ->
475                     if (canDrag.invoke(down)) {
476                         if (!isListeningForEvents) {
477                             if (channel == null) {
478                                 channel = Channel(capacity = Channel.UNLIMITED)
479                             }
480                             startListeningForEvents()
481                         }
482                         velocityTracker.addPointerInputChange(down)
483                         val dragStartedOffset = slopTriggerChange.position - postSlopOffset
484                         // the drag start event offset is the down event + touch slop value
485                         // or in this case the event that triggered the touch slop minus
486                         // the post slop offset
487                         channel?.trySend(DragStarted(dragStartedOffset))
488                     }
489                 }
490 
491             val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
492                 velocityTracker.addPointerInputChange(upEvent)
493                 val maximumVelocity = viewConfiguration.maximumFlingVelocity
494                 val velocity =
495                     velocityTracker.calculateVelocity(Velocity(maximumVelocity, maximumVelocity))
496                 velocityTracker.resetTracking()
497                 channel?.trySend(DragStopped(velocity.toValidVelocity()))
498             }
499 
500             val onDragCancel: () -> Unit = { channel?.trySend(DragCancelled) }
501 
502             val shouldAwaitTouchSlop: () -> Boolean = { !startDragImmediately() }
503 
504             val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
505                 { change, delta ->
506                     velocityTracker.addPointerInputChange(change)
507                     channel?.trySend(DragDelta(delta))
508                 }
509 
510             coroutineScope {
511                 try {
512                     detectDragGestures(
513                         orientationLock = orientationLock,
514                         onDragStart = onDragStart,
515                         onDragEnd = onDragEnd,
516                         onDragCancel = onDragCancel,
517                         shouldAwaitTouchSlop = shouldAwaitTouchSlop,
518                         onDrag = onDrag
519                     )
520                 } catch (cancellation: CancellationException) {
521                     channel?.trySend(DragCancelled)
522                     if (!isActive) throw cancellation
523                 }
524             }
525         }
526     }
527 
528     override fun onCancelPointerInput() {
529         pointerInputNode?.onCancelPointerInput()
530     }
531 
532     private suspend fun processDragStart(event: DragStarted) {
533         dragInteraction?.let { oldInteraction ->
534             interactionSource?.emit(DragInteraction.Cancel(oldInteraction))
535         }
536         val interaction = DragInteraction.Start()
537         interactionSource?.emit(interaction)
538         dragInteraction = interaction
539         onDragStarted(event.startPoint)
540     }
541 
542     private suspend fun processDragStop(event: DragStopped) {
543         dragInteraction?.let { interaction ->
544             interactionSource?.emit(DragInteraction.Stop(interaction))
545             dragInteraction = null
546         }
547         onDragStopped(event.velocity)
548     }
549 
550     private suspend fun processDragCancel() {
551         dragInteraction?.let { interaction ->
552             interactionSource?.emit(DragInteraction.Cancel(interaction))
553             dragInteraction = null
554         }
555         onDragStopped(Velocity.Zero)
556     }
557 
558     fun disposeInteractionSource() {
559         dragInteraction?.let { interaction ->
560             interactionSource?.tryEmit(DragInteraction.Cancel(interaction))
561             dragInteraction = null
562         }
563     }
564 
565     fun update(
566         canDrag: (PointerInputChange) -> Boolean = this.canDrag,
567         enabled: Boolean = this.enabled,
568         interactionSource: MutableInteractionSource? = this.interactionSource,
569         orientationLock: Orientation? = this.orientationLock,
570         shouldResetPointerInputHandling: Boolean = false
571     ) {
572         var resetPointerInputHandling = shouldResetPointerInputHandling
573 
574         this.canDrag = canDrag
575         if (this.enabled != enabled) {
576             this.enabled = enabled
577             if (!enabled) {
578                 disposeInteractionSource()
579                 pointerInputNode?.let { undelegate(it) }
580                 pointerInputNode = null
581             }
582             resetPointerInputHandling = true
583         }
584         if (this.interactionSource != interactionSource) {
585             disposeInteractionSource()
586             this.interactionSource = interactionSource
587         }
588 
589         if (this.orientationLock != orientationLock) {
590             this.orientationLock = orientationLock
591             resetPointerInputHandling = true
592         }
593 
594         if (resetPointerInputHandling) {
595             pointerInputNode?.resetPointerInputHandler()
596         }
597     }
598 }
599 
600 private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
601 
602     private val dragScope: DragScope =
603         object : DragScope {
dragBynull604             override fun dragBy(pixels: Float): Unit = onDelta(pixels)
605         }
606 
607     private val scrollMutex = MutatorMutex()
608 
609     override suspend fun drag(
610         dragPriority: MutatePriority,
611         block: suspend DragScope.() -> Unit
612     ): Unit = coroutineScope { scrollMutex.mutateWith(dragScope, dragPriority, block) }
613 
dispatchRawDeltanull614     override fun dispatchRawDelta(delta: Float) {
615         return onDelta(delta)
616     }
617 }
618 
619 internal sealed class DragEvent {
620     class DragStarted(val startPoint: Offset) : DragEvent()
621 
622     class DragStopped(val velocity: Velocity) : DragEvent()
623 
624     object DragCancelled : DragEvent()
625 
626     class DragDelta(val delta: Offset) : DragEvent()
627 }
628 
toFloatnull629 private fun Offset.toFloat(orientation: Orientation) =
630     if (orientation == Orientation.Vertical) this.y else this.x
631 
632 private fun Velocity.toFloat(orientation: Orientation) =
633     if (orientation == Orientation.Vertical) this.y else this.x
634 
635 private fun Velocity.toValidVelocity() =
636     Velocity(if (this.x.isNaN()) 0f else this.x, if (this.y.isNaN()) 0f else this.y)
637 
638 private val NoOpOnDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}
<lambda>null639 private val NoOpOnDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}
640