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