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.collection.LongSparseArray
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.graphics.Matrix
24 import androidx.compose.ui.node.HitTestResult
25 import androidx.compose.ui.node.InternalCoreApi
26 import androidx.compose.ui.node.LayoutNode
27 import androidx.compose.ui.util.fastForEach
28
29 internal interface PositionCalculator {
screenToLocalnull30 fun screenToLocal(positionOnScreen: Offset): Offset
31
32 fun localToScreen(localPosition: Offset): Offset
33 }
34
35 internal interface MatrixPositionCalculator : PositionCalculator {
36
37 /**
38 * Takes a matrix which transforms some coordinate system to local coordinates, and updates the
39 * matrix to transform to screen coordinates instead.
40 */
41 fun localToScreen(localTransform: Matrix)
42 }
43
44 /** The core element that receives [PointerInputEvent]s and process them in Compose UI. */
45 internal class PointerInputEventProcessor(val root: LayoutNode) {
46
47 private val hitPathTracker = HitPathTracker(root.coordinates)
48 private val pointerInputChangeEventProducer = PointerInputChangeEventProducer()
49 private val hitResult = HitTestResult()
50
51 /**
52 * [process] doesn't currently support reentrancy. This prevents reentrant calls from causing a
53 * crash with an early exit.
54 */
55 private var isProcessing = false
56
57 /**
58 * Receives [PointerInputEvent]s and process them through the tree rooted on [root].
59 *
60 * @param pointerEvent The [PointerInputEvent] to process.
61 * @return the result of processing.
62 * @see ProcessResult
63 * @see PointerInputEvent
64 */
processnull65 fun process(
66 @OptIn(InternalCoreApi::class) pointerEvent: PointerInputEvent,
67 positionCalculator: PositionCalculator,
68 isInBounds: Boolean = true
69 ): ProcessResult {
70 if (isProcessing) {
71 // Processing currently does not support reentrancy.
72 return ProcessResult(
73 dispatchedToAPointerInputModifier = false,
74 anyMovementConsumed = false
75 )
76 }
77 try {
78 isProcessing = true
79
80 // Gets a new PointerInputChangeEvent with the PointerInputEvent.
81 @OptIn(InternalCoreApi::class)
82 val internalPointerEvent =
83 pointerInputChangeEventProducer.produce(pointerEvent, positionCalculator)
84
85 var isHover = true
86 for (i in 0 until internalPointerEvent.changes.size()) {
87 val pointerInputChange = internalPointerEvent.changes.valueAt(i)
88 if (pointerInputChange.pressed || pointerInputChange.previousPressed) {
89 isHover = false
90 break
91 }
92 }
93
94 // Add new hit paths to the tracker due to down events.
95 for (i in 0 until internalPointerEvent.changes.size()) {
96 val pointerInputChange = internalPointerEvent.changes.valueAt(i)
97 if (isHover || pointerInputChange.changedToDownIgnoreConsumed()) {
98 root.hitTest(pointerInputChange.position, hitResult, pointerInputChange.type)
99 if (hitResult.isNotEmpty()) {
100 hitPathTracker.addHitPath(
101 pointerId = pointerInputChange.id,
102 pointerInputNodes = hitResult,
103 // Prunes PointerIds (and changes) to support dynamically
104 // adding/removing pointer input modifier nodes.
105 // Note: We do not do this for hover because hover relies on those
106 // non hit PointerIds to trigger hover exit events.
107 prunePointerIdsAndChangesNotInNodesList =
108 pointerInputChange.changedToDownIgnoreConsumed()
109 )
110 hitResult.clear()
111 }
112 }
113 }
114
115 // Dispatch to PointerInputFilters
116 val dispatchedToSomething =
117 hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds)
118
119 val anyMovementConsumed =
120 if (internalPointerEvent.suppressMovementConsumption) {
121 false
122 } else {
123 var result = false
124 for (i in 0 until internalPointerEvent.changes.size()) {
125 val event = internalPointerEvent.changes.valueAt(i)
126 if (event.positionChangedIgnoreConsumed() && event.isConsumed) {
127 result = true
128 break
129 }
130 }
131 result
132 }
133
134 return ProcessResult(dispatchedToSomething, anyMovementConsumed)
135 } finally {
136 isProcessing = false
137 }
138 }
139
140 /**
141 * Responds appropriately to Android ACTION_CANCEL events.
142 *
143 * Specifically, [PointerInputFilter.onCancel] is invoked on tracked [PointerInputFilter]s and
144 * and this [PointerInputEventProcessor] is reset such that it is no longer tracking any
145 * [PointerInputFilter]s and expects the next [PointerInputEvent] it processes to represent only
146 * new pointers.
147 */
processCancelnull148 fun processCancel() {
149 if (!isProcessing) {
150 // Processing currently does not support reentrancy.
151 pointerInputChangeEventProducer.clear()
152 hitPathTracker.processCancel()
153 }
154 }
155
156 /**
157 * In some cases we need to clear the HIT Modifier.Node(s) cached from previous events because
158 * they are no longer relevant.
159 */
clearPreviouslyHitModifierNodesnull160 fun clearPreviouslyHitModifierNodes() {
161 hitPathTracker.clearPreviouslyHitModifierNodeCache()
162 }
163 }
164
165 /** Produces [InternalPointerEvent]s by tracking changes between [PointerInputEvent]s */
166 @OptIn(InternalCoreApi::class)
167 private class PointerInputChangeEventProducer {
168 private val previousPointerInputData: LongSparseArray<PointerInputData> = LongSparseArray()
169
170 /** Produces [InternalPointerEvent]s by tracking changes between [PointerInputEvent]s */
producenull171 fun produce(
172 pointerInputEvent: PointerInputEvent,
173 positionCalculator: PositionCalculator
174 ): InternalPointerEvent {
175 // Set initial capacity to avoid resizing - we know the size the map will be.
176 val changes: LongSparseArray<PointerInputChange> =
177 LongSparseArray(pointerInputEvent.pointers.size)
178 pointerInputEvent.pointers.fastForEach {
179 val previousTime: Long
180 val previousPosition: Offset
181 val previousDown: Boolean
182
183 val previousData = previousPointerInputData[it.id.value]
184 if (previousData == null) {
185 previousTime = it.uptime
186 previousPosition = it.position
187 previousDown = false
188 } else {
189 previousTime = previousData.uptime
190 previousDown = previousData.down
191 previousPosition = positionCalculator.screenToLocal(previousData.positionOnScreen)
192 }
193
194 changes.put(
195 it.id.value,
196 PointerInputChange(
197 it.id,
198 it.uptime,
199 it.position,
200 it.down,
201 it.pressure,
202 previousTime,
203 previousPosition,
204 previousDown,
205 false,
206 it.type,
207 it.historical,
208 it.scrollDelta,
209 it.originalEventPosition
210 )
211 )
212 if (it.down) {
213 previousPointerInputData.put(
214 it.id.value,
215 PointerInputData(it.uptime, it.positionOnScreen, it.down)
216 )
217 } else {
218 previousPointerInputData.remove(it.id.value)
219 }
220 }
221
222 return InternalPointerEvent(changes, pointerInputEvent)
223 }
224
225 /** Clears all tracked information. */
clearnull226 fun clear() {
227 previousPointerInputData.clear()
228 }
229
230 private class PointerInputData(
231 val uptime: Long,
232 val positionOnScreen: Offset,
233 val down: Boolean
234 )
235 }
236
237 /** The result of a call to [PointerInputEventProcessor.process]. */
238 @kotlin.jvm.JvmInline
239 internal value class ProcessResult(val value: Int) {
240 val dispatchedToAPointerInputModifier
241 inline get() = (value and 0x1) != 0
242
243 val anyMovementConsumed
244 inline get() = (value and 0x2) != 0
245 }
246
247 /**
248 * Constructs a new ProcessResult.
249 *
250 * @param dispatchedToAPointerInputModifier True if the dispatch resulted in at least 1
251 * [PointerInputModifier] receiving the event.
252 * @param anyMovementConsumed True if any movement occurred and was consumed.
253 */
ProcessResultnull254 internal fun ProcessResult(
255 dispatchedToAPointerInputModifier: Boolean,
256 anyMovementConsumed: Boolean
257 ): ProcessResult {
258 return ProcessResult(
259 dispatchedToAPointerInputModifier.toInt() or (anyMovementConsumed.toInt() shl 1)
260 )
261 }
262
toIntnull263 private inline fun Boolean.toInt() = if (this) 1 else 0
264