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