1 /*
2 * Copyright 2020 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 @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
18
19 package androidx.compose.ui.input.pointer
20
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.ui.ExperimentalComposeUiApi
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.geometry.Offset
25 import androidx.compose.ui.geometry.Size
26 import androidx.compose.ui.input.pointer.PointerEventPass.Final
27 import androidx.compose.ui.input.pointer.PointerEventPass.Initial
28 import androidx.compose.ui.input.pointer.PointerEventPass.Main
29 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
30 import androidx.compose.ui.layout.LayoutCoordinates
31 import androidx.compose.ui.unit.IntSize
32
33 /** A [Modifier.Element] that can interact with pointer input. */
34 @JvmDefaultWithCompatibility
35 interface PointerInputModifier : Modifier.Element {
36 val pointerInputFilter: PointerInputFilter
37 }
38
39 /**
40 * A PointerInputFilter represents a single entity that receives [PointerInputChange]s), interprets
41 * them, and consumes the aspects of the changes that it is react to such that other
42 * PointerInputFilters don't also react to them.
43 */
44 abstract class PointerInputFilter {
45
46 /**
47 * Invoked when pointers that previously hit this [PointerInputFilter] have changed. It is
48 * expected that any [PointerInputChange]s that are used during this event and should not be
49 * considered valid to be used in other filters should be marked as consumed by calling
50 * [PointerInputChange.consume].
51 *
52 * @param pointerEvent The list of [PointerInputChange]s with positions relative to this
53 * [PointerInputFilter].
54 * @param pass The [PointerEventPass] in which this function is being called.
55 * @param bounds The width and height associated with this [PointerInputFilter].
56 * @see PointerInputChange
57 * @see PointerEventPass
58 */
onPointerEventnull59 abstract fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize)
60
61 /**
62 * Invoked to notify the handler that no more calls to [PointerInputFilter] will be made, until
63 * at least new pointers exist. This can occur for a few reasons:
64 * 1. Android dispatches ACTION_CANCEL to Compose.
65 * 2. This [PointerInputFilter] is no longer associated with a LayoutNode.
66 * 3. This [PointerInputFilter]'s associated LayoutNode is no longer in the composition tree.
67 */
68 abstract fun onCancel()
69
70 internal var layoutCoordinates: LayoutCoordinates? = null
71
72 /** The layout size assigned to this [PointerInputFilter]. */
73 val size: IntSize
74 get() = layoutCoordinates?.size ?: IntSize.Zero
75
76 internal var isAttached: Boolean = false
77
78 /**
79 * Intercept pointer input that children receive even if the pointer is out of bounds.
80 *
81 * If `true`, and a child has been moved out of this layout and receives an event, this will
82 * receive that event. If `false`, a child receiving pointer input outside of the bounds of this
83 * layout will not trigger any events in this.
84 */
85 open val interceptOutOfBoundsChildEvents: Boolean
86 get() = false
87
88 /**
89 * If `false`, then this [PointerInputFilter] will not allow siblings under it to respond to
90 * events. If `true`, this will have the first chance to respond and the next sibling under will
91 * then get a chance to respond as well. This trigger acts at the Layout level, so if any
92 * [PointerInputFilter]s on a Layout has [shareWithSiblings] set to `true` then the Layout will
93 * share with siblings.
94 */
95 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
96 open val shareWithSiblings: Boolean
97 get() = false
98 }
99
100 /** Describes a pointer input change event that has occurred at a particular point in time. */
101 expect class PointerEvent
102 internal constructor(
103 changes: List<PointerInputChange>,
104 internalPointerEvent: InternalPointerEvent?
105 ) {
106 /** @param changes The changes. */
107 constructor(changes: List<PointerInputChange>)
108
109 /** The changes. */
110 val changes: List<PointerInputChange>
111
112 /** The state of buttons (e.g. mouse or stylus buttons) during this event. */
113 val buttons: PointerButtons
114
115 /** The state of modifier keys during this event. */
116 val keyboardModifiers: PointerKeyboardModifiers
117
118 /** The primary reason the [PointerEvent] was sent. */
119 var type: PointerEventType
120 internal set
121 }
122
123 // TODO mark internal once https://youtrack.jetbrains.com/issue/KT-36695 is fixed
124 /* internal */ expect class NativePointerButtons
125
126 /** Contains the state of pointer buttons (e.g. mouse and stylus buttons). */
127 @kotlin.jvm.JvmInline value class PointerButtons(internal val packedValue: NativePointerButtons)
128
129 /**
130 * `true` when the primary button (left mouse button) is pressed or `false` when it isn't pressed.
131 */
132 expect val PointerButtons.isPrimaryPressed: Boolean
133
134 /**
135 * `true` when the secondary button (right mouse button) is pressed or `false` when it isn't
136 * pressed.
137 */
138 expect val PointerButtons.isSecondaryPressed: Boolean
139
140 /**
141 * `true` when the tertiary button (middle mouse button) is pressed or `false` when it isn't
142 * pressed.
143 */
144 expect val PointerButtons.isTertiaryPressed: Boolean
145
146 /**
147 * `true` when the back button (mouse back button) is pressed or `false` when it isn't pressed or
148 * there is no mouse button assigned to "back."
149 */
150 expect val PointerButtons.isBackPressed: Boolean
151
152 /**
153 * `true` when the forward button (mouse forward button) is pressed or `false` when it isn't pressed
154 * or there is no button assigned to "forward."
155 */
156 expect val PointerButtons.isForwardPressed: Boolean
157
158 /**
159 * Returns `true` when the button at [buttonIndex] is pressed and `false` when it isn't pressed.
160 * This method can handle buttons that haven't been assigned a designated purpose like
161 * [isPrimaryPressed] and [isSecondaryPressed].
162 */
isPressednull163 expect fun PointerButtons.isPressed(buttonIndex: Int): Boolean
164
165 /** Returns `true` if any button is pressed or `false` if all buttons are released. */
166 expect val PointerButtons.areAnyPressed: Boolean
167
168 /**
169 * Returns the index of first button pressed as used in [isPressed] or `-1` if no button is pressed.
170 */
171 expect fun PointerButtons.indexOfFirstPressed(): Int
172
173 /**
174 * Returns the index of last button pressed as used in [isPressed] or `-1` if no button is pressed.
175 */
176 expect fun PointerButtons.indexOfLastPressed(): Int
177
178 // TODO mark internal once https://youtrack.jetbrains.com/issue/KT-36695 is fixed
179 /* internal */ expect class NativePointerKeyboardModifiers
180
181 /**
182 * Contains the state of modifier keys, such as Shift, Control, and Alt, as well as the state of the
183 * lock keys, such as Caps Lock and Num Lock.
184 */
185 @kotlin.jvm.JvmInline
186 value class PointerKeyboardModifiers(internal val packedValue: NativePointerKeyboardModifiers)
187
188 // helps initialize `WindowInfo.keyboardModifiers` with a non-null value
189 internal expect fun EmptyPointerKeyboardModifiers(): PointerKeyboardModifiers
190
191 /** `true` when the Control key is pressed. */
192 expect val PointerKeyboardModifiers.isCtrlPressed: Boolean
193
194 /**
195 * `true` when the Meta key is pressed. This is commonly associated with the Windows or Command key
196 * on some keyboards.
197 */
198 expect val PointerKeyboardModifiers.isMetaPressed: Boolean
199
200 /**
201 * `true` when the Alt key is pressed. This is commonly associated with the Option key on some
202 * keyboards.
203 */
204 expect val PointerKeyboardModifiers.isAltPressed: Boolean
205
206 /** `true` when the AltGraph key is pressed. */
207 expect val PointerKeyboardModifiers.isAltGraphPressed: Boolean
208
209 /** `true` when the Sym key is pressed. */
210 expect val PointerKeyboardModifiers.isSymPressed: Boolean
211
212 /** `true` when the Shift key is pressed. */
213 expect val PointerKeyboardModifiers.isShiftPressed: Boolean
214
215 /** `true` when the Function key is pressed. */
216 expect val PointerKeyboardModifiers.isFunctionPressed: Boolean
217
218 /** `true` when the keyboard's Caps Lock is on. */
219 expect val PointerKeyboardModifiers.isCapsLockOn: Boolean
220
221 /** `true` when the keyboard's Scroll Lock is on. */
222 expect val PointerKeyboardModifiers.isScrollLockOn: Boolean
223
224 /** `true` when the keyboard's Num Lock is on. */
225 expect val PointerKeyboardModifiers.isNumLockOn: Boolean
226
227 /** The device type that produces a [PointerInputChange], such as a mouse or stylus. */
228 @kotlin.jvm.JvmInline
229 value class PointerType private constructor(private val value: Int) {
230
231 override fun toString(): String =
232 when (value) {
233 1 -> "Touch"
234 2 -> "Mouse"
235 3 -> "Stylus"
236 4 -> "Eraser"
237 else -> "Unknown"
238 }
239
240 companion object {
241 /** An unknown device type or the device type isn't relevant. */
242 val Unknown = PointerType(0)
243
244 /** Touch (finger) input. */
245 val Touch = PointerType(1)
246
247 /** A mouse pointer. */
248 val Mouse = PointerType(2)
249
250 /** A stylus. */
251 val Stylus = PointerType(3)
252
253 /** An eraser or an inverted stylus. */
254 val Eraser = PointerType(4)
255 }
256 }
257
258 /** Indicates the primary reason that the [PointerEvent] was sent. */
259 @kotlin.jvm.JvmInline
260 value class PointerEventType private constructor(internal val value: Int) {
261 companion object {
262 /** An unknown reason for the event. */
263 val Unknown = PointerEventType(0)
264
265 /** A button on the device was pressed or a new pointer was detected. */
266 val Press = PointerEventType(1)
267
268 /** A button on the device was released or a pointer was raised. */
269 val Release = PointerEventType(2)
270
271 /** The cursor or one or more touch pointers was moved. */
272 val Move = PointerEventType(3)
273
274 /**
275 * The cursor has entered the input region. This will only be sent after the cursor is
276 * hovering when in the input region.
277 *
278 * For example, the user's cursor is outside the input region and presses the button prior
279 * to entering the input region. The [Enter] event will be sent when the button is released
280 * inside the input region.
281 */
282 val Enter = PointerEventType(4)
283
284 /**
285 * A cursor device or elevated stylus exited the input region. This will only follow an
286 * [Enter] event, so if a cursor with the button pressed enters and exits region, neither
287 * [Enter] nor [Exit] will be sent for the input region. However, if a cursor enters the
288 * input region, then a button is pressed, then the cursor exits and reenters, [Enter],
289 * [Exit], and [Enter] will be received.
290 */
291 val Exit = PointerEventType(5)
292
293 /**
294 * A scroll event was sent. This can happen, for example, due to a mouse scroll wheel. This
295 * event indicates that the [PointerInputChange.scrollDelta]'s [Offset] is non-zero.
296 */
297 val Scroll = PointerEventType(6)
298 }
299
toStringnull300 override fun toString(): String =
301 when (this) {
302 Press -> "Press"
303 Release -> "Release"
304 Move -> "Move"
305 Enter -> "Enter"
306 Exit -> "Exit"
307 Scroll -> "Scroll"
308 else -> "Unknown"
309 }
310 }
311
312 /**
313 * Describes a change that has occurred for a particular pointer, as well as how much of the change
314 * has been consumed (meaning, used by a node in the UI).
315 *
316 * The [position] represents the position of the pointer relative to the element that this
317 * [PointerInputChange] is being dispatched to.
318 *
319 * Note 1: A [PointerEvent]'s [PointerEventType] is the cause of an event and the associated
320 * [PointerInputChange]'s properties reflecting that. Most of those are exclusive, in other words, a
321 * Press/Release [PointerEventType] will not cause a scrollDelta change and a Scroll
322 * [PointerEventType] will not cause a pressed or previousPressed change. However, either a a Scroll
323 * or a Press/Release may contain a position change in its [PointerInputChange]s. (You can imagine
324 * one finger moving while another is lifted up.)
325 *
326 * Examples of [PointerEventType] and the associated [PointerInputChange] property changes:
327 * - Press -> press will change (position may change) but scroll delta will not.
328 * - Release -> press will change (position may change) but scroll delta will not.
329 * - Move -> position will change but press and scroll delta will not.
330 * - Scroll -> scroll delta will change (position may change) but press will not.
331 *
332 * Note 2: The [position] values can be outside the actual bounds of the element itself meaning the
333 * numbers can be negative or larger than the element bounds.
334 *
335 * The [previousPosition] represents the position of the pointer offset to the current position of
336 * the pointer relative to the screen.
337 *
338 * This means that [position] and [previousPosition] can always be used to understand how much a
339 * pointer has moved relative to an element, even if that element is moving along with the changes
340 * to the pointer. For example, if a pointer touches a 1x1 pixel box in the middle, [position] will
341 * report a position of (0, 0) when dispatched to it. If the next event moves x position 5 pixels,
342 * [position] will report (5, 0) and [previousPosition] will report (0, 0). If the box moves all 5
343 * pixels, and the next event represents the pointer moving along the x axis for 5 more pixels,
344 * [position] will again report (5, 0) and [previousPosition] will report (0, 0).
345 *
346 * @param id The unique id of the pointer associated with this [PointerInputChange].
347 * @param uptimeMillis The time of the current pointer event, in milliseconds. The start (`0`) time
348 * is platform-dependent
349 * @param position The [Offset] of the current pointer event, relative to the containing element
350 * (values can be negative or larger than the element bounds).
351 * @param pressed `true` if the pointer event is considered "pressed." For example, finger touching
352 * the screen or a mouse button is pressed [pressed] would be `true`.
353 * @param pressure The pressure of the of the pointer event
354 * @param previousUptimeMillis The [uptimeMillis] of the previous pointer event
355 * @param previousPosition The [Offset] of the previous pointer event, offset to the [position] and
356 * relative to the containing element.
357 * @param previousPressed `true` if the pointer event was considered "pressed." For example , if a
358 * finger was touching the screen or a mouse button was pressed, [previousPressed] would be
359 * `true`.
360 * @param isInitiallyConsumed whether the change was consumed from the start or not. This value can
361 * change over time as change is propagated through the pointer handlers. To query the actual
362 * status of the change use [isConsumed]
363 * @param type The device type that produced the event, such as [mouse][PointerType.Mouse], or
364 * [touch][PointerType.Touch].git
365 * @param scrollDelta The amount of scroll wheel movement in the horizontal and vertical directions.
366 */
367 @Immutable
368 class PointerInputChange(
369 val id: PointerId,
370 val uptimeMillis: Long,
371 val position: Offset,
372 val pressed: Boolean,
373 val pressure: Float,
374 val previousUptimeMillis: Long,
375 val previousPosition: Offset,
376 val previousPressed: Boolean,
377 isInitiallyConsumed: Boolean,
378 val type: PointerType = PointerType.Touch,
379 val scrollDelta: Offset = Offset.Zero
380 ) {
381 constructor(
382 id: PointerId,
383 uptimeMillis: Long,
384 position: Offset,
385 pressed: Boolean,
386 previousUptimeMillis: Long,
387 previousPosition: Offset,
388 previousPressed: Boolean,
389 isInitiallyConsumed: Boolean,
390 type: PointerType = PointerType.Touch,
391 scrollDelta: Offset = Offset.Zero
392 ) : this(
393 id,
394 uptimeMillis,
395 position,
396 pressed,
397 pressure = 1.0f,
398 previousUptimeMillis,
399 previousPosition,
400 previousPressed,
401 isInitiallyConsumed,
402 type,
403 scrollDelta
404 )
405
406 @Deprecated(
407 level = DeprecationLevel.HIDDEN,
408 replaceWith =
409 ReplaceWith(
410 "this(id, uptimeMillis, position, pressed, previousUptimeMillis," +
411 " previousPosition, previousPressed," +
412 " consumed.downChange || consumed.positionChange, type, Offset.Zero)"
413 ),
414 message = "Use another constructor with `scrollDelta` and without `ConsumedData` instead"
415 )
416 @Suppress("DEPRECATION")
417 constructor(
418 id: PointerId,
419 uptimeMillis: Long,
420 position: Offset,
421 pressed: Boolean,
422 previousUptimeMillis: Long,
423 previousPosition: Offset,
424 previousPressed: Boolean,
425 consumed: ConsumedData,
426 type: PointerType = PointerType.Touch
427 ) : this(
428 id,
429 uptimeMillis,
430 position,
431 pressed,
432 pressure = 1.0f,
433 previousUptimeMillis,
434 previousPosition,
435 previousPressed,
436 consumed.downChange || consumed.positionChange,
437 type,
438 Offset.Zero
439 )
440
441 internal constructor(
442 id: PointerId,
443 uptimeMillis: Long,
444 position: Offset,
445 pressed: Boolean,
446 pressure: Float,
447 previousUptimeMillis: Long,
448 previousPosition: Offset,
449 previousPressed: Boolean,
450 isInitiallyConsumed: Boolean,
451 type: PointerType,
452 historical: List<HistoricalChange>,
453 scrollDelta: Offset,
454 originalEventPosition: Offset,
455 ) : this(
456 id,
457 uptimeMillis,
458 position,
459 pressed,
460 pressure,
461 previousUptimeMillis,
462 previousPosition,
463 previousPressed,
464 isInitiallyConsumed,
465 type,
466 scrollDelta
467 ) {
468 _historical = historical
469 this.originalEventPosition = originalEventPosition
470 }
471
472 /**
473 * Optional high-frequency pointer moves in between the last two dispatched events. Can be used
474 * for extra accuracy when touchscreen rate exceeds framerate.
475 */
476 // With these experimental annotations, the API can be either cleanly removed or
477 // stabilized. It doesn't appear in current.txt; and in experimental_current.txt,
478 // it has the same effect as a primary constructor val.
479 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
480 val historical: List<HistoricalChange>
481 get() = _historical ?: listOf()
482
483 private var _historical: List<HistoricalChange>? = null
484
485 internal var originalEventPosition: Offset = Offset.Zero
486
487 /**
488 * Indicates whether the change was consumed or not. Note that the change must be consumed in
489 * full as there's no partial consumption system provided.
490 */
491 val isConsumed: Boolean
492 get() = consumedDelegate?.isConsumed ?: (downChange || positionChange)
493
494 internal var downChange = isInitiallyConsumed
495 internal var positionChange = isInitiallyConsumed
496 // Used by shallow copies (see [copy]) to share the consumed state across pointer input changes
497 internal var consumedDelegate: PointerInputChange? = null
498
499 /**
500 * Consume change event, claiming all the corresponding change info to the caller. This is
501 * usually needed when, button, when being clicked, consumed the "up" event so no other parents
502 * of this button could consume this "up" again.
503 *
504 * "Consumption" is just an indication of the claim and each pointer input handler
505 * implementation must manually check this flag to respect it.
506 */
consumenull507 fun consume() {
508 if (consumedDelegate == null) {
509 downChange = true
510 positionChange = true
511 } else {
512 consumedDelegate?.consume()
513 }
514 }
515
516 @Suppress("DEPRECATION") private var _consumed: ConsumedData? = null
517
518 @Deprecated("use isConsumed and consume() pair of methods instead")
519 @Suppress("DEPRECATION")
520 val consumed: ConsumedData
521 get() {
522 if (_consumed == null) {
523 _consumed = ConsumedData(this)
524 }
525 return _consumed!!
526 }
527
528 @Deprecated(
529 level = DeprecationLevel.HIDDEN,
530 replaceWith =
531 ReplaceWith(
532 "copy(id,currentTime, currentPosition, currentPressed, previousTime," +
533 "previousPosition, previousPressed, consumed, type, this.scrollDelta)"
534 ),
535 message = "Use another copy() method with scrollDelta parameter instead"
536 )
537 @Suppress("DEPRECATION")
copynull538 fun copy(
539 id: PointerId = this.id,
540 currentTime: Long = this.uptimeMillis,
541 currentPosition: Offset = this.position,
542 currentPressed: Boolean = this.pressed,
543 previousTime: Long = this.previousUptimeMillis,
544 previousPosition: Offset = this.previousPosition,
545 previousPressed: Boolean = this.previousPressed,
546 consumed: ConsumedData = this.consumed,
547 type: PointerType = this.type,
548 ): PointerInputChange =
549 PointerInputChange(
550 id,
551 currentTime,
552 currentPosition,
553 currentPressed,
554 this.pressure,
555 previousTime,
556 previousPosition,
557 previousPressed,
558 consumed.downChange || consumed.positionChange,
559 type,
560 this.historical,
561 this.scrollDelta,
562 this.originalEventPosition,
563 )
564 .also {
565 // This method makes a deep copy, copy the consumed state directly without setting
566 // the consumed delegate, which is used for shallow copies.
567 it.positionChange = this.positionChange
568 it.downChange = this.downChange
569 }
570
571 /**
572 * Make a shallow copy of the [PointerInputChange]
573 *
574 * **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
575 * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
576 * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
577 * is not possible. Consider creating a new [PointerInputChange]
578 */
copynull579 fun copy(
580 id: PointerId = this.id,
581 currentTime: Long = this.uptimeMillis,
582 currentPosition: Offset = this.position,
583 currentPressed: Boolean = this.pressed,
584 previousTime: Long = this.previousUptimeMillis,
585 previousPosition: Offset = this.previousPosition,
586 previousPressed: Boolean = this.previousPressed,
587 type: PointerType = this.type,
588 scrollDelta: Offset = this.scrollDelta
589 ): PointerInputChange =
590 copy(
591 id = id,
592 currentTime = currentTime,
593 currentPosition = currentPosition,
594 currentPressed = currentPressed,
595 pressure = this.pressure,
596 previousTime = previousTime,
597 previousPosition = previousPosition,
598 previousPressed = previousPressed,
599 type = type,
600 historical = this.historical,
601 scrollDelta = scrollDelta
602 )
603 .also {
604 // This method makes a shallow copy, copy the delegate to share the consumed state
605 // across instances. The local consumed state is irrelevant since we won't look at
606 // it, meaning there's no need to copy positionChange and downChange.
607 it.consumedDelegate = this.consumedDelegate ?: this
608 }
609
610 @Suppress("DEPRECATION")
611 @Deprecated(
612 "Partial consumption has been deprecated. Use copy() instead without `consumed` " +
613 "parameter to create a shallow copy or a constructor to create a new " +
614 "PointerInputChange",
615 replaceWith =
616 ReplaceWith(
617 "copy(id, currentTime, currentPosition, currentPressed, previousTime, " +
618 "previousPosition, previousPressed, type, scrollDelta)"
619 )
620 )
copynull621 fun copy(
622 id: PointerId = this.id,
623 currentTime: Long = this.uptimeMillis,
624 currentPosition: Offset = this.position,
625 currentPressed: Boolean = this.pressed,
626 previousTime: Long = this.previousUptimeMillis,
627 previousPosition: Offset = this.previousPosition,
628 previousPressed: Boolean = this.previousPressed,
629 consumed: ConsumedData,
630 type: PointerType = this.type,
631 scrollDelta: Offset = this.scrollDelta
632 ): PointerInputChange =
633 PointerInputChange(
634 id,
635 currentTime,
636 currentPosition,
637 currentPressed,
638 this.pressure,
639 previousTime,
640 previousPosition,
641 previousPressed,
642 consumed.downChange || consumed.positionChange,
643 type,
644 this.historical,
645 scrollDelta,
646 this.originalEventPosition,
647 )
648 .also {
649 // This method makes a deep copy, copy the consumed state directly without setting
650 // the consumed delegate, which is used for shallow copies.
651 it.positionChange = this.positionChange
652 it.downChange = this.downChange
653 }
654
655 /**
656 * Make a shallow copy of the [PointerInputChange]
657 *
658 * **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
659 * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
660 * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
661 * is not possible. Consider creating a new [PointerInputChange].
662 */
copynull663 fun copy(
664 id: PointerId = this.id,
665 currentTime: Long = this.uptimeMillis,
666 currentPosition: Offset = this.position,
667 currentPressed: Boolean = this.pressed,
668 pressure: Float = this.pressure,
669 previousTime: Long = this.previousUptimeMillis,
670 previousPosition: Offset = this.previousPosition,
671 previousPressed: Boolean = this.previousPressed,
672 type: PointerType = this.type,
673 scrollDelta: Offset = this.scrollDelta
674 ): PointerInputChange =
675 PointerInputChange(
676 id,
677 currentTime,
678 currentPosition,
679 currentPressed,
680 pressure,
681 previousTime,
682 previousPosition,
683 previousPressed,
684 isInitiallyConsumed = false, // doesn't matter, we will copy the consumed booleans
685 type,
686 historical = this.historical,
687 scrollDelta,
688 this.originalEventPosition,
689 )
690 .also {
691 // This method makes a shallow copy, copy the delegate to share the consumed state
692 // across instances. The local consumed state is irrelevant since we won't look at
693 // it, meaning there's no need to copy positionChange and downChange.
694 it.consumedDelegate = this.consumedDelegate ?: this
695 }
696
697 /**
698 * Make a shallow copy of the [PointerInputChange]
699 *
700 * **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
701 * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
702 * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
703 * is not possible. Consider creating a new [PointerInputChange].
704 */
705 @ExperimentalComposeUiApi
copynull706 fun copy(
707 id: PointerId = this.id,
708 currentTime: Long = this.uptimeMillis,
709 currentPosition: Offset = this.position,
710 currentPressed: Boolean = this.pressed,
711 previousTime: Long = this.previousUptimeMillis,
712 previousPosition: Offset = this.previousPosition,
713 previousPressed: Boolean = this.previousPressed,
714 type: PointerType = this.type,
715 historical: List<HistoricalChange>,
716 scrollDelta: Offset = this.scrollDelta
717 ): PointerInputChange =
718 copy(
719 id = id,
720 currentTime = currentTime,
721 currentPosition = currentPosition,
722 currentPressed = currentPressed,
723 pressure = this.pressure,
724 previousTime = previousTime,
725 previousPosition = previousPosition,
726 previousPressed = previousPressed,
727 type = type,
728 historical = historical,
729 scrollDelta = scrollDelta
730 )
731 .also {
732 // This method makes a shallow copy, copy the delegate to share the consumed state
733 // across instances. The local consumed state is irrelevant since we won't look at
734 // it, meaning there's no need to copy positionChange and downChange.
735 it.consumedDelegate = this.consumedDelegate ?: this
736 }
737
738 /**
739 * Make a shallow copy of the [PointerInputChange]
740 *
741 * **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
742 * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
743 * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
744 * is not possible. Consider creating a new [PointerInputChange].
745 */
copynull746 fun copy(
747 id: PointerId = this.id,
748 currentTime: Long = this.uptimeMillis,
749 currentPosition: Offset = this.position,
750 currentPressed: Boolean = this.pressed,
751 pressure: Float = this.pressure,
752 previousTime: Long = this.previousUptimeMillis,
753 previousPosition: Offset = this.previousPosition,
754 previousPressed: Boolean = this.previousPressed,
755 type: PointerType = this.type,
756 historical: List<HistoricalChange> = this.historical,
757 scrollDelta: Offset = this.scrollDelta
758 ): PointerInputChange =
759 PointerInputChange(
760 id,
761 currentTime,
762 currentPosition,
763 currentPressed,
764 pressure,
765 previousTime,
766 previousPosition,
767 previousPressed,
768 isInitiallyConsumed = false, // doesn't matter, we will copy the consumed booleans
769 type,
770 historical,
771 scrollDelta,
772 originalEventPosition = this.originalEventPosition,
773 )
774 .also {
775 // This method makes a shallow copy, copy the delegate to share the consumed state
776 // across instances. The local consumed state is irrelevant since we won't look at
777 // it, meaning there's no need to copy positionChange and downChange.
778 it.consumedDelegate = this.consumedDelegate ?: this
779 }
780
toStringnull781 override fun toString(): String {
782 return "PointerInputChange(id=$id, " +
783 "uptimeMillis=$uptimeMillis, " +
784 "position=$position, " +
785 "pressed=$pressed, " +
786 "pressure=$pressure, " +
787 "previousUptimeMillis=$previousUptimeMillis, " +
788 "previousPosition=$previousPosition, " +
789 "previousPressed=$previousPressed, " +
790 "isConsumed=$isConsumed, " +
791 "type=$type, " +
792 "historical=$historical," +
793 "scrollDelta=$scrollDelta)"
794 }
795 }
796
797 /**
798 * Data structure for "historical" pointer moves.
799 *
800 * Optional high-frequency pointer moves in between the last two dispatched events: can be used for
801 * extra accuracy when touchscreen rate exceeds framerate.
802 *
803 * @param uptimeMillis The time of the historical pointer event, in milliseconds. In between the
804 * current and previous pointer event times.
805 * @param position The [Offset] of the historical pointer event, relative to the containing element.
806 */
807 @Immutable
808 class HistoricalChange(val uptimeMillis: Long, val position: Offset) {
809 internal var originalEventPosition: Offset = Offset.Zero
810 private set
811
812 internal constructor(
813 uptimeMillis: Long,
814 position: Offset,
815 originalEventPosition: Offset
816 ) : this(uptimeMillis, position) {
817 this.originalEventPosition = originalEventPosition
818 }
819
toStringnull820 override fun toString(): String {
821 return "HistoricalChange(uptimeMillis=$uptimeMillis, position=$position)"
822 }
823 }
824
825 /**
826 * An ID for a given pointer.
827 *
828 * @param value The actual value of the id.
829 */
830 @kotlin.jvm.JvmInline value class PointerId(val value: Long)
831
832 /**
833 * Describes what aspects of a change has been consumed.
834 *
835 * @param positionChange True if a position change in this event has been consumed.
836 * @param downChange True if a change to down or up has been consumed.
837 */
838 @Deprecated("Use PointerInputChange.isConsumed and PointerInputChange.consume() instead")
839 class ConsumedData(positionChange: Boolean = false, downChange: Boolean = false) {
840 private var change: PointerInputChange? = null
841
842 internal constructor(
843 change: PointerInputChange
844 ) : this(change.positionChange, change.downChange) {
845 this.change = change
846 }
847
848 @Suppress("GetterSetterNames")
849 @get:Suppress("GetterSetterNames")
850 @Deprecated(
851 "Partial consumption was deprecated. Use PointerEvent.isConsumed " +
852 "and PointerEvent.consume() instead."
853 )
854 var positionChange: Boolean = positionChange
855 get() = change?.consumedDelegate?.positionChange ?: (change?.positionChange ?: field)
856 set(value) {
857 change?.consumedDelegate?.positionChange = value
858 change?.positionChange = value
859 field = value
860 }
861
862 @Suppress("GetterSetterNames")
863 @get:Suppress("GetterSetterNames")
864 @Deprecated(
865 "Partial consumption was deprecated. Use PointerEvent.isConsumed " +
866 "and PointerEvent.consume() instead."
867 )
868 var downChange: Boolean = downChange
869 get() = change?.consumedDelegate?.downChange ?: (change?.downChange ?: field)
870 set(value) {
871 change?.consumedDelegate?.downChange = value
872 change?.downChange = value
873 field = value
874 }
875 }
876
877 /**
878 * The enumeration of passes where [PointerInputChange] traverses up and down the UI tree.
879 *
880 * PointerInputChanges traverse throw the hierarchy in the following passes:
881 * 1. [Initial]: Down the tree from ancestor to descendant.
882 * 2. [Main]: Up the tree from descendant to ancestor.
883 * 3. [Final]: Down the tree from ancestor to descendant.
884 *
885 * These passes serve the following purposes:
886 * 1. Initial: Allows ancestors to consume aspects of [PointerInputChange] before descendants. This
887 * is where, for example, a scroller may block buttons from getting tapped by other fingers once
888 * scrolling has started.
889 * 2. Main: The primary pass where gesture filters should react to and consume aspects of
890 * [PointerInputChange]s. This is the primary path where descendants will interact with
891 * [PointerInputChange]s before parents. This allows for buttons to respond to a tap before a
892 * container of the bottom to respond to a tap.
893 * 3. Final: This pass is where children can learn what aspects of [PointerInputChange]s were
894 * consumed by parents during the [Main] pass. For example, this is how a button determines that
895 * it should no longer respond to fingers lifting off of it because a parent scroller has
896 * consumed movement in a [PointerInputChange].
897 */
898 enum class PointerEventPass {
899 Initial,
900 Main,
901 Final
902 }
903
904 /**
905 * True if this [PointerInputChange] represents a pointer coming in contact with the screen and that
906 * change has not been consumed.
907 */
PointerInputChangenull908 fun PointerInputChange.changedToDown() = !isConsumed && !previousPressed && pressed
909
910 /**
911 * True if this [PointerInputChange] represents a pointer coming in contact with the screen, whether
912 * or not that change has been consumed.
913 */
914 fun PointerInputChange.changedToDownIgnoreConsumed() = !previousPressed && pressed
915
916 /**
917 * True if this [PointerInputChange] represents a pointer breaking contact with the screen and that
918 * change has not been consumed.
919 */
920 fun PointerInputChange.changedToUp() = !isConsumed && previousPressed && !pressed
921
922 /**
923 * True if this [PointerInputChange] represents a pointer breaking contact with the screen, whether
924 * or not that change has been consumed.
925 */
926 fun PointerInputChange.changedToUpIgnoreConsumed() = previousPressed && !pressed
927
928 /**
929 * True if this [PointerInputChange] represents a pointer moving on the screen and some of that
930 * movement has not been consumed.
931 */
932 fun PointerInputChange.positionChanged() =
933 this.positionChangeInternal(false) != Offset.Companion.Zero
934
935 /**
936 * True if this [PointerInputChange] represents a pointer moving on the screen ignoring how much of
937 * that movement may have been consumed.
938 */
939 fun PointerInputChange.positionChangedIgnoreConsumed() =
940 this.positionChangeInternal(true) != Offset.Companion.Zero
941
942 /**
943 * The distance that the pointer has moved on the screen minus any distance that has been consumed.
944 */
945 fun PointerInputChange.positionChange() = this.positionChangeInternal(false)
946
947 /**
948 * The distance that the pointer has moved on the screen, ignoring the fact that it might have been
949 * consumed.
950 */
951 fun PointerInputChange.positionChangeIgnoreConsumed() = this.positionChangeInternal(true)
952
953 private fun PointerInputChange.positionChangeInternal(ignoreConsumed: Boolean = false): Offset {
954 val previousPosition = previousPosition
955 val currentPosition = position
956
957 val offset = currentPosition - previousPosition
958
959 return if (!ignoreConsumed && isConsumed) Offset.Zero else offset
960 }
961
962 /** True if this [PointerInputChange]'s movement has been consumed. */
963 @Deprecated(
964 "Partial consumption has been deprecated. Use isConsumed instead",
965 replaceWith = ReplaceWith("isConsumed")
966 )
positionChangeConsumednull967 fun PointerInputChange.positionChangeConsumed() = isConsumed
968
969 /** True if any aspect of this [PointerInputChange] has been consumed. */
970 @Deprecated(
971 "Partial consumption has been deprecated. Use isConsumed instead",
972 replaceWith = ReplaceWith("isConsumed")
973 )
974 fun PointerInputChange.anyChangeConsumed() = isConsumed
975
976 /**
977 * Consume the up or down change of this [PointerInputChange] if there is an up or down change to
978 * consume.
979 */
980 @Deprecated(
981 "Partial consumption has been deprecated. Use consume() instead.",
982 replaceWith = ReplaceWith("if (pressed != previousPressed) consume()")
983 )
984 fun PointerInputChange.consumeDownChange() {
985 if (pressed != previousPressed) {
986 consume()
987 }
988 }
989
990 /** Consume position change if there is any */
991 @Deprecated(
992 "Partial consumption has been deprecated. Use consume() instead.",
993 replaceWith = ReplaceWith("if (positionChange() != Offset.Zero) consume()")
994 )
PointerInputChangenull995 fun PointerInputChange.consumePositionChange() {
996 if (positionChange() != Offset.Zero) {
997 consume()
998 }
999 }
1000
1001 /** Consumes all changes associated with the [PointerInputChange] */
1002 @Deprecated("Use consume() instead", replaceWith = ReplaceWith("consume()"))
PointerInputChangenull1003 fun PointerInputChange.consumeAllChanges() {
1004 consume()
1005 }
1006
1007 /**
1008 * Returns `true` if the pointer has moved outside of the region of `(0, 0, size.width,
1009 * size.height)` or `false` if the current pointer is up or it is inside the given bounds.
1010 */
1011 @Deprecated(
1012 message = "Use isOutOfBounds() that supports minimum touch target",
1013 replaceWith = ReplaceWith("this.isOutOfBounds(size, extendedTouchPadding)")
1014 )
PointerInputChangenull1015 fun PointerInputChange.isOutOfBounds(size: IntSize): Boolean {
1016 val position = position
1017 val x = position.x
1018 val y = position.y
1019 val width = size.width
1020 val height = size.height
1021 // Branch-less
1022 return (x < 0f) or (x > width) or (y < 0f) or (y > height)
1023 }
1024
1025 /**
1026 * Returns `true` if the pointer has moved outside of the pointer region. For Touch events, this is
1027 * (-extendedTouchPadding.width, -extendedTouchPadding.height, size.width +
1028 * extendedTouchPadding.width, size.height + extendedTouchPadding.height) and for other events, this
1029 * is `(0, 0, size.width, size.height)`. Returns`false` if the current pointer is up or it is inside
1030 * the pointer region.
1031 */
PointerInputChangenull1032 fun PointerInputChange.isOutOfBounds(size: IntSize, extendedTouchPadding: Size): Boolean {
1033 // Set to 1 when the pointer type is touch, 0 otherwise
1034 // No-op at the CPU level
1035 val isTouch = (type == PointerType.Touch).toInt()
1036
1037 val position = position
1038 val x = position.x
1039 val y = position.y
1040
1041 // Set extentX to 0 when the pointer type is *not* touch
1042 val extentX = extendedTouchPadding.width * isTouch
1043 val maxX = size.width + extentX
1044
1045 // Set extentY to 0 when the pointer type is *not* touch
1046 val extentY = extendedTouchPadding.height * isTouch
1047 val maxY = size.height + extentY
1048
1049 // Don't branch
1050 return (x < -extentX) or (x > maxX) or (y < -extentY) or (y > maxY)
1051 }
1052
toIntnull1053 private inline fun Boolean.toInt() = if (this) 1 else 0
1054