1 /*
<lambda>null2 * 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 package androidx.compose.ui.input.pointer
18
19 import android.os.Build
20 import android.view.KeyEvent
21 import android.view.MotionEvent
22 import android.view.MotionEvent.ACTION_SCROLL
23 import android.view.MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
24 import android.view.MotionEvent.CLASSIFICATION_DEEP_PRESS
25 import android.view.MotionEvent.CLASSIFICATION_NONE
26 import android.view.MotionEvent.CLASSIFICATION_PINCH
27 import android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
28 import androidx.annotation.IntDef
29 import androidx.collection.LongSparseArray
30 import androidx.compose.ui.util.fastForEach
31
32 internal actual typealias NativePointerButtons = Int
33
34 internal actual typealias NativePointerKeyboardModifiers = Int
35
36 /**
37 * Restricts Ints to `MotionEvent`'s classification types. See the
38 * [Android documentation on MotionEvent.getClassification()]
39 * (https://developer.android.com/reference/android/view/MotionEvent#getClassification()) for more
40 * details.
41 */
42 @IntDef(
43 CLASSIFICATION_NONE,
44 CLASSIFICATION_AMBIGUOUS_GESTURE,
45 CLASSIFICATION_DEEP_PRESS,
46 CLASSIFICATION_TWO_FINGER_SWIPE,
47 CLASSIFICATION_PINCH
48 )
49 @Retention(AnnotationRetention.SOURCE) // Only for compile-time checks
50 internal annotation class MotionEventClassification
51
52 /** Describes a pointer input change event that has occurred at a particular point in time. */
53 actual class PointerEvent
54 internal actual constructor(
55 /** The changes. */
56 actual val changes: List<PointerInputChange>,
57 internal val internalPointerEvent: InternalPointerEvent?
58 ) {
59 /**
60 * The underlying Android [MotionEvent] that triggered this [PointerEvent].
61 *
62 * This property provides access to the raw [MotionEvent] for retrieving platform-specific
63 * information not yet exposed by the Compose [PointerEvent] API (e.g., stylus tilt angle).
64 *
65 * **Important Considerations:**
66 * 1. **Read-Only:** The returned [MotionEvent] is strictly read-only. Modifying it will lead to
67 * unpredictable behavior.
68 * 2. **Transient:** Do not store a reference to this [MotionEvent]. The Android framework may
69 * recycle it, rendering its state undefined and causing errors if accessed later. Access the
70 * data only within the scope where the [PointerEvent] is received.
71 * 3. **Metadata Only:** This [MotionEvent] should *not* be used for primary input handling
72 * logic (e.g., determining pointer position or button presses). Rely on the properties of
73 * [PointerEvent] and [PointerInputChange] for this purpose. The [MotionEvent] is intended
74 * solely for accessing supplemental metadata.
75 * 4. **Nullability:** This property will be `null` in two cases:
76 * * The [PointerEvent] was fabricated within Compose (i.e., not directly from a system
77 * input event).
78 * * The [PointerEvent] has already been dispatched within the Compose input system. (See
79 * [androidx.compose.ui.samples.PointerEventMotionEventSample] for details).
80 *
81 * @sample androidx.compose.ui.samples.PointerEventMotionEventSample
82 */
83 val motionEvent: MotionEvent?
84 get() = internalPointerEvent?.motionEvent
85
86 /**
87 * Returns
88 * [`MotionEvent`'s classification](https://developer.android.com/reference/android/view/MotionEvent#getClassification()).
89 */
90 @get:MotionEventClassification
91 val classification: Int =
92 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
93 motionEvent?.classification ?: CLASSIFICATION_NONE
94 } else {
95 CLASSIFICATION_NONE // Return NONE for versions lower than Android Q
96 }
97
98 /** @param changes The changes. */
99 actual constructor(changes: List<PointerInputChange>) : this(changes, null)
100
101 actual val buttons = PointerButtons(motionEvent?.buttonState ?: 0)
102
103 actual val keyboardModifiers = PointerKeyboardModifiers(motionEvent?.metaState ?: 0)
104
105 actual var type: PointerEventType = calculatePointerEventType()
106 internal set
107
108 private fun calculatePointerEventType(): PointerEventType {
109 val motionEvent = motionEvent
110 if (motionEvent != null) {
111 return when (motionEvent.actionMasked) {
112 MotionEvent.ACTION_DOWN,
113 MotionEvent.ACTION_POINTER_DOWN -> PointerEventType.Press
114 MotionEvent.ACTION_UP,
115 MotionEvent.ACTION_POINTER_UP -> PointerEventType.Release
116 MotionEvent.ACTION_HOVER_MOVE,
117 MotionEvent.ACTION_MOVE -> PointerEventType.Move
118 MotionEvent.ACTION_HOVER_ENTER -> PointerEventType.Enter
119 MotionEvent.ACTION_HOVER_EXIT -> PointerEventType.Exit
120 ACTION_SCROLL -> PointerEventType.Scroll
121 else -> PointerEventType.Unknown
122 }
123 }
124 // Used for testing.
125 changes.fastForEach {
126 if (it.changedToUpIgnoreConsumed()) {
127 return PointerEventType.Release
128 }
129 if (it.changedToDownIgnoreConsumed()) {
130 return PointerEventType.Press
131 }
132 }
133 return PointerEventType.Move
134 }
135
136 // only because PointerEvent was a data class
137 fun component1(): List<PointerInputChange> = changes
138
139 // only because PointerEvent was a data class
140 fun copy(changes: List<PointerInputChange>, motionEvent: MotionEvent?): PointerEvent =
141 when (motionEvent) {
142 null -> PointerEvent(changes, null)
143 this.motionEvent -> PointerEvent(changes, internalPointerEvent)
144 else -> {
145 val changesArray = LongSparseArray<PointerInputChange>(changes.size)
146 val pointerEventData = ArrayList<PointerInputEventData>(changes.size)
147 changes.fastForEach { change ->
148 changesArray.put(change.id.value, change)
149 pointerEventData +=
150 PointerInputEventData(
151 change.id,
152 change.uptimeMillis,
153 change.position,
154 change.position,
155 change.pressed,
156 change.pressure,
157 change.type,
158 this.internalPointerEvent?.activeHoverEvent(change.id) == true
159 )
160 }
161
162 val pointerInputEvent =
163 PointerInputEvent(motionEvent.eventTime, pointerEventData, motionEvent)
164 val event = InternalPointerEvent(changesArray, pointerInputEvent)
165 PointerEvent(changes, event)
166 }
167 }
168 }
169
EmptyPointerKeyboardModifiersnull170 internal actual fun EmptyPointerKeyboardModifiers() = PointerKeyboardModifiers(0)
171
172 actual val PointerButtons.isPrimaryPressed: Boolean
173 get() = packedValue and (MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_STYLUS_PRIMARY) != 0
174
175 actual val PointerButtons.isSecondaryPressed: Boolean
176 get() =
177 packedValue and (MotionEvent.BUTTON_SECONDARY or MotionEvent.BUTTON_STYLUS_SECONDARY) != 0
178
179 actual val PointerButtons.isTertiaryPressed: Boolean
180 get() = packedValue and MotionEvent.BUTTON_TERTIARY != 0
181
182 actual val PointerButtons.isBackPressed: Boolean
183 get() = packedValue and MotionEvent.BUTTON_BACK != 0
184
185 actual val PointerButtons.isForwardPressed: Boolean
186 get() = packedValue and MotionEvent.BUTTON_FORWARD != 0
187
188 actual fun PointerButtons.isPressed(buttonIndex: Int): Boolean =
189 when (buttonIndex) {
190 0 -> isPrimaryPressed
191 1 -> isSecondaryPressed
192 2,
193 3,
194 4 -> packedValue and (1 shl buttonIndex) != 0
195 else -> packedValue and (1 shl (buttonIndex + 2)) != 0
196 }
197
198 actual val PointerButtons.areAnyPressed: Boolean
199 get() = packedValue != 0
200
indexOfFirstPressednull201 actual fun PointerButtons.indexOfFirstPressed(): Int {
202 if (packedValue == 0) {
203 return -1
204 }
205 var index = 0
206 // shift stylus primary and secondary to primary and secondary
207 var shifted = ((packedValue and 0x60) ushr 5) or (packedValue and 0x60.inv())
208 while (shifted and 1 == 0) {
209 index++
210 shifted = shifted ushr 1
211 }
212 return index
213 }
214
indexOfLastPressednull215 actual fun PointerButtons.indexOfLastPressed(): Int {
216 // shift stylus primary and secondary to primary and secondary
217 var shifted = ((packedValue and 0x60) ushr 5) or (packedValue and 0x60.inv())
218 var index = -1
219 while (shifted != 0) {
220 index++
221 shifted = shifted ushr 1
222 }
223 return index
224 }
225
226 actual val PointerKeyboardModifiers.isCtrlPressed: Boolean
227 get() = (packedValue and KeyEvent.META_CTRL_ON) != 0
228
229 actual val PointerKeyboardModifiers.isMetaPressed: Boolean
230 get() = (packedValue and KeyEvent.META_META_ON) != 0
231
232 actual val PointerKeyboardModifiers.isAltPressed: Boolean
233 get() = (packedValue and KeyEvent.META_ALT_ON) != 0
234
235 actual val PointerKeyboardModifiers.isAltGraphPressed: Boolean
236 get() = false
237
238 actual val PointerKeyboardModifiers.isSymPressed: Boolean
239 get() = (packedValue and KeyEvent.META_SYM_ON) != 0
240
241 actual val PointerKeyboardModifiers.isShiftPressed: Boolean
242 get() = (packedValue and KeyEvent.META_SHIFT_ON) != 0
243
244 actual val PointerKeyboardModifiers.isFunctionPressed: Boolean
245 get() = (packedValue and KeyEvent.META_FUNCTION_ON) != 0
246
247 actual val PointerKeyboardModifiers.isCapsLockOn: Boolean
248 get() = (packedValue and KeyEvent.META_CAPS_LOCK_ON) != 0
249
250 actual val PointerKeyboardModifiers.isScrollLockOn: Boolean
251 get() = (packedValue and KeyEvent.META_SCROLL_LOCK_ON) != 0
252
253 actual val PointerKeyboardModifiers.isNumLockOn: Boolean
254 get() = (packedValue and KeyEvent.META_NUM_LOCK_ON) != 0
255