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