1 /*
2 * 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.node
18
19 import androidx.collection.MutableLongList
20 import androidx.collection.MutableObjectList
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.util.unpackFloat1
23 import kotlin.math.min
24 import kotlin.math.sign
25
26 /**
27 * This tracks the hit test results to allow for minimum touch target and single-pass hit testing.
28 * If there is a hit at the minimum touch target, searching for a hit within the layout bounds can
29 * still continue, but the near miss is still tracked.
30 *
31 * The List<T> interface should only be used after hit testing has completed.
32 *
33 * @see LayoutNode.hitTest
34 * @see NodeCoordinator.hitTest
35 */
36 internal class HitTestResult : List<Modifier.Node> {
37 private var values = MutableObjectList<Any>(16)
38 // contains DistanceAndInLayer
39 private var distanceFromEdgeAndFlags = MutableLongList(16)
40 private var hitDepth = -1
41
42 override val size: Int
43 get() = values.size
44
45 /**
46 * `true` when there has been a direct hit within touch bounds ([hit] called) or `false`
47 * otherwise.
48 */
hasHitnull49 fun hasHit(): Boolean {
50 val distance = findBestHitDistance()
51 return distance.distance < 0f && distance.isInLayer && !distance.isInExpandedBounds
52 }
53
54 /**
55 * Accepts all hits for a child and moves the hit depth. This should only be called within
56 * [siblingHits] to allow multiple siblings to have hit results.
57 */
acceptHitsnull58 fun acceptHits() {
59 hitDepth = size - 1
60 }
61
62 /**
63 * Returns `true` if [distanceFromEdge] is less than the previous value passed in
64 * [hitInMinimumTouchTarget] or [speculativeHit].
65 */
isHitInMinimumTouchTargetBetternull66 fun isHitInMinimumTouchTargetBetter(distanceFromEdge: Float, isInLayer: Boolean): Boolean {
67 if (hitDepth == lastIndex) {
68 return true
69 }
70 val distanceAndFlags = DistanceAndFlags(distanceFromEdge, isInLayer)
71 val bestDistance = findBestHitDistance()
72 return bestDistance > distanceAndFlags
73 }
74
findBestHitDistancenull75 private fun findBestHitDistance(): DistanceAndFlags {
76 var bestDistance = DistanceAndFlags(Float.POSITIVE_INFINITY, false)
77 for (i in hitDepth + 1..lastIndex) {
78 val distance = DistanceAndFlags(distanceFromEdgeAndFlags[i])
79 bestDistance = if (distance < bestDistance) distance else bestDistance
80 if (bestDistance.distance < 0f && bestDistance.isInLayer) {
81 return bestDistance
82 }
83 }
84 return bestDistance
85 }
86
87 /**
88 * Records [node] as a hit, adding it to the [HitTestResult] or replacing the existing one. Runs
89 * [childHitTest] to do further hit testing for children.
90 */
hitnull91 inline fun hit(node: Modifier.Node, isInLayer: Boolean, childHitTest: () -> Unit) {
92 hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
93 }
94
hitInMinimumTouchTargetnull95 inline fun hitInMinimumTouchTarget(
96 node: Modifier.Node,
97 distanceFromEdge: Float,
98 isInLayer: Boolean,
99 childHitTest: () -> Unit
100 ) = hitInMinimumTouchTarget(node, distanceFromEdge, isInLayer, false, childHitTest)
101
102 /**
103 * Records [node] as a hit with [distanceFromEdge] distance, replacing any existing record. Runs
104 * [childHitTest] to do further hit testing for children.
105 */
106 inline fun hitInMinimumTouchTarget(
107 node: Modifier.Node,
108 distanceFromEdge: Float,
109 isInLayer: Boolean,
110 isInExpandedBounds: Boolean,
111 childHitTest: () -> Unit
112 ) {
113 val startDepth = hitDepth
114 removeNodesInRange(hitDepth + 1, size)
115 hitDepth++
116 values.add(node)
117 distanceFromEdgeAndFlags.add(
118 DistanceAndFlags(distanceFromEdge, isInLayer, isInExpandedBounds).packedValue
119 )
120 childHitTest()
121 hitDepth = startDepth
122 }
123
124 /**
125 * Records a hit in an expanded touch bound. It's similar to a speculative hit, except that if
126 * the previous hit is also in expanded touch, it'll share the pointer.
127 */
hitExpandedTouchBoundsnull128 fun hitExpandedTouchBounds(node: Modifier.Node, isInLayer: Boolean, childHitTest: () -> Unit) {
129 if (hitDepth == lastIndex) {
130 // No hit test on siblings yet, simply record hit on this node.
131 hitInMinimumTouchTarget(node, 0f, isInLayer, isInExpandedBounds = true, childHitTest)
132 return
133 }
134
135 val previousDistance = findBestHitDistance()
136 val previousHitDepth = hitDepth
137
138 if (previousDistance.isInExpandedBounds) {
139 // Previous hits was in expanded bounds. Share the pointer unless the childHitTest has
140 // a direct hit.
141 // Accept the previous hit result for now, and shuffle the array only when we have a
142 // direct hit.
143 hitDepth = lastIndex
144 hitInMinimumTouchTarget(node, 0f, isInLayer, isInExpandedBounds = true, childHitTest)
145 val newDistance = findBestHitDistance()
146 if (newDistance.distance < 0) {
147 // Have a direct hit, remove the previous hit result.
148 val startIndex = previousHitDepth + 1
149 val endIndex = hitDepth + 1
150 removeNodesInRange(startIndex, endIndex)
151 }
152 hitDepth = previousHitDepth
153 } else if (previousDistance.distance > 0) {
154 // Previous hit is out of expanded bounds, clear the previous hit and record a hit for
155 // this node.
156 hitInMinimumTouchTarget(node, 0f, isInLayer, isInExpandedBounds = true, childHitTest)
157 }
158 // If previous hit is a direct hit on sibling, do nothing.
159 // This case should never happen, because when a node gets a direct hit, siblingHitTest will
160 // stop the hit test for other children.
161 }
162
163 /**
164 * Temporarily records [node] as a hit with [distanceFromEdge] distance and calls [childHitTest]
165 * to record hits for children. If no children have hits, then the hit is discarded. If a child
166 * had a hit, then [node] replaces an existing hit.
167 */
speculativeHitnull168 fun speculativeHit(
169 node: Modifier.Node,
170 distanceFromEdge: Float,
171 isInLayer: Boolean,
172 childHitTest: () -> Unit
173 ) {
174 if (hitDepth == lastIndex) {
175 // Speculation is easy. We don't have to do any array shuffling.
176 hitInMinimumTouchTarget(node, distanceFromEdge, isInLayer, childHitTest)
177 if (hitDepth + 1 == lastIndex || findBestHitDistance().isInExpandedBounds) {
178 // Discard the hit because there were no child hits or the child is hit in expanded
179 // touch bounds. Removing this node at hitDepth + 1 works for both cases.
180 // A parent can't intercept the event if child is at its expanded touch bounds.
181 // Note: We don't need to check whether this node intercepts child events,
182 // because speculativeHit() is only called when
183 // node.interceptOutOfBoundsChildEvents() returns true.
184 removeNodeAtDepth(hitDepth + 1)
185 }
186 return
187 }
188
189 // We have to track the speculation to the end of the array
190 val previousDistance = findBestHitDistance()
191 val previousHitDepth = hitDepth
192 hitDepth = lastIndex
193
194 hitInMinimumTouchTarget(node, distanceFromEdge, isInLayer, childHitTest)
195 val newBestDistance = findBestHitDistance()
196 if (hitDepth + 1 < lastIndex && previousDistance > newBestDistance) {
197 // This was a successful hit, so we should remove the previous hit from the hit path.
198 val startIndex = previousHitDepth + 1
199 val endIndex =
200 if (newBestDistance.isInExpandedBounds) {
201 // If the new hit is in expanded touch bounds, this node can't intercept the
202 // event. We'll remove this node as well.
203 hitDepth + 2
204 } else {
205 hitDepth + 1
206 }
207 removeNodesInRange(startIndex, endIndex)
208 } else {
209 // Previous hit is better, remove this hit result from the hit path.
210 removeNodesInRange(hitDepth + 1, size)
211 }
212 hitDepth = previousHitDepth
213 }
214
215 /**
216 * Util method to remove a node at the given depth. The given depth must be in the range of [0,
217 * size).
218 */
removeNodeAtDepthnull219 private fun removeNodeAtDepth(depth: Int) {
220 values.removeAt(depth)
221 distanceFromEdgeAndFlags.removeAt(depth)
222 }
223
224 /** Util method to remove nodes at the given depth range. */
removeNodesInRangenull225 private fun removeNodesInRange(startDepth: Int, endDepth: Int) {
226 if (startDepth >= endDepth) {
227 return
228 }
229 values.removeRange(start = startDepth, end = endDepth)
230 distanceFromEdgeAndFlags.removeRange(
231 start = startDepth,
232 end = endDepth,
233 )
234 }
235
236 /**
237 * Allow multiple sibling children to have a target hit within a Layout. Use [acceptHits] within
238 * [block] to mark a child's hits as accepted and proceed to hit test more children.
239 */
siblingHitsnull240 inline fun siblingHits(block: () -> Unit) {
241 val depth = hitDepth
242 block()
243 hitDepth = depth
244 }
245
containsnull246 override fun contains(element: Modifier.Node): Boolean = indexOf(element) != -1
247
248 override fun containsAll(elements: Collection<Modifier.Node>): Boolean {
249 elements.forEach {
250 if (!contains(it)) {
251 return false
252 }
253 }
254 return true
255 }
256
getnull257 override fun get(index: Int): Modifier.Node = values[index] as Modifier.Node
258
259 override fun indexOf(element: Modifier.Node): Int {
260 for (i in 0..lastIndex) {
261 if (values[i] == element) {
262 return i
263 }
264 }
265 return -1
266 }
267
isEmptynull268 override fun isEmpty(): Boolean = values.isEmpty()
269
270 override fun iterator(): Iterator<Modifier.Node> = HitTestResultIterator()
271
272 override fun lastIndexOf(element: Modifier.Node): Int {
273 for (i in lastIndex downTo 0) {
274 if (values[i] == element) {
275 return i
276 }
277 }
278 return -1
279 }
280
listIteratornull281 override fun listIterator(): ListIterator<Modifier.Node> = HitTestResultIterator()
282
283 override fun listIterator(index: Int): ListIterator<Modifier.Node> =
284 HitTestResultIterator(index)
285
286 override fun subList(fromIndex: Int, toIndex: Int): List<Modifier.Node> =
287 SubList(fromIndex, toIndex)
288
289 /** Clears all entries to make an empty list. */
290 fun clear() {
291 hitDepth = -1
292 values.clear()
293 distanceFromEdgeAndFlags.clear()
294 }
295
296 private inner class HitTestResultIterator(
297 var index: Int = 0,
298 val minIndex: Int = 0,
299 val maxIndex: Int = size
300 ) : ListIterator<Modifier.Node> {
hasNextnull301 override fun hasNext(): Boolean = index < maxIndex
302
303 override fun hasPrevious(): Boolean = index > minIndex
304
305 @Suppress("UNCHECKED_CAST")
306 override fun next(): Modifier.Node = values[index++] as Modifier.Node
307
308 override fun nextIndex(): Int = index - minIndex
309
310 @Suppress("UNCHECKED_CAST")
311 override fun previous(): Modifier.Node = values[--index] as Modifier.Node
312
313 override fun previousIndex(): Int = index - minIndex - 1
314 }
315
316 private inner class SubList(val minIndex: Int, val maxIndex: Int) : List<Modifier.Node> {
317 override val size: Int
318 get() = maxIndex - minIndex
319
320 override fun contains(element: Modifier.Node): Boolean = indexOf(element) != -1
321
322 override fun containsAll(elements: Collection<Modifier.Node>): Boolean {
323 elements.forEach {
324 if (!contains(it)) {
325 return false
326 }
327 }
328 return true
329 }
330
331 override fun get(index: Int): Modifier.Node = values[index + minIndex] as Modifier.Node
332
333 override fun indexOf(element: Modifier.Node): Int {
334 for (i in minIndex..maxIndex) {
335 if (values[i] == element) {
336 return i - minIndex
337 }
338 }
339 return -1
340 }
341
342 override fun isEmpty(): Boolean = size == 0
343
344 override fun iterator(): Iterator<Modifier.Node> =
345 HitTestResultIterator(minIndex, minIndex, maxIndex)
346
347 override fun lastIndexOf(element: Modifier.Node): Int {
348 for (i in maxIndex downTo minIndex) {
349 if (values[i] == element) {
350 return i - minIndex
351 }
352 }
353 return -1
354 }
355
356 override fun listIterator(): ListIterator<Modifier.Node> =
357 HitTestResultIterator(minIndex, minIndex, maxIndex)
358
359 override fun listIterator(index: Int): ListIterator<Modifier.Node> =
360 HitTestResultIterator(minIndex + index, minIndex, maxIndex)
361
362 override fun subList(fromIndex: Int, toIndex: Int): List<Modifier.Node> =
363 SubList(minIndex + fromIndex, minIndex + toIndex)
364 }
365 }
366
367 private const val IS_IN_LAYER: Long = 1L
368 private const val IS_IN_EXPANDED_BOUNDS: Long = 1 shl 1
369
370 @kotlin.jvm.JvmInline
371 internal value class DistanceAndFlags(val packedValue: Long) {
372 val distance: Float
373 get() = unpackFloat1(packedValue)
374
375 val isInLayer: Boolean
376 get() = (packedValue and IS_IN_LAYER) != 0L
377
378 val isInExpandedBounds: Boolean
379 get() = (packedValue and IS_IN_EXPANDED_BOUNDS) != 0L
380
compareTonull381 operator fun compareTo(other: DistanceAndFlags): Int {
382 val thisIsInLayer = isInLayer
383 val otherIsInLayer = other.isInLayer
384 if (thisIsInLayer != otherIsInLayer) {
385 return if (thisIsInLayer) -1 else 1
386 }
387 val distanceDiff = sign(this.distance - other.distance).toInt()
388 // One has a direct hit, use distance for comparison.
389 if (min(this.distance, other.distance) < 0f) {
390 return distanceDiff
391 }
392 // Both are out of bounds hit, the one in the expanded touch bounds wins.
393 if (this.isInExpandedBounds != other.isInExpandedBounds) {
394 return if (this.isInExpandedBounds) -1 else 1
395 }
396 return distanceDiff
397 }
398 }
399
DistanceAndFlagsnull400 private fun DistanceAndFlags(
401 distance: Float,
402 isInLayer: Boolean,
403 isInExpandedBounds: Boolean = false
404 ): DistanceAndFlags {
405 val v1 = distance.toRawBits().toLong()
406 var v2 = if (isInLayer) IS_IN_LAYER else 0L
407 v2 = v2 or if (isInExpandedBounds) IS_IN_EXPANDED_BOUNDS else 0L
408 return DistanceAndFlags(v1.shl(32) or (v2 and 0xFFFFFFFF))
409 }
410