1 /*
<lambda>null2 * Copyright 2024 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.spatial
18
19 import androidx.collection.IntObjectMap
20 import androidx.collection.intObjectMapOf
21 import androidx.collection.mutableObjectListOf
22 import androidx.compose.ui.ComposeUiFlags
23 import androidx.compose.ui.ExperimentalComposeUiApi
24 import androidx.compose.ui.currentTimeMillis
25 import androidx.compose.ui.geometry.MutableRect
26 import androidx.compose.ui.geometry.Offset
27 import androidx.compose.ui.graphics.Matrix
28 import androidx.compose.ui.graphics.isIdentity
29 import androidx.compose.ui.node.DelegatableNode
30 import androidx.compose.ui.node.DelegatableNode.RegistrationHandle
31 import androidx.compose.ui.node.LayoutNode
32 import androidx.compose.ui.node.NodeCoordinator
33 import androidx.compose.ui.node.Nodes
34 import androidx.compose.ui.postDelayed
35 import androidx.compose.ui.removePost
36 import androidx.compose.ui.unit.IntOffset
37 import androidx.compose.ui.unit.IntSize
38 import androidx.compose.ui.unit.plus
39 import androidx.compose.ui.unit.round
40 import androidx.compose.ui.unit.toOffset
41 import androidx.compose.ui.util.trace
42 import kotlin.math.max
43
44 internal class RectManager(
45 /** [LayoutNode.semanticsId] to [LayoutNode] mapping, maintained by Owner. */
46 private val layoutNodes: IntObjectMap<LayoutNode> = intObjectMapOf(),
47 ) {
48 val rects: RectList = RectList()
49
50 private val throttledCallbacks = ThrottledCallbacks()
51 private val callbacks = mutableObjectListOf<() -> Unit>()
52 private var isDirty = false
53 private var isScreenOrWindowDirty = false
54 private var isFragmented = false
55 private var dispatchToken: Any? = null
56 private var scheduledDispatchDeadline: Long = -1
57 private val dispatchLambda = {
58 dispatchToken = null
59 trace("OnPositionedDispatch") { dispatchCallbacks() }
60 }
61
62 fun invalidate() {
63 isDirty = true
64 }
65
66 fun updateOffsets(
67 screenOffset: IntOffset,
68 windowOffset: IntOffset,
69 viewToWindowMatrix: Matrix
70 ) {
71 val analysis = viewToWindowMatrix.analyzeComponents()
72 isScreenOrWindowDirty =
73 throttledCallbacks.updateOffsets(
74 screenOffset,
75 windowOffset,
76 if (analysis.hasNonTranslationComponents) viewToWindowMatrix else null,
77 ) || isScreenOrWindowDirty
78 }
79
80 // TODO: we need to make sure these are dispatched after draw if needed
81 fun dispatchCallbacks() {
82 val currentTime = currentTimeMillis()
83
84 // For ThrottledCallbacks on global changes we need to make sure they are all called for any
85 // change
86 val isDispatchGlobalCallbacks = isDirty || isScreenOrWindowDirty
87
88 // TODO: we need to move this to avoid double-firing
89 if (isDirty) {
90 isDirty = false
91 callbacks.forEach { it() }
92 rects.forEachUpdatedRect { id, topLeft, bottomRight ->
93 throttledCallbacks.fireOnUpdatedRect(id, topLeft, bottomRight, currentTime)
94 }
95 rects.clearUpdated()
96 }
97 if (isScreenOrWindowDirty) {
98 isScreenOrWindowDirty = false
99 throttledCallbacks.fireOnRectChangedEntries(currentTime)
100 }
101 if (isDispatchGlobalCallbacks) {
102 throttledCallbacks.fireGlobalChangeEntries(currentTime)
103 }
104 // The hierarchy is "settled" in terms of nodes being added/removed for this frame
105 // This makes it a reasonable time to "defragment" the RectList data structure. This
106 // will keep operations on this data structure efficient over time. This is a fairly
107 // cheap operation to run, so we just do it every time
108 if (isFragmented) {
109 isFragmented = false
110 // TODO: if we want to take advantage of the "generation", we will be motivated to
111 // call this less often. Alternatively we could track the number of remove() calls
112 // we have made and only call defragment once it exceeds a certain percentage of
113 // the overall list.
114 rects.defragment()
115 }
116 // this gets called frequently, but we might need to schedule it more often to ensure that
117 // debounced callbacks get fired
118 throttledCallbacks.triggerDebounced(currentTime)
119 }
120
121 fun scheduleDebounceCallback(ensureSomethingScheduled: Boolean) {
122 val canExitEarly = !ensureSomethingScheduled || dispatchToken != null
123 val nextDeadline = throttledCallbacks.minDebounceDeadline
124 if (nextDeadline < 0 && canExitEarly) {
125 return
126 }
127 val currentScheduledDeadline = scheduledDispatchDeadline
128 if (currentScheduledDeadline == nextDeadline && canExitEarly) {
129 return
130 }
131 if (dispatchToken != null) {
132 removePost(dispatchToken)
133 }
134 val currentTime = currentTimeMillis()
135 val nextFrameIsh = currentTime + 16
136 val deadline = max(nextDeadline, nextFrameIsh)
137 scheduledDispatchDeadline = deadline
138 val delay = deadline - currentTime
139 dispatchToken = postDelayed(delay, dispatchLambda)
140 }
141
142 fun currentRectInfo(id: Int, node: DelegatableNode): RelativeLayoutBounds? {
143 var result: RelativeLayoutBounds? = null
144 rects.withRect(id) { l, t, r, b ->
145 result =
146 rectInfoFor(
147 node = node,
148 topLeft = packXY(l, t),
149 bottomRight = packXY(r, b),
150 windowOffset = throttledCallbacks.windowOffset,
151 screenOffset = throttledCallbacks.screenOffset,
152 viewToWindowMatrix = throttledCallbacks.viewToWindowMatrix,
153 )
154 }
155 return result
156 }
157
158 fun registerOnChangedCallback(callback: () -> Unit): Any? {
159 callbacks.add(callback)
160 return callback
161 }
162
163 fun registerOnRectChangedCallback(
164 id: Int,
165 throttleMillis: Long,
166 debounceMillis: Long,
167 node: DelegatableNode,
168 callback: (RelativeLayoutBounds) -> Unit
169 ): RegistrationHandle {
170 return throttledCallbacks.registerOnRectChanged(
171 id,
172 throttleMillis,
173 debounceMillis,
174 node,
175 callback,
176 )
177 }
178
179 fun registerOnGlobalLayoutCallback(
180 id: Int,
181 throttleMillis: Long,
182 debounceMillis: Long,
183 node: DelegatableNode,
184 callback: (RelativeLayoutBounds) -> Unit
185 ): RegistrationHandle {
186 return throttledCallbacks.registerOnGlobalChange(
187 id = id,
188 throttleMillis = throttleMillis,
189 debounceMillis = debounceMillis,
190 node = node,
191 callback = callback,
192 )
193 }
194
195 fun unregisterOnChangedCallback(token: Any?) {
196 @Suppress("UNCHECKED_CAST")
197 token as? (() -> Unit) ?: return
198 callbacks.remove(token)
199 }
200
201 fun invalidateCallbacksFor(layoutNode: LayoutNode) {
202 isDirty = true
203 rects.markUpdated(layoutNode.semanticsId)
204 scheduleDebounceCallback(ensureSomethingScheduled = true)
205 }
206
207 fun updateFlagsFor(layoutNode: LayoutNode, focusable: Boolean, gesturable: Boolean) {
208 if (layoutNode.isAttached) {
209 rects.updateFlagsFor(
210 value = layoutNode.semanticsId,
211 focusable = focusable,
212 gesturable = gesturable
213 )
214 }
215 }
216
217 fun onLayoutLayerPositionalPropertiesChanged(layoutNode: LayoutNode) {
218 @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
219 val outerToInnerOffset = layoutNode.outerToInnerOffset()
220 if (outerToInnerOffset.isSet) {
221 // translational properties only. AABB still valid.
222 layoutNode.outerToInnerOffset = outerToInnerOffset
223 layoutNode.outerToInnerOffsetDirty = false
224 layoutNode.forEachChild {
225 // NOTE: this calls rectlist.move(...) so does not need to be recursive
226 // TODO: we could potentially move to a single call of `updateSubhierarchy(...)`
227 onLayoutPositionChanged(it, it.outerCoordinator.position, false)
228 }
229 invalidateCallbacksFor(layoutNode)
230 } else {
231 // there are rotations/skews/scales going on, so we need to do a more expensive update
232 insertOrUpdateTransformedNodeSubhierarchy(layoutNode)
233 }
234 }
235
236 fun onLayoutPositionChanged(
237 layoutNode: LayoutNode,
238 position: IntOffset,
239 firstPlacement: Boolean
240 ) {
241 @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
242 // Our goal here is to get the right "root" coordinates for every layout. We can use
243 // LayoutCoordinates.localToRoot to calculate this somewhat readily, however this function
244 // is getting called with a very high frequency and so it is important that extracting these
245 // coordinates remains relatively cheap to limit the overhead of this tracking. The
246 // LayoutCoordinates will traverse up the entire "spine" of the hierarchy, so as we do this
247 // calculation for many nodes, we would be making many redundant calculations. In order to
248 // minimize this, we store the "offsetFromRoot" of each layout node as we calculate it, and
249 // attempt to utilize this value when calculating it for a node that is below it.
250 // Additionally, we calculate and cache the parent's "outer to inner offset" which may
251 val delegate = layoutNode.measurePassDelegate
252 val width = delegate.measuredWidth
253 val height = delegate.measuredHeight
254
255 val parent = layoutNode.parent
256 val offset: IntOffset
257
258 val lastOffset = layoutNode.offsetFromRoot
259 val lastSize = layoutNode.lastSize
260 val lastWidth = lastSize.width
261 val lastHeight = lastSize.height
262
263 var hasNonTranslationTransformations = false
264
265 if (parent != null) {
266 val parentOffsetDirty = parent.outerToInnerOffsetDirty
267 val parentOffset = parent.offsetFromRoot
268 val prevOuterToInnerOffset = parent.outerToInnerOffset
269
270 offset =
271 if (parentOffset.isSet) {
272 val parentOuterInnerOffset =
273 if (parentOffsetDirty) {
274 val it = parent.outerToInnerOffset()
275
276 parent.outerToInnerOffset = it
277 parent.outerToInnerOffsetDirty = false
278 it
279 } else {
280 prevOuterToInnerOffset
281 }
282 hasNonTranslationTransformations = !parentOuterInnerOffset.isSet
283 parentOffset + parentOuterInnerOffset + position
284 } else {
285 layoutNode.outerCoordinator.positionInRoot()
286 }
287 } else {
288 // root
289 offset = position
290 }
291
292 // If unset is returned then that means there is a rotation/skew/scale
293 if (hasNonTranslationTransformations || !offset.isSet) {
294 insertOrUpdateTransformedNode(layoutNode, position, firstPlacement)
295 return
296 }
297
298 layoutNode.offsetFromRoot = offset
299 layoutNode.lastSize = IntSize(width, height)
300
301 val l = offset.x
302 val t = offset.y
303 val r = l + width
304 val b = t + height
305
306 if (!firstPlacement && offset == lastOffset && lastWidth == width && lastHeight == height) {
307 return
308 }
309
310 insertOrUpdate(layoutNode, firstPlacement, l, t, r, b)
311 }
312
313 private fun insertOrUpdateTransformedNodeSubhierarchy(layoutNode: LayoutNode) {
314 layoutNode.forEachChild {
315 insertOrUpdateTransformedNode(it, it.outerCoordinator.position, false)
316 insertOrUpdateTransformedNodeSubhierarchy(it)
317 }
318 }
319
320 private val cachedRect = MutableRect(0f, 0f, 0f, 0f)
321
322 private fun insertOrUpdateTransformedNode(
323 layoutNode: LayoutNode,
324 position: IntOffset,
325 firstPlacement: Boolean,
326 ) {
327 val coord = layoutNode.outerCoordinator
328 val delegate = layoutNode.measurePassDelegate
329 val width = delegate.measuredWidth
330 val height = delegate.measuredHeight
331 val rect = cachedRect
332
333 rect.set(
334 left = position.x.toFloat(),
335 top = position.y.toFloat(),
336 right = (position.x + width).toFloat(),
337 bottom = (position.y + height).toFloat(),
338 )
339
340 coord.boundingRectInRoot(rect)
341
342 val l = rect.left.toInt()
343 val t = rect.top.toInt()
344 val r = rect.right.toInt()
345 val b = rect.bottom.toInt()
346 val id = layoutNode.semanticsId
347 // NOTE: we call update here instead of move since the subhierarchy will not be moved by a
348 // simple delta since we are dealing with rotation/skew/scale/etc.
349 if (firstPlacement || !rects.update(id, l, t, r, b)) {
350 val parentId = layoutNode.parent?.semanticsId ?: -1
351 rects.insert(
352 id,
353 l,
354 t,
355 r,
356 b,
357 parentId = parentId,
358 focusable = layoutNode.nodes.has(Nodes.FocusTarget),
359 gesturable = layoutNode.nodes.has(Nodes.PointerInput)
360 )
361 }
362 invalidate()
363 }
364
365 private fun insertOrUpdate(
366 layoutNode: LayoutNode,
367 firstPlacement: Boolean,
368 l: Int,
369 t: Int,
370 r: Int,
371 b: Int,
372 ) {
373 val id = layoutNode.semanticsId
374 if (firstPlacement || !rects.move(id, l, t, r, b)) {
375 val parentId = layoutNode.parent?.semanticsId ?: -1
376 rects.insert(
377 id,
378 l,
379 t,
380 r,
381 b,
382 parentId = parentId,
383 focusable = layoutNode.nodes.has(Nodes.FocusTarget),
384 gesturable = layoutNode.nodes.has(Nodes.PointerInput)
385 )
386 }
387 invalidate()
388 }
389
390 private fun NodeCoordinator.positionInRoot(): IntOffset {
391 // TODO: can we use offsetFromRoot here to speed up calculation?
392 var position = Offset.Zero
393 var coordinator: NodeCoordinator? = this
394 while (coordinator != null) {
395 val layer = coordinator.layer
396 position += coordinator.position
397 coordinator = coordinator.wrappedBy
398 if (layer != null) {
399 val matrix = layer.underlyingMatrix
400 val analysis = matrix.analyzeComponents()
401 if (analysis == 0b11) continue
402 val hasNonTranslationComponents = analysis and 0b10 == 0
403 if (hasNonTranslationComponents) {
404 return IntOffset.Max
405 }
406 position = matrix.map(position)
407 }
408 }
409 return position.round()
410 }
411
412 private fun NodeCoordinator.boundingRectInRoot(rect: MutableRect) {
413 // TODO: can we use offsetFromRoot here to speed up calculation?
414 var coordinator: NodeCoordinator? = this
415 while (coordinator != null) {
416 val layer = coordinator.layer
417 rect.translate(coordinator.position.toOffset())
418 coordinator = coordinator.wrappedBy
419 if (layer != null) {
420 val matrix = layer.underlyingMatrix
421 if (!matrix.isIdentity()) {
422 matrix.map(rect)
423 }
424 }
425 }
426 }
427
428 private fun LayoutNode.outerToInnerOffset(): IntOffset {
429 val terminator = outerCoordinator
430 var position = Offset.Zero
431 var coordinator: NodeCoordinator? = innerCoordinator
432 while (coordinator != null) {
433 if (coordinator === terminator) break
434 val layer = coordinator.layer
435 position += coordinator.position
436 coordinator = coordinator.wrappedBy
437 if (layer != null) {
438 val matrix = layer.underlyingMatrix
439 val analysis = matrix.analyzeComponents()
440 if (analysis.isIdentity) continue
441 if (analysis.hasNonTranslationComponents) {
442 return IntOffset.Max
443 }
444 position = matrix.map(position)
445 }
446 }
447 return position.round()
448 }
449
450 fun remove(layoutNode: LayoutNode) {
451 rects.remove(layoutNode.semanticsId)
452 invalidate()
453 isFragmented = true
454 }
455
456 /**
457 * For occlusion calculation, returns whether the [LayoutNode] corresponding to [targetId] is
458 * drawn first compared to the [LayoutNode] corresponding to [otherId].
459 *
460 * @return true when the target will be drawn first, false for everything else, including when
461 * either is an ancestor of the other.
462 */
463 internal fun isTargetDrawnFirst(targetId: Int, otherId: Int): Boolean {
464 var nodeA = layoutNodes[targetId] ?: return false
465 var nodeB = layoutNodes[otherId] ?: return false
466
467 if (nodeA.depth == 0 || nodeB.depth == 0) {
468 // Fail early when either is Root
469 return false
470 }
471
472 while (nodeA.depth > nodeB.depth) {
473 nodeA = nodeA.parent ?: return false // Early check avoids these null parent cases
474 }
475
476 if (nodeA === nodeB) {
477 // Node B was parent of Node A
478 return false
479 }
480
481 while (nodeB.depth > nodeA.depth) {
482 nodeB = nodeB.parent ?: return false
483 }
484
485 if (nodeA === nodeB) {
486 // Node A was parent of Node B
487 return false
488 }
489
490 // Keep track of the branching parent for both nodes, the draw order of these decide how
491 // [targetId] and [otherId] compare.
492 var lastParentA: LayoutNode = nodeA
493 var lastParentB: LayoutNode = nodeB
494
495 while (nodeA !== nodeB) {
496 lastParentA = nodeA
497 lastParentB = nodeB
498 // Null parent means we went past the root without finding common ancestor, shouldn't
499 // happen but we only return true when we know the target is drawn first
500 nodeA = nodeA.parent ?: return false
501 nodeB = nodeB.parent ?: return false
502 }
503
504 // Lower zIndex is drawn first, otherwise lower placeOrder decides draw order
505 if (lastParentA.measurePassDelegate.zIndex == lastParentB.measurePassDelegate.zIndex) {
506 return lastParentA.placeOrder < lastParentB.placeOrder
507 } else {
508 return lastParentA.measurePassDelegate.zIndex < lastParentB.measurePassDelegate.zIndex
509 }
510 }
511 }
512
513 /**
514 * Returns true if the offset is not IntOffset.Max. In this class we are using `IntOffset.Max` to be
515 * a sentinel value for "unspecified" so that we can avoid boxing.
516 */
517 private val IntOffset.isSet: Boolean
518 get() = this != IntOffset.Max
519
520 /**
521 * We have logic that looks at whether or not a Matrix is an identity matrix, in which case we avoid
522 * doing expensive matrix calculations. Additionally, even if the matrix is non-identity, we can
523 * avoid a lot of extra work if the matrix is only doing translations, and no rotations/skews/scale.
524 *
525 * Since checking for these conditions involves a lot of overlapping work, we have this bespoke
526 * function which will return an Int that encodes the answer to both questions. If the 2nd bit of
527 * the result is set, this means that there are no rotations/skews/scales. If the first bit of the
528 * result is set, it means that there are no translations.
529 *
530 * This also means that the result of `0b11` indicates that it is the identity matrix.
531 */
Matrixnull532 private fun Matrix.analyzeComponents(): Int {
533 // See top-level comment
534 val v = values
535 if (v.size < 16) return 0
536 val isIdentity3x3 =
537 v[0] == 1f &&
538 v[1] == 0f &&
539 v[2] == 0f &&
540 v[4] == 0f &&
541 v[5] == 1f &&
542 v[6] == 0f &&
543 v[8] == 0f &&
544 v[9] == 0f &&
545 v[10] == 1f
546
547 // translation components
548 val hasNoTranslationComponents = v[12] == 0f && v[13] == 0f && v[14] == 0f && v[15] == 1f
549
550 return isIdentity3x3.toInt() shl 1 or hasNoTranslationComponents.toInt()
551 }
552
553 @Suppress("NOTHING_TO_INLINE")
554 private inline val Int.isIdentity: Boolean
555 get() = this == 0b11
556
557 @Suppress("NOTHING_TO_INLINE")
558 private inline val Int.hasNonTranslationComponents: Boolean
559 get() = this and 0b10 == 0
560
toIntnull561 @Suppress("NOTHING_TO_INLINE") internal inline fun Boolean.toInt(): Int = if (this) 1 else 0
562