1 /*
<lambda>null2  * Copyright 2021 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.ui.test
18 
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.lerp
21 import androidx.compose.ui.platform.ViewConfiguration
22 import androidx.compose.ui.test.internal.JvmDefaultWithCompatibility
23 import androidx.compose.ui.util.lerp
24 import kotlin.math.ceil
25 import kotlin.math.max
26 import kotlin.math.roundToInt
27 import kotlin.math.roundToLong
28 
29 /**
30  * The receiver scope of the touch input injection lambda from [performTouchInput].
31  *
32  * The functions in [TouchInjectionScope] can roughly be divided into two groups: full gestures and
33  * individual touch events. The individual touch events are: [down], [move] and friends, [up],
34  * [cancel] and [advanceEventTime]. Full gestures are all the other functions, like
35  * [click][TouchInjectionScope.click], [doubleClick][TouchInjectionScope.doubleClick],
36  * [swipe][TouchInjectionScope.swipe], etc. These are built on top of the individual events and
37  * serve as a good example on how you can build your own full gesture functions.
38  *
39  * A touch gesture is started with a [down] event, followed by a sequence of [move] events and
40  * finally an [up] event, optionally combined with more sets of [down] and [up] events for
41  * multi-touch gestures. Most methods accept a pointerId to specify which pointer (finger) the event
42  * applies to. Movement can be expressed absolutely with [moveTo] and [updatePointerTo], or relative
43  * to the current pointer position with [moveBy] and [updatePointerBy]. The `moveTo/By` methods
44  * enqueue an event immediately, while the `updatePointerTo/By` methods don't. This allows you to
45  * update the position of multiple pointers in a single [move] event for multi-touch gestures. Touch
46  * gestures can be cancelled with [cancel]. All events, regardless the method used, will always
47  * contain the current position of _all_ pointers.
48  *
49  * The entire event injection state is shared between all `perform.*Input` methods, meaning you can
50  * continue an unfinished touch gesture in a subsequent invocation of [performTouchInput] or
51  * [performMultiModalInput]. Note however that while the pointer positions are retained across
52  * invocation of `perform.*Input` methods, they are always manipulated in the current node's local
53  * coordinate system. That means that two subsequent invocations of [performTouchInput] on different
54  * nodes will report a different [currentPosition], even though it is actually the same position on
55  * the screen.
56  *
57  * All events sent by these methods are batched together and sent as a whole after
58  * [performTouchInput] has executed its code block. Because gestures don't have to be defined all in
59  * the same [performTouchInput] block, keep in mind that while the gesture is not complete, all code
60  * you execute in between these blocks will be executed while imaginary fingers are actively
61  * touching the screen. The events sent as part of the same batch will not be interrupted by
62  * recomposition, however, if a gesture spans multiple [performTouchInput] blocks it is important to
63  * remember that recomposition, layout and drawing could take place during the gesture, which may
64  * lead to events being injected into a moving target. As pointer positions are manipulated in the
65  * current node's local coordinate system, this could lead to issues caused by the fact that part of
66  * the gesture will take effect before the rest of the events have been enqueued.
67  *
68  * Example of performing a click:
69  *
70  * @sample androidx.compose.ui.test.samples.touchInputClick
71  *
72  * Example of performing a swipe up:
73  *
74  * @sample androidx.compose.ui.test.samples.touchInputSwipeUp
75  *
76  * Example of performing an L-shaped gesture:
77  *
78  * @sample androidx.compose.ui.test.samples.touchInputLShapedGesture
79  * @see InjectionScope
80  */
81 @JvmDefaultWithCompatibility
82 interface TouchInjectionScope : InjectionScope {
83     /**
84      * Returns the current position of the given [pointerId]. The default [pointerId] is 0. The
85      * position is returned in the local coordinate system of the node with which we're interacting.
86      * (0, 0) is the top left corner of the node.
87      */
88     fun currentPosition(pointerId: Int = 0): Offset?
89 
90     /**
91      * Sends a down event for the pointer with the given [pointerId] at [position] on the associated
92      * node. The [position] is in the node's local coordinate system, where (0, 0) is the top left
93      * corner of the node.
94      *
95      * If no pointers are down yet, this will start a new touch gesture. If a gesture is already in
96      * progress, this event is sent at the same timestamp as the last event. If the given pointer is
97      * already down, an [IllegalArgumentException] will be thrown.
98      *
99      * @param pointerId The id of the pointer, can be any number not yet in use by another pointer
100      * @param position The position of the down event, in the node's local coordinate system
101      */
102     fun down(pointerId: Int, position: Offset)
103 
104     /**
105      * Sends a down event for the default pointer at [position] on the associated node. The
106      * [position] is in the node's local coordinate system, where (0, 0) is the top left corner of
107      * the node. The default pointer has `pointerId = 0`.
108      *
109      * If no pointers are down yet, this will start a new touch gesture. If a gesture is already in
110      * progress, this event is sent at the same timestamp as the last event. If the default pointer
111      * is already down, an [IllegalArgumentException] will be thrown.
112      *
113      * @param position The position of the down event, in the node's local coordinate system
114      */
115     fun down(position: Offset) {
116         down(0, position)
117     }
118 
119     /**
120      * Sends a move event [delayMillis] after the last sent event on the associated node, with the
121      * position of the pointer with the given [pointerId] updated to [position]. The [position] is
122      * in the node's local coordinate system, where (0, 0) is the top left corner of the node.
123      *
124      * If the pointer is not yet down, an [IllegalArgumentException] will be thrown.
125      *
126      * @param pointerId The id of the pointer to move, as supplied in [down]
127      * @param position The new position of the pointer, in the node's local coordinate system
128      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
129      *   by default.
130      */
131     fun moveTo(pointerId: Int, position: Offset, delayMillis: Long = eventPeriodMillis) {
132         updatePointerTo(pointerId, position)
133         move(delayMillis)
134     }
135 
136     /**
137      * Sends a move event [delayMillis] after the last sent event on the associated node, with the
138      * position of the default pointer updated to [position]. The [position] is in the node's local
139      * coordinate system, where (0, 0) is the top left corner of the node. The default pointer has
140      * `pointerId = 0`.
141      *
142      * If the default pointer is not yet down, an [IllegalArgumentException] will be thrown.
143      *
144      * @param position The new position of the pointer, in the node's local coordinate system
145      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
146      *   by default.
147      */
148     fun moveTo(position: Offset, delayMillis: Long = eventPeriodMillis) {
149         moveTo(0, position, delayMillis)
150     }
151 
152     /**
153      * Updates the position of the pointer with the given [pointerId] to the given [position], but
154      * does not send a move event. The move event can be sent with [move]. The [position] is in the
155      * node's local coordinate system, where (0.px, 0.px) is the top left corner of the node.
156      *
157      * If the pointer is not yet down, an [IllegalArgumentException] will be thrown.
158      *
159      * @param pointerId The id of the pointer to move, as supplied in [down]
160      * @param position The new position of the pointer, in the node's local coordinate system
161      */
162     fun updatePointerTo(pointerId: Int, position: Offset)
163 
164     /**
165      * Sends a move event [delayMillis] after the last sent event on the associated node, with the
166      * position of the pointer with the given [pointerId] moved by the given [delta].
167      *
168      * If the pointer is not yet down, an [IllegalArgumentException] will be thrown.
169      *
170      * @param pointerId The id of the pointer to move, as supplied in [down]
171      * @param delta The position for this move event, relative to the current position of the
172      *   pointer. For example, `delta = Offset(10.px, -10.px) will add 10.px to the pointer's
173      *   x-position, and subtract 10.px from the pointer's y-position.
174      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
175      *   by default.
176      */
177     fun moveBy(pointerId: Int, delta: Offset, delayMillis: Long = eventPeriodMillis) {
178         updatePointerBy(pointerId, delta)
179         move(delayMillis)
180     }
181 
182     /**
183      * Sends a move event [delayMillis] after the last sent event on the associated node, with the
184      * position of the default pointer moved by the given [delta]. The default pointer has
185      * `pointerId = 0`.
186      *
187      * If the pointer is not yet down, an [IllegalArgumentException] will be thrown.
188      *
189      * @param delta The position for this move event, relative to the current position of the
190      *   pointer. For example, `delta = Offset(10.px, -10.px) will add 10.px to the pointer's
191      *   x-position, and subtract 10.px from the pointer's y-position.
192      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
193      *   by default.
194      */
195     fun moveBy(delta: Offset, delayMillis: Long = eventPeriodMillis) {
196         moveBy(0, delta, delayMillis)
197     }
198 
199     /**
200      * Updates the position of the pointer with the given [pointerId] by the given [delta], but does
201      * not send a move event. The move event can be sent with [move].
202      *
203      * If the pointer is not yet down, an [IllegalArgumentException] will be thrown.
204      *
205      * @param pointerId The id of the pointer to move, as supplied in [down]
206      * @param delta The position for this move event, relative to the last sent position of the
207      *   pointer. For example, `delta = Offset(10.px, -10.px) will add 10.px to the pointer's
208      *   x-position, and subtract 10.px from the pointer's y-position.
209      */
210     fun updatePointerBy(pointerId: Int, delta: Offset) {
211         // Ignore currentPosition of null here, let updatePointerTo generate the error
212         val currentPosition = currentPosition(pointerId) ?: Offset.Zero
213 
214         val position =
215             if (currentPosition.isValid() && delta.isValid()) {
216                 currentPosition + delta
217             } else {
218                 // Allows invalid position to still pass back through Compose (for testing)
219                 Offset.Unspecified
220             }
221 
222         updatePointerTo(pointerId, position)
223     }
224 
225     /**
226      * Sends a move event [delayMillis] after the last sent event without updating any of the
227      * pointer positions. This can be useful when batching movement of multiple pointers together,
228      * which can be done with [updatePointerTo] and [updatePointerBy].
229      *
230      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
231      *   by default.
232      */
233     fun move(delayMillis: Long = eventPeriodMillis)
234 
235     /**
236      * Sends a move event [delayMillis] after the last sent event without updating any of the
237      * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
238      * to the move event. This corresponds to the scenario where a touch screen generates touch
239      * events quicker than can be dispatched and batches them together.
240      *
241      * @sample androidx.compose.ui.test.samples.touchInputMultiTouchWithHistory
242      * @param relativeHistoricalTimes Time of each historical event, as a millisecond relative to
243      *   the time the actual event is sent. For example, -10L means 10ms earlier.
244      * @param historicalCoordinates Coordinates of each historical event, in the same coordinate
245      *   space as [moveTo]. The outer list must have the same size as the number of pointers in the
246      *   event, and each inner list must have the same size as [relativeHistoricalTimes]. The `i`th
247      *   pointer is assigned the `i`th history, with the pointers sorted on ascending pointerId.
248      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
249      *   by default.
250      */
251     @ExperimentalTestApi
252     fun moveWithHistoryMultiPointer(
253         relativeHistoricalTimes: List<Long>,
254         historicalCoordinates: List<List<Offset>>,
255         delayMillis: Long = eventPeriodMillis
256     )
257 
258     /**
259      * Sends a move event [delayMillis] after the last sent event without updating any of the
260      * pointer positions, while adding the [historicalCoordinates] at the [relativeHistoricalTimes]
261      * to the move event. This corresponds to the scenario where a touch screen generates touch
262      * events quicker than can be dispatched and batches them together.
263      *
264      * This overload is a convenience method for the common case where the gesture only has one
265      * pointer.
266      *
267      * @param relativeHistoricalTimes Time of each historical event, as a millisecond relative to
268      *   the time the actual event is sent. For example, -10L means 10ms earlier.
269      * @param historicalCoordinates Coordinates of each historical event, in the same coordinate
270      *   space as [moveTo]. The list must have the same size as [relativeHistoricalTimes].
271      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
272      *   by default.
273      */
274     @ExperimentalTestApi
275     fun moveWithHistory(
276         relativeHistoricalTimes: List<Long>,
277         historicalCoordinates: List<Offset>,
278         delayMillis: Long = eventPeriodMillis
279     ) =
280         moveWithHistoryMultiPointer(
281             relativeHistoricalTimes,
282             listOf(historicalCoordinates),
283             delayMillis
284         )
285 
286     /**
287      * Sends an up event for the pointer with the given [pointerId], or the default pointer if
288      * [pointerId] is omitted, on the associated node.
289      *
290      * @param pointerId The id of the pointer to lift up, as supplied in [down]
291      */
292     fun up(pointerId: Int = 0)
293 
294     /**
295      * Sends a cancel event [delayMillis] after the last sent event to cancel the current gesture.
296      * The cancel event contains the current position of all active pointers.
297      *
298      * @param delayMillis The time between the last sent event and this event. [eventPeriodMillis]
299      *   by default.
300      */
301     fun cancel(delayMillis: Long = eventPeriodMillis)
302 }
303 
304 internal class TouchInjectionScopeImpl(private val baseScope: MultiModalInjectionScopeImpl) :
<lambda>null305     TouchInjectionScope, InjectionScope by baseScope {
306     private val inputDispatcher
307         get() = baseScope.inputDispatcher
308 
309     private fun localToRoot(position: Offset) = baseScope.localToRoot(position)
310 
311     override fun currentPosition(pointerId: Int): Offset? {
312         val positionInRoot = inputDispatcher.getCurrentTouchPosition(pointerId) ?: return null
313         return baseScope.rootToLocal(positionInRoot)
314     }
315 
316     override fun down(pointerId: Int, position: Offset) {
317         val positionInRoot = localToRoot(position)
318         inputDispatcher.enqueueTouchDown(pointerId, positionInRoot)
319     }
320 
321     override fun updatePointerTo(pointerId: Int, position: Offset) {
322         val positionInRoot = localToRoot(position)
323         inputDispatcher.updateTouchPointer(pointerId, positionInRoot)
324     }
325 
326     override fun move(delayMillis: Long) {
327         advanceEventTime(delayMillis)
328         inputDispatcher.enqueueTouchMove()
329     }
330 
331     @ExperimentalTestApi
332     override fun moveWithHistoryMultiPointer(
333         relativeHistoricalTimes: List<Long>,
334         historicalCoordinates: List<List<Offset>>,
335         delayMillis: Long
336     ) {
337         repeat(relativeHistoricalTimes.size) {
338             check(relativeHistoricalTimes[it] < 0) {
339                 "Relative historical times should be negative, in order to be in the past" +
340                     "(offset $it was: ${relativeHistoricalTimes[it]})"
341             }
342             check(relativeHistoricalTimes[it] >= -delayMillis) {
343                 "Relative historical times should not be earlier than the previous event " +
344                     "(offset $it was: ${relativeHistoricalTimes[it]}, ${-delayMillis})"
345             }
346         }
347 
348         advanceEventTime(delayMillis)
349         inputDispatcher.enqueueTouchMoves(relativeHistoricalTimes, historicalCoordinates)
350     }
351 
352     override fun up(pointerId: Int) {
353         inputDispatcher.enqueueTouchUp(pointerId)
354     }
355 
356     override fun cancel(delayMillis: Long) {
357         advanceEventTime(delayMillis)
358         inputDispatcher.enqueueTouchCancel()
359     }
360 }
361 
362 /**
363  * Performs a click gesture (aka a tap) on the associated node.
364  *
365  * The click is done at the given [position], or in the [center] if the [position] is omitted. The
366  * [position] is in the node's local coordinate system, where (0, 0) is the top left corner of the
367  * node.
368  *
369  * @param position The position where to click, in the node's local coordinate system. If omitted,
370  *   the [center] of the node will be used.
371  */
TouchInjectionScopenull372 fun TouchInjectionScope.click(position: Offset = center) {
373     down(position)
374     move()
375     up()
376 }
377 
378 /**
379  * Performs a long click gesture (aka a long press) on the associated node.
380  *
381  * The long click is done at the given [position], or in the [center] if the [position] is omitted.
382  * By default, the [durationMillis] of the press is 100ms longer than the minimum required duration
383  * for a long press. The [position] is in the node's local coordinate system, where (0, 0) is the
384  * top left corner of the node.
385  *
386  * @param position The position of the long click, in the node's local coordinate system. If
387  *   omitted, the [center] of the node will be used.
388  * @param durationMillis The time between the down and the up event
389  */
TouchInjectionScopenull390 fun TouchInjectionScope.longClick(
391     position: Offset = center,
392     durationMillis: Long = viewConfiguration.longPressTimeoutMillis + 100
393 ) {
394     require(durationMillis >= viewConfiguration.longPressTimeoutMillis) {
395         "Long click must have a duration of at least ${viewConfiguration.longPressTimeoutMillis}ms"
396     }
397     swipe(position, position, durationMillis)
398 }
399 
400 // The average of min and max is a safe default
401 private val ViewConfiguration.defaultDoubleTapDelayMillis: Long
402     get() = (doubleTapMinTimeMillis + doubleTapTimeoutMillis) / 2
403 
404 /**
405  * Performs a double click gesture (aka a double tap) on the associated node.
406  *
407  * The double click is done at the given [position] or in the [center] if the [position] is omitted.
408  * By default, the [delayMillis] between the first and the second click is half way in between the
409  * minimum and maximum required delay for a double click. The [position] is in the node's local
410  * coordinate system, where (0, 0) is the top left corner of the node.
411  *
412  * @param position The position of the double click, in the node's local coordinate system. If
413  *   omitted, the [center] position will be used.
414  * @param delayMillis The time between the up event of the first click and the down event of the
415  *   second click
416  */
doubleClicknull417 fun TouchInjectionScope.doubleClick(
418     position: Offset = center,
419     delayMillis: Long = viewConfiguration.defaultDoubleTapDelayMillis
420 ) {
421     require(delayMillis >= viewConfiguration.doubleTapMinTimeMillis) {
422         "Time between clicks in double click must be at least " +
423             "${viewConfiguration.doubleTapMinTimeMillis}ms"
424     }
425     require(delayMillis < viewConfiguration.doubleTapTimeoutMillis) {
426         "Time between clicks in double click must be smaller than " +
427             "${viewConfiguration.doubleTapTimeoutMillis}ms"
428     }
429     click(position)
430     advanceEventTime(delayMillis)
431     click(position)
432 }
433 
434 /**
435  * Performs a swipe gesture on the associated node.
436  *
437  * The motion events are linearly interpolated between [start] and [end]. The coordinates are in the
438  * node's local coordinate system, where (0, 0) is the top left corner of the node. The default
439  * duration is 200 milliseconds.
440  *
441  * @param start The start position of the gesture, in the node's local coordinate system
442  * @param end The end position of the gesture, in the node's local coordinate system
443  * @param durationMillis The duration of the gesture
444  */
TouchInjectionScopenull445 fun TouchInjectionScope.swipe(start: Offset, end: Offset, durationMillis: Long = 200) {
446     val durationFloat = durationMillis.toFloat()
447     swipe(curve = { lerp(start, end, it / durationFloat) }, durationMillis = durationMillis)
448 }
449 
450 /**
451  * Performs a swipe gesture on the associated node.
452  *
453  * The swipe follows the [curve] from 0 till [durationMillis]. Will force sampling of an event at
454  * all times defined in [keyTimes]. The time between events is kept as close to
455  * [eventPeriodMillis][InjectionScope.eventPeriodMillis] as possible, given the constraints. The
456  * coordinates are in the node's local coordinate system, where (0, 0) is the top left corner of the
457  * node. The default duration is 200 milliseconds.
458  *
459  * @param curve The function that describes the gesture. The argument passed to the function is the
460  *   time in milliseconds since the start of the swipe, and the return value is the location of the
461  *   pointer at that point in time.
462  * @param durationMillis The duration of the gesture
463  * @param keyTimes An optional list of timestamps in milliseconds at which a move event must be
464  *   sampled
465  */
TouchInjectionScopenull466 fun TouchInjectionScope.swipe(
467     curve: (timeMillis: Long) -> Offset,
468     durationMillis: Long = 200,
469     keyTimes: List<Long> = emptyList()
470 ) {
471     multiTouchSwipe(listOf(curve), durationMillis, keyTimes)
472 }
473 
474 /**
475  * Performs a multi touch swipe gesture on the associated node.
476  *
477  * Each pointer follows [curves]&#91;i] from 0 till [durationMillis]. Sampling of an event is forced
478  * at all times defined in [keyTimes]. The time between events is kept as close to
479  * [eventPeriodMillis][InjectionScope.eventPeriodMillis] as possible, given the constraints. The
480  * coordinates are in the node's local coordinate system, where (0, 0) is the top left corner of the
481  * node. The default duration is 200 milliseconds.
482  *
483  * @param curves The functions that describe the gesture. Function _i_ defines the position over
484  *   time for pointer id _i_. The argument passed to each function is the time in milliseconds since
485  *   the start of the swipe, and the return value is the location of that pointer at that point in
486  *   time.
487  * @param durationMillis The duration of the gesture
488  * @param keyTimes An optional list of timestamps in milliseconds at which a move event must be
489  *   sampled
490  */
multiTouchSwipenull491 fun TouchInjectionScope.multiTouchSwipe(
492     curves: List<(timeMillis: Long) -> Offset>,
493     durationMillis: Long = 200,
494     keyTimes: List<Long> = emptyList()
495 ) {
496     val startTime = 0L
497     val endTime = durationMillis
498 
499     // Validate input
500     require(durationMillis >= 1) { "duration must be at least 1 millisecond, not $durationMillis" }
501     val validRange = startTime..endTime
502     require(keyTimes.all { it in validRange }) {
503         "keyTimes contains timestamps out of range [$startTime..$endTime]: $keyTimes"
504     }
505     require(keyTimes.asSequence().zipWithNext { a, b -> a <= b }.all { it }) {
506         "keyTimes must be sorted: $keyTimes"
507     }
508 
509     // Send down events
510     curves.forEachIndexed { i, curve -> down(i, curve(startTime)) }
511 
512     // Send move events between each consecutive pair in [t0, ..keyTimes, tN]
513     var currTime = startTime
514     var key = 0
515     while (currTime < endTime) {
516         // advance key
517         while (key < keyTimes.size && keyTimes[key] <= currTime) {
518             key++
519         }
520         // send events between t and next keyTime
521         val tNext = if (key < keyTimes.size) keyTimes[key] else endTime
522         sendMultiTouchSwipeSegment(curves, currTime, tNext)
523         currTime = tNext
524     }
525 
526     // And end with up events
527     repeat(curves.size) { up(it) }
528 }
529 
530 /**
531  * Generates move events between `f([t0])` and `f([tN])` during the time window `(t0, tN]`, for each
532  * `f` in [fs], following the curves defined by each `f`. The number of events sent (#numEvents) is
533  * such that the time between each event is as close to
534  * [eventPeriodMillis][InputDispatcher.eventPeriodMillis] as possible, but at least 1. The first
535  * event is sent at time `downTime + (tN - t0) / #numEvents`, the last event is sent at time tN.
536  *
537  * @param fs The functions that define the coordinates of the respective gestures over time
538  * @param t0 The start time of this segment of the swipe, in milliseconds relative to downTime
539  * @param tN The end time of this segment of the swipe, in milliseconds relative to downTime
540  */
TouchInjectionScopenull541 private fun TouchInjectionScope.sendMultiTouchSwipeSegment(
542     fs: List<(Long) -> Offset>,
543     t0: Long,
544     tN: Long
545 ) {
546     var step = 0
547     // How many steps will we take between t0 and tN? At least 1, and a number that will
548     // bring as as close to eventPeriod as possible
549     val steps = max(1, ((tN - t0) / eventPeriodMillis.toFloat()).roundToInt())
550 
551     var tPrev = t0
552     while (step++ < steps) {
553         val progress = step / steps.toFloat()
554         val t = lerp(t0, tN, progress)
555         fs.forEachIndexed { i, f -> updatePointerTo(i, f(t)) }
556         move(t - tPrev)
557         tPrev = t
558     }
559 }
560 
561 /**
562  * Performs a pinch gesture on the associated node.
563  *
564  * For each pair of start and end [Offset]s, the motion events are linearly interpolated. The
565  * coordinates are in the node's local coordinate system where (0, 0) is the top left corner of the
566  * node. The default duration is 400 milliseconds.
567  *
568  * @param start0 The start position of the first gesture in the node's local coordinate system
569  * @param end0 The end position of the first gesture in the node's local coordinate system
570  * @param start1 The start position of the second gesture in the node's local coordinate system
571  * @param end1 The end position of the second gesture in the node's local coordinate system
572  * @param durationMillis the duration of the gesture
573  */
pinchnull574 fun TouchInjectionScope.pinch(
575     start0: Offset,
576     end0: Offset,
577     start1: Offset,
578     end1: Offset,
579     durationMillis: Long = 400
580 ) {
581     val durationFloat = durationMillis.toFloat()
582     multiTouchSwipe(
583         listOf(
584             { lerp(start0, end0, it / durationFloat) },
585             { lerp(start1, end1, it / durationFloat) }
586         ),
587         durationMillis
588     )
589 }
590 
591 /**
592  * Performs a swipe gesture on the associated node such that it ends with the given [endVelocity].
593  *
594  * The swipe will go through [start] at t=0 and through [end] at t=[durationMillis]. In between, the
595  * swipe will go monotonically from [start] and [end], but not strictly. Due to imprecision, no
596  * guarantees can be made for the actual velocity at the end of the gesture, but generally it is
597  * within 0.1 of the desired velocity.
598  *
599  * When a swipe cannot be created that results in the desired velocity (because the input is too
600  * restrictive), an exception will be thrown with suggestions to fix the input.
601  *
602  * The coordinates are in the node's local coordinate system, where (0, 0) is the top left corner of
603  * the node. The default duration is calculated such that a feasible swipe can be created that ends
604  * in the given velocity.
605  *
606  * @param start The start position of the gesture, in the node's local coordinate system
607  * @param end The end position of the gesture, in the node's local coordinate system
608  * @param endVelocity The velocity of the gesture at the moment it ends in px/second. Must be
609  *   positive.
610  * @param durationMillis The duration of the gesture in milliseconds. Must be long enough that at
611  *   least 3 input events are generated, which happens with a duration of 40ms or more. If omitted,
612  *   a duration is calculated such that a valid swipe with velocity can be created.
613  * @throws IllegalArgumentException When no swipe can be generated that will result in the desired
614  *   velocity. The error message will suggest changes to the input parameters such that a swipe will
615  *   become feasible.
616  */
TouchInjectionScopenull617 fun TouchInjectionScope.swipeWithVelocity(
618     start: Offset,
619     end: Offset,
620     /*@FloatRange(from = 0.0)*/
621     endVelocity: Float,
622     durationMillis: Long = VelocityPathFinder.calculateDefaultDuration(start, end, endVelocity)
623 ) {
624     require(endVelocity >= 0f) { "Velocity cannot be $endVelocity, it must be positive" }
625     require(eventPeriodMillis < 40) {
626         "InputDispatcher.eventPeriod must be smaller than 40ms in order to generate velocities"
627     }
628     val minimumDuration = ceil(2.5f * eventPeriodMillis).roundToLong()
629     require(durationMillis >= minimumDuration) {
630         "Duration must be at least ${minimumDuration}ms because " +
631             "velocity requires at least 3 input events"
632     }
633 
634     val pathFinder = VelocityPathFinder(start, end, endVelocity, durationMillis)
635     val swipeFunction: (Long) -> Offset = { pathFinder.calculateOffsetForTime(it) }
636     swipe(swipeFunction, durationMillis)
637 }
638 
639 /**
640  * Performs a swipe up gesture along `x = [centerX]` of the associated node, from [startY] till
641  * [endY], taking [durationMillis] milliseconds.
642  *
643  * @param startY The y-coordinate of the start of the swipe. Must be greater than or equal to the
644  *   [endY]. By default the [bottom] of the node.
645  * @param endY The y-coordinate of the end of the swipe. Must be less than or equal to the [startY].
646  *   By default the [top] of the node.
647  * @param durationMillis The duration of the swipe. By default 200 milliseconds.
648  */
TouchInjectionScopenull649 fun TouchInjectionScope.swipeUp(
650     startY: Float = bottom,
651     endY: Float = top,
652     durationMillis: Long = 200
653 ) {
654     require(startY >= endY) { "startY=$startY needs to be greater than or equal to endY=$endY" }
655     val start = Offset(centerX, startY)
656     val end = Offset(centerX, endY)
657     swipe(start, end, durationMillis)
658 }
659 
660 /**
661  * Performs a swipe down gesture along `x = [centerX]` of the associated node, from [startY] till
662  * [endY], taking [durationMillis] milliseconds.
663  *
664  * @param startY The y-coordinate of the start of the swipe. Must be less than or equal to the
665  *   [endY]. By default the [top] of the node.
666  * @param endY The y-coordinate of the end of the swipe. Must be greater than or equal to the
667  *   [startY]. By default the [bottom] of the node.
668  * @param durationMillis The duration of the swipe. By default 200 milliseconds.
669  */
TouchInjectionScopenull670 fun TouchInjectionScope.swipeDown(
671     startY: Float = top,
672     endY: Float = bottom,
673     durationMillis: Long = 200
674 ) {
675     require(startY <= endY) { "startY=$startY needs to be less than or equal to endY=$endY" }
676     val start = Offset(centerX, startY)
677     val end = Offset(centerX, endY)
678     swipe(start, end, durationMillis)
679 }
680 
681 /**
682  * Performs a swipe left gesture along `y = [centerY]` of the associated node, from [startX] till
683  * [endX], taking [durationMillis] milliseconds.
684  *
685  * @param startX The x-coordinate of the start of the swipe. Must be greater than or equal to the
686  *   [endX]. By default the [right] of the node.
687  * @param endX The x-coordinate of the end of the swipe. Must be less than or equal to the [startX].
688  *   By default the [left] of the node.
689  * @param durationMillis The duration of the swipe. By default 200 milliseconds.
690  */
TouchInjectionScopenull691 fun TouchInjectionScope.swipeLeft(
692     startX: Float = right,
693     endX: Float = left,
694     durationMillis: Long = 200
695 ) {
696     require(startX >= endX) { "startX=$startX needs to be greater than or equal to endX=$endX" }
697     val start = Offset(startX, centerY)
698     val end = Offset(endX, centerY)
699     swipe(start, end, durationMillis)
700 }
701 
702 /**
703  * Performs a swipe right gesture along `y = [centerY]` of the associated node, from [startX] till
704  * [endX], taking [durationMillis] milliseconds.
705  *
706  * @param startX The x-coordinate of the start of the swipe. Must be less than or equal to the
707  *   [endX]. By default the [left] of the node.
708  * @param endX The x-coordinate of the end of the swipe. Must be greater than or equal to the
709  *   [startX]. By default the [right] of the node.
710  * @param durationMillis The duration of the swipe. By default 200 milliseconds.
711  */
swipeRightnull712 fun TouchInjectionScope.swipeRight(
713     startX: Float = left,
714     endX: Float = right,
715     durationMillis: Long = 200
716 ) {
717     require(startX <= endX) { "startX=$startX needs to be less than or equal to endX=$endX" }
718     val start = Offset(startX, centerY)
719     val end = Offset(endX, centerY)
720     swipe(start, end, durationMillis)
721 }
722