1 /*
<lambda>null2 * Copyright 2022 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.MutableObjectIntMap
20 import androidx.collection.mutableObjectIntMapOf
21 import androidx.compose.runtime.snapshots.Snapshot
22 import androidx.compose.ui.Modifier
23 import androidx.compose.ui.geometry.MutableRect
24 import androidx.compose.ui.geometry.Offset
25 import androidx.compose.ui.geometry.Rect
26 import androidx.compose.ui.geometry.Size
27 import androidx.compose.ui.geometry.isFinite
28 import androidx.compose.ui.geometry.isSpecified
29 import androidx.compose.ui.geometry.toRect
30 import androidx.compose.ui.graphics.Canvas
31 import androidx.compose.ui.graphics.DefaultCameraDistance
32 import androidx.compose.ui.graphics.GraphicsLayerScope
33 import androidx.compose.ui.graphics.Matrix
34 import androidx.compose.ui.graphics.Paint
35 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
36 import androidx.compose.ui.graphics.TransformOrigin
37 import androidx.compose.ui.graphics.layer.GraphicsLayer
38 import androidx.compose.ui.input.pointer.MatrixPositionCalculator
39 import androidx.compose.ui.input.pointer.PointerType
40 import androidx.compose.ui.internal.checkPrecondition
41 import androidx.compose.ui.internal.checkPreconditionNotNull
42 import androidx.compose.ui.internal.requirePrecondition
43 import androidx.compose.ui.layout.AlignmentLine
44 import androidx.compose.ui.layout.LayoutCoordinates
45 import androidx.compose.ui.layout.LookaheadLayoutCoordinates
46 import androidx.compose.ui.layout.Measurable
47 import androidx.compose.ui.layout.MeasureResult
48 import androidx.compose.ui.layout.Placeable
49 import androidx.compose.ui.layout.findRootCoordinates
50 import androidx.compose.ui.layout.positionInRoot
51 import androidx.compose.ui.layout.positionOnScreen
52 import androidx.compose.ui.ui.FrameRateCategory
53 import androidx.compose.ui.unit.Constraints
54 import androidx.compose.ui.unit.Density
55 import androidx.compose.ui.unit.IntOffset
56 import androidx.compose.ui.unit.IntSize
57 import androidx.compose.ui.unit.LayoutDirection
58 import androidx.compose.ui.unit.minus
59 import androidx.compose.ui.unit.plus
60 import androidx.compose.ui.unit.toSize
61 import androidx.compose.ui.util.fastIsFinite
62
63 /** Measurable and Placeable type that has a position. */
64 internal abstract class NodeCoordinator(
65 override val layoutNode: LayoutNode,
66 ) : LookaheadCapablePlaceable(), Measurable, LayoutCoordinates, OwnerScope {
67
68 internal var forcePlaceWithLookaheadOffset: Boolean = false
69 internal var forceMeasureWithLookaheadConstraints: Boolean = false
70 abstract val tail: Modifier.Node
71
72 internal var wrapped: NodeCoordinator? = null
73 internal var wrappedBy: NodeCoordinator? = null
74
75 override val layoutDirection: LayoutDirection
76 get() = layoutNode.layoutDirection
77
78 override val density: Float
79 get() = layoutNode.density.density
80
81 override val fontScale: Float
82 get() = layoutNode.density.fontScale
83
84 override val parent: LookaheadCapablePlaceable?
85 get() = wrappedBy
86
87 override val coordinates: LayoutCoordinates
88 get() = this
89
90 override val introducesMotionFrameOfReference: Boolean
91 get() = isPlacedUnderMotionFrameOfReference
92
93 private var released = false
94
95 private fun headNode(includeTail: Boolean): Modifier.Node? {
96 return if (layoutNode.outerCoordinator === this) {
97 layoutNode.nodes.head
98 } else if (includeTail) {
99 wrappedBy?.tail?.child
100 } else {
101 wrappedBy?.tail
102 }
103 }
104
105 inline fun visitNodes(mask: Int, includeTail: Boolean, block: (Modifier.Node) -> Unit) {
106 val stopNode = if (includeTail) tail else (tail.parent ?: return)
107 var node: Modifier.Node? = headNode(includeTail)
108 while (node != null) {
109 if (node.aggregateChildKindSet and mask == 0) return
110 if (node.kindSet and mask != 0) block(node)
111 if (node === stopNode) break
112 node = node.child
113 }
114 }
115
116 inline fun <reified T> visitNodes(type: NodeKind<T>, block: (T) -> Unit) {
117 visitNodes(type.mask, type.includeSelfInTraversal) { it.dispatchForKind(type, block) }
118 }
119
120 private fun hasNode(type: NodeKind<*>): Boolean {
121 return headNode(type.includeSelfInTraversal)?.has(type) == true
122 }
123
124 fun head(type: NodeKind<*>): Modifier.Node? {
125 visitNodes(type.mask, type.includeSelfInTraversal) {
126 return it
127 }
128 return null
129 }
130
131 // Size exposed to LayoutCoordinates.
132 final override val size: IntSize
133 get() = measuredSize
134
135 private var isClipping: Boolean = false
136
137 protected var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
138 private set
139
140 private var layerDensity: Density = layoutNode.density
141 private var layerLayoutDirection: LayoutDirection = layoutNode.layoutDirection
142
143 private var lastLayerAlpha: Float = 0.8f
144
145 fun isTransparent(): Boolean {
146 if (layer != null && lastLayerAlpha <= 0f) return true
147 return this.wrappedBy?.isTransparent() ?: return false
148 }
149
150 override val alignmentLinesOwner: AlignmentLinesOwner
151 get() = layoutNode.layoutDelegate.alignmentLinesOwner
152
153 override val child: LookaheadCapablePlaceable?
154 get() = wrapped
155
156 override fun replace() {
157 val explicitLayer = explicitLayer
158 if (explicitLayer != null) {
159 placeAt(position, zIndex, explicitLayer)
160 } else {
161 placeAt(position, zIndex, layerBlock)
162 }
163 }
164
165 override val hasMeasureResult: Boolean
166 get() = _measureResult != null
167
168 override val isAttached: Boolean
169 get() = tail.isAttached
170
171 private var _measureResult: MeasureResult? = null
172 override var measureResult: MeasureResult
173 get() = _measureResult ?: error(UnmeasuredError)
174 internal set(value) {
175 val old = _measureResult
176 if (value !== old) {
177 _measureResult = value
178 if (old == null || value.width != old.width || value.height != old.height) {
179 onMeasureResultChanged(value.width, value.height)
180 }
181 // We do not simply compare against old.alignmentLines in case this is a
182 // MutableStateMap and the same instance might be passed.
183 if (
184 ((oldAlignmentLines != null && oldAlignmentLines!!.isNotEmpty()) ||
185 value.alignmentLines.isNotEmpty()) &&
186 !compareEquals(oldAlignmentLines, value.alignmentLines)
187 ) {
188 alignmentLinesOwner.alignmentLines.onAlignmentsChanged()
189
190 @Suppress("PrimitiveInCollection")
191 val oldLines =
192 oldAlignmentLines
193 ?: (mutableObjectIntMapOf<AlignmentLine>().also {
194 oldAlignmentLines = it
195 })
196 oldLines.clear()
197 value.alignmentLines.forEach { entry -> oldLines[entry.key] = entry.value }
198 }
199 }
200 }
201
202 abstract var lookaheadDelegate: LookaheadDelegate?
203 protected set
204
205 private var oldAlignmentLines: MutableObjectIntMap<AlignmentLine>? = null
206
207 abstract fun ensureLookaheadDelegateCreated()
208
209 override val providedAlignmentLines: Set<AlignmentLine>
210 get() {
211 var set: MutableSet<AlignmentLine>? = null
212 var coordinator: NodeCoordinator? = this
213 while (coordinator != null) {
214 val alignmentLines = coordinator._measureResult?.alignmentLines
215 if (alignmentLines?.isNotEmpty() == true) {
216 if (set == null) {
217 set = mutableSetOf()
218 }
219 set.addAll(alignmentLines.keys)
220 }
221 coordinator = coordinator.wrapped
222 }
223 return set ?: emptySet()
224 }
225
226 /**
227 * Called when the width or height of [measureResult] change. The object instance pointed to by
228 * [measureResult] may or may not have changed.
229 */
230 protected open fun onMeasureResultChanged(width: Int, height: Int) {
231 val layer = layer
232 if (layer != null) {
233 layer.resize(IntSize(width, height))
234 } else {
235 // if the node is not placed then this change will not be visible
236 if (layoutNode.isPlaced) {
237 wrappedBy?.invalidateLayer()
238 }
239 }
240 measuredSize = IntSize(width, height)
241 if (layerBlock != null) {
242 updateLayerParameters(invokeOnLayoutChange = false)
243 }
244 visitNodes(Nodes.Draw) { it.onMeasureResultChanged() }
245 layoutNode.owner?.onLayoutChange(layoutNode)
246 }
247
248 override var position: IntOffset = IntOffset.Zero
249 protected set
250
251 var zIndex: Float = 0f
252 protected set
253
254 override val parentData: Any?
255 get() {
256 // NOTE: If you make changes to this getter, please check the generated bytecode to
257 // ensure no extra allocation is made. See the note below.
258 if (layoutNode.nodes.has(Nodes.ParentData)) {
259 val thisNode = tail
260 // NOTE: Keep this mutable variable scoped inside the if statement. When moved
261 // to the outer scope of get(), this causes the compiler to generate a
262 // Ref$ObjectRef instance on every call of this getter.
263 var data: Any? = null
264 layoutNode.nodes.tailToHead { node ->
265 if (node.isKind(Nodes.ParentData)) {
266 node.dispatchForKind(Nodes.ParentData) {
267 data = with(it) { layoutNode.density.modifyParentData(data) }
268 }
269 }
270 if (node === thisNode) return@tailToHead
271 }
272 return data
273 }
274 return null
275 }
276
277 internal fun onCoordinatesUsed() {
278 layoutNode.layoutDelegate.onCoordinatesUsed()
279 }
280
281 final override val parentLayoutCoordinates: LayoutCoordinates?
282 get() {
283 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
284 onCoordinatesUsed()
285 return layoutNode.outerCoordinator.wrappedBy
286 }
287
288 final override val parentCoordinates: LayoutCoordinates?
289 get() {
290 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
291 onCoordinatesUsed()
292 return wrappedBy
293 }
294
295 private var _rectCache: MutableRect? = null
296 protected val rectCache: MutableRect
297 get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also { _rectCache = it }
298
299 private val snapshotObserver
300 get() = layoutNode.requireOwner().snapshotObserver
301
302 /** The current layer's positional attributes. */
303 private var layerPositionalProperties: LayerPositionalProperties? = null
304
305 internal val lastMeasurementConstraints: Constraints
306 get() = measurementConstraints
307
308 protected inline fun performingMeasure(
309 constraints: Constraints,
310 crossinline block: () -> Placeable
311 ): Placeable {
312 measurementConstraints = constraints
313 return block()
314 }
315
316 fun onMeasured() {
317 if (hasNode(Nodes.LayoutAware)) {
318 Snapshot.withoutReadObservation {
319 visitNodes(Nodes.LayoutAware) { it.onRemeasured(measuredSize) }
320 }
321 }
322 }
323
324 fun onUnplaced() {
325 if (hasNode(Nodes.Unplaced)) {
326 visitNodes(Nodes.Unplaced) { it.onUnplaced() }
327 }
328 }
329
330 /** Places the modified child. */
331 /*@CallSuper*/
332 override fun placeAt(
333 position: IntOffset,
334 zIndex: Float,
335 layerBlock: (GraphicsLayerScope.() -> Unit)?
336 ) {
337 if (forcePlaceWithLookaheadOffset) {
338 placeSelf(lookaheadDelegate!!.position, zIndex, layerBlock, null)
339 } else {
340 placeSelf(position, zIndex, layerBlock, null)
341 }
342 }
343
344 override fun placeAt(position: IntOffset, zIndex: Float, layer: GraphicsLayer) {
345 if (forcePlaceWithLookaheadOffset) {
346 placeSelf(lookaheadDelegate!!.position, zIndex, null, layer)
347 } else {
348 placeSelf(position, zIndex, null, layer)
349 }
350 }
351
352 private fun placeSelf(
353 position: IntOffset,
354 zIndex: Float,
355 layerBlock: (GraphicsLayerScope.() -> Unit)?,
356 explicitLayer: GraphicsLayer?
357 ) {
358 if (explicitLayer != null) {
359 requirePrecondition(layerBlock == null) {
360 "both ways to create layers shouldn't be used together"
361 }
362 if (this.explicitLayer !== explicitLayer) {
363 // reset previous layer object first if the explicitLayer changed
364 this.explicitLayer = null
365 updateLayerBlock(null)
366 this.explicitLayer = explicitLayer
367 }
368 if (layer == null) {
369 layer =
370 layoutNode
371 .requireOwner()
372 .createLayer(drawBlock, invalidateParentLayer, explicitLayer)
373 .apply {
374 resize(measuredSize)
375 move(position)
376 }
377 layoutNode.innerLayerCoordinatorIsDirty = true
378 invalidateParentLayer()
379 }
380 } else {
381 if (this.explicitLayer != null) {
382 this.explicitLayer = null
383 // we need to first release the OwnedLayer created for explicitLayer
384 // as we don't support updating the same OwnedLayer object from using
385 // explicit layer to implicit one.
386 updateLayerBlock(null)
387 }
388 updateLayerBlock(layerBlock)
389 }
390 if (this.position != position) {
391 layoutNode.requireOwner().voteFrameRate(FrameRateCategory.High.value)
392 this.position = position
393 layoutNode.layoutDelegate.measurePassDelegate
394 .notifyChildrenUsingCoordinatesWhilePlacing()
395 val layer = layer
396 if (layer != null) {
397 layer.move(position)
398 } else {
399 wrappedBy?.invalidateLayer()
400 }
401 invalidateAlignmentLinesFromPositionChange()
402 layoutNode.owner?.onLayoutChange(layoutNode)
403 }
404 this.zIndex = zIndex
405 if (!isPlacingForAlignment) {
406 captureRulersIfNeeded(measureResult)
407 }
408 }
409
410 fun releaseLayer() {
411 if (layer != null) {
412 if (explicitLayer != null) {
413 explicitLayer = null
414 }
415 updateLayerBlock(null)
416
417 // as we removed the layer the node was placed with, we have to request relayout in
418 // case the node will be reused in future. during the relayout the layer will be
419 // recreated again if needed.
420 layoutNode.requestRelayout()
421 }
422 }
423
424 fun placeSelfApparentToRealOffset(
425 position: IntOffset,
426 zIndex: Float,
427 layerBlock: (GraphicsLayerScope.() -> Unit)?,
428 layer: GraphicsLayer?
429 ) {
430 placeSelf(position + apparentToRealOffset, zIndex, layerBlock, layer)
431 }
432
433 /** Draws the content of the LayoutNode */
434 fun draw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
435 val layer = layer
436 if (layer != null) {
437 layer.drawLayer(canvas, graphicsLayer)
438 } else {
439 val x = position.x.toFloat()
440 val y = position.y.toFloat()
441 canvas.translate(x, y)
442 drawContainedDrawModifiers(canvas, graphicsLayer)
443 canvas.translate(-x, -y)
444 }
445 }
446
447 private fun drawContainedDrawModifiers(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
448 val head = head(Nodes.Draw)
449 if (head == null) {
450 performDraw(canvas, graphicsLayer)
451 } else {
452 val drawScope = layoutNode.mDrawScope
453 drawScope.draw(canvas, size.toSize(), this, head, graphicsLayer)
454 }
455 }
456
457 open fun performDraw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
458 wrapped?.draw(canvas, graphicsLayer)
459 }
460
461 fun onPlaced() {
462 visitNodes(Nodes.LayoutAware) { it.onPlaced(this) }
463 }
464
465 private var drawBlockParentLayer: GraphicsLayer? = null
466 private var drawBlockCanvas: Canvas? = null
467
468 private var _drawBlock: ((Canvas, GraphicsLayer?) -> Unit)? = null
469
470 // implementation of draw block passed to the OwnedLayer
471 private val drawBlock: (Canvas, GraphicsLayer?) -> Unit
472 get() {
473 var block = _drawBlock
474 if (block == null) {
475 val drawBlockCallToDrawModifiers = {
476 drawContainedDrawModifiers(drawBlockCanvas!!, drawBlockParentLayer)
477 }
478 block = { canvas, parentLayer ->
479 if (layoutNode.isPlaced) {
480 this.drawBlockCanvas = canvas
481 this.drawBlockParentLayer = parentLayer
482 snapshotObserver.observeReads(
483 this,
484 onCommitAffectingLayer,
485 drawBlockCallToDrawModifiers
486 )
487 lastLayerDrawingWasSkipped = false
488 } else {
489 // The invalidation is requested even for nodes which are not placed. As we
490 // are not going to display them we skip the drawing. It is safe to just
491 // draw nothing as the layer will be invalidated again when the node will be
492 // finally placed.
493 lastLayerDrawingWasSkipped = true
494 }
495 }
496 _drawBlock = block
497 }
498 return block
499 }
500
501 fun updateLayerBlock(
502 layerBlock: (GraphicsLayerScope.() -> Unit)?,
503 forceUpdateLayerParameters: Boolean = false
504 ) {
505 requirePrecondition(layerBlock == null || explicitLayer == null) {
506 "layerBlock can't be provided when explicitLayer is provided"
507 }
508 val layoutNode = layoutNode
509 val updateParameters =
510 forceUpdateLayerParameters ||
511 this.layerBlock !== layerBlock ||
512 layerDensity != layoutNode.density ||
513 layerLayoutDirection != layoutNode.layoutDirection
514 this.layerDensity = layoutNode.density
515 this.layerLayoutDirection = layoutNode.layoutDirection
516
517 if (layoutNode.isAttached && layerBlock != null) {
518 this.layerBlock = layerBlock
519 if (layer == null) {
520 layer =
521 layoutNode
522 .requireOwner()
523 .createLayer(
524 drawBlock,
525 invalidateParentLayer,
526 forceUseOldLayers = layoutNode.forceUseOldLayers
527 )
528 .apply {
529 resize(measuredSize)
530 move(position)
531 }
532 updateLayerParameters()
533 layoutNode.innerLayerCoordinatorIsDirty = true
534 invalidateParentLayer()
535 } else if (updateParameters) {
536 val positionalPropertiesChanged = updateLayerParameters()
537 if (positionalPropertiesChanged) {
538 layoutNode
539 .requireOwner()
540 .rectManager
541 .onLayoutLayerPositionalPropertiesChanged(layoutNode)
542 }
543 }
544 } else {
545 this.layerBlock = null
546 layer?.let {
547 it.destroy()
548 layoutNode.innerLayerCoordinatorIsDirty = true
549 invalidateParentLayer()
550 if (isAttached && layoutNode.isPlaced) {
551 layoutNode.owner?.onLayoutChange(layoutNode)
552 }
553 }
554 layer = null
555 lastLayerDrawingWasSkipped = false
556 }
557 }
558
559 /** returns true if some of the positional properties did change. */
560 private fun updateLayerParameters(invokeOnLayoutChange: Boolean = true): Boolean {
561 if (explicitLayer != null) {
562 // the parameters of the explicit layers are configured differently.
563 return false
564 }
565 val layer = layer
566 if (layer != null) {
567 val layerBlock =
568 checkPreconditionNotNull(layerBlock) {
569 "updateLayerParameters requires a non-null layerBlock"
570 }
571 graphicsLayerScope.reset()
572 graphicsLayerScope.graphicsDensity = layoutNode.density
573 graphicsLayerScope.layoutDirection = layoutNode.layoutDirection
574 graphicsLayerScope.size = size.toSize()
575 snapshotObserver.observeReads(this, onCommitAffectingLayerParams) {
576 layerBlock.invoke(graphicsLayerScope)
577 graphicsLayerScope.updateOutline()
578 }
579 val layerPositionalProperties =
580 layerPositionalProperties
581 ?: LayerPositionalProperties().also { layerPositionalProperties = it }
582 tmpLayerPositionalProperties.copyFrom(layerPositionalProperties)
583 layerPositionalProperties.copyFrom(graphicsLayerScope)
584 layer.updateLayerProperties(graphicsLayerScope)
585 val wasClipping = isClipping
586 isClipping = graphicsLayerScope.clip
587 lastLayerAlpha = graphicsLayerScope.alpha
588 val positionalPropertiesChanged =
589 !tmpLayerPositionalProperties.hasSameValuesAs(layerPositionalProperties)
590 if (
591 invokeOnLayoutChange && (positionalPropertiesChanged || wasClipping != isClipping)
592 ) {
593 layoutNode.owner?.onLayoutChange(layoutNode)
594 }
595 return positionalPropertiesChanged
596 } else {
597 checkPrecondition(layerBlock == null) { "null layer with a non-null layerBlock" }
598 return false
599 }
600 }
601
602 private val invalidateParentLayer: () -> Unit = { wrappedBy?.invalidateLayer() }
603
604 /**
605 * True when the last drawing of this layer didn't draw the real content as the LayoutNode
606 * containing this layer was not placed by the parent.
607 */
608 internal var lastLayerDrawingWasSkipped = false
609 private set
610
611 var layer: OwnedLayer? = null
612 private set
613
614 private var explicitLayer: GraphicsLayer? = null
615
616 override val isValidOwnerScope: Boolean
617 get() = layer != null && !released && layoutNode.isAttached
618
619 val minimumTouchTargetSize: Size
620 get() = with(layerDensity) { layoutNode.viewConfiguration.minimumTouchTargetSize.toSize() }
621
622 fun onAttach() {
623 if (layer == null && layerBlock != null) {
624 // This has been detached and is now being reattached. It previously had a layer, so
625 // reconstitute one.
626 layer =
627 layoutNode
628 .requireOwner()
629 .createLayer(drawBlock, invalidateParentLayer, explicitLayer)
630 .apply {
631 resize(measuredSize)
632 move(position)
633 invalidate()
634 }
635 }
636 }
637
638 fun onDetach() {
639 layer?.destroy()
640 layer = null
641 }
642
643 /**
644 * Executes a hit test for this [NodeCoordinator].
645 *
646 * @param hitTestSource The hit test specifics for pointer input or semantics
647 * @param pointerPosition The tested pointer position, which is relative to the
648 * [NodeCoordinator].
649 * @param hitTestResult The parent [HitTestResult] that any hit should be added to.
650 * @param pointerType The [PointerType] of the source input. Touch sources allow for minimum
651 * touch target. Semantics hit tests always treat hits as needing minimum touch target.
652 * @param isInLayer `true` if the touch event is in the layer of this and all parents or `false`
653 * if it is outside the layer, but within the minimum touch target of the edge of the layer.
654 * This can only be `false` when [pointerType] is [PointerType.Touch] or else a layer miss
655 * means the event will be clipped out.
656 */
657 fun hitTest(
658 hitTestSource: HitTestSource,
659 pointerPosition: Offset,
660 hitTestResult: HitTestResult,
661 pointerType: PointerType,
662 isInLayer: Boolean
663 ) {
664 val head = head(hitTestSource.entityType())
665 if (!withinLayerBounds(pointerPosition)) {
666 // This missed the clip, but if this layout is too small and this is within the
667 // minimum touch target, we still consider it a hit.
668 if (pointerType == PointerType.Touch) {
669 val distanceFromEdge =
670 distanceInMinimumTouchTarget(pointerPosition, minimumTouchTargetSize)
671 if (
672 distanceFromEdge.fastIsFinite() &&
673 hitTestResult.isHitInMinimumTouchTargetBetter(distanceFromEdge, false)
674 ) {
675 head.hitNear(
676 hitTestSource,
677 pointerPosition,
678 hitTestResult,
679 pointerType,
680 false,
681 distanceFromEdge
682 )
683 } // else it is a complete miss.
684 }
685 } else if (head == null) {
686 hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
687 } else if (isPointerInBounds(pointerPosition)) {
688 // A real hit
689 head.hit(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
690 } else {
691 val distanceFromEdge =
692 if (pointerType != PointerType.Touch) Float.POSITIVE_INFINITY
693 else {
694 distanceInMinimumTouchTarget(pointerPosition, minimumTouchTargetSize)
695 }
696 val isHitInMinimumTouchTargetBetter =
697 distanceFromEdge.fastIsFinite() &&
698 hitTestResult.isHitInMinimumTouchTargetBetter(distanceFromEdge, isInLayer)
699
700 head.outOfBoundsHit(
701 hitTestSource,
702 pointerPosition,
703 hitTestResult,
704 pointerType,
705 isInLayer,
706 distanceFromEdge,
707 isHitInMinimumTouchTargetBetter
708 )
709 }
710 }
711
712 /**
713 * The [NodeCoordinator] had a hit in bounds and can record any children in the [hitTestResult].
714 */
715 private fun Modifier.Node?.hit(
716 hitTestSource: HitTestSource,
717 pointerPosition: Offset,
718 hitTestResult: HitTestResult,
719 pointerType: PointerType,
720 isInLayer: Boolean
721 ) {
722 if (this == null) {
723 hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
724 } else {
725 hitTestResult.hit(this, isInLayer) {
726 nextUntil(hitTestSource.entityType(), Nodes.Layout)
727 .hit(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
728 }
729 }
730 }
731
732 /**
733 * The pointer lands outside the node's bounds. There are three cases we have to handle:
734 * 1. hitNear: if the nodes is smaller than the minimumTouchTargetSize, it's touch bounds will
735 * be expanded to the minimal touch target size.
736 * 2. hitExpandedTouchBounds: if the nodes has a expanded touch bounds.
737 * 3. speculativeHit: if the hit misses this node, but its child can still get the pointer
738 * event.
739 *
740 * The complication is when touch bounds overlaps, there are 3 possibilities:
741 * 1. hit in this node's expanded touch bounds or minimum touch target bounds overlaps with a
742 * direct hit in the other node. The node with direct hit will get the event.
743 * 2. hit in this node's expanded touch bounds overlaps with other node's expanded touch bounds.
744 * Both nodes will get the event.
745 * 3. hit in this node's expanded touch bounds overlaps with the other node's minimum touch
746 * touch bounds. The node with expanded touch bounds will get the event.
747 *
748 * The logic to handle the hit priority is implemented in [HitTestResult.speculativeHit] and
749 * [HitTestResult.hitExpandedTouchBounds].
750 */
751 private fun Modifier.Node?.outOfBoundsHit(
752 hitTestSource: HitTestSource,
753 pointerPosition: Offset,
754 hitTestResult: HitTestResult,
755 pointerType: PointerType,
756 isInLayer: Boolean,
757 distanceFromEdge: Float,
758 isHitInMinimumTouchTargetBetter: Boolean
759 ) {
760 if (this == null) {
761 hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
762 } else if (isInExpandedTouchBounds(pointerPosition, pointerType)) {
763 hitTestResult.hitExpandedTouchBounds(this, isInLayer) {
764 nextUntil(hitTestSource.entityType(), Nodes.Layout)
765 .outOfBoundsHit(
766 hitTestSource,
767 pointerPosition,
768 hitTestResult,
769 pointerType,
770 isInLayer,
771 distanceFromEdge,
772 isHitInMinimumTouchTargetBetter
773 )
774 }
775 } else if (isHitInMinimumTouchTargetBetter) {
776 hitNear(
777 hitTestSource,
778 pointerPosition,
779 hitTestResult,
780 pointerType,
781 isInLayer,
782 distanceFromEdge
783 )
784 } else {
785 speculativeHit(
786 hitTestSource,
787 pointerPosition,
788 hitTestResult,
789 pointerType,
790 isInLayer,
791 distanceFromEdge
792 )
793 }
794 }
795
796 /**
797 * The [NodeCoordinator] had a hit [distanceFromEdge] from the bounds and it is within the
798 * minimum touch target distance, so it should be recorded as such in the [hitTestResult].
799 */
800 private fun Modifier.Node?.hitNear(
801 hitTestSource: HitTestSource,
802 pointerPosition: Offset,
803 hitTestResult: HitTestResult,
804 pointerType: PointerType,
805 isInLayer: Boolean,
806 distanceFromEdge: Float
807 ) {
808 if (this == null) {
809 hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
810 } else {
811 // Hit closer than existing handlers, so just record it
812 hitTestResult.hitInMinimumTouchTarget(this, distanceFromEdge, isInLayer) {
813 nextUntil(hitTestSource.entityType(), Nodes.Layout)
814 .outOfBoundsHit(
815 hitTestSource,
816 pointerPosition,
817 hitTestResult,
818 pointerType,
819 isInLayer,
820 distanceFromEdge,
821 isHitInMinimumTouchTargetBetter = true
822 )
823 }
824 }
825 }
826
827 /**
828 * The [NodeCoordinator] had a miss, but it hasn't been clipped out. The child must be checked
829 * to see if it hit.
830 */
831 private fun Modifier.Node?.speculativeHit(
832 hitTestSource: HitTestSource,
833 pointerPosition: Offset,
834 hitTestResult: HitTestResult,
835 pointerType: PointerType,
836 isInLayer: Boolean,
837 distanceFromEdge: Float
838 ) {
839 if (this == null) {
840 hitTestChild(hitTestSource, pointerPosition, hitTestResult, pointerType, isInLayer)
841 } else if (hitTestSource.interceptOutOfBoundsChildEvents(this)) {
842 // We only want to replace the existing touch target if there are better
843 // hits in the children
844 hitTestResult.speculativeHit(this, distanceFromEdge, isInLayer) {
845 nextUntil(hitTestSource.entityType(), Nodes.Layout)
846 .outOfBoundsHit(
847 hitTestSource,
848 pointerPosition,
849 hitTestResult,
850 pointerType,
851 isInLayer,
852 distanceFromEdge,
853 isHitInMinimumTouchTargetBetter = false
854 )
855 }
856 } else {
857 nextUntil(hitTestSource.entityType(), Nodes.Layout)
858 .outOfBoundsHit(
859 hitTestSource,
860 pointerPosition,
861 hitTestResult,
862 pointerType,
863 isInLayer,
864 distanceFromEdge,
865 isHitInMinimumTouchTargetBetter = false
866 )
867 }
868 }
869
870 /**
871 * Helper method to check if the pointer is inside the node's expanded touch bounds. This only
872 * applies to pointer input modifier nodes whose [PointerInputModifierNode.touchBoundsExpansion]
873 * is not null.
874 */
875 private fun Modifier.Node?.isInExpandedTouchBounds(
876 pointerPosition: Offset,
877 pointerType: PointerType
878 ): Boolean {
879 if (this == null) {
880 return false
881 }
882 // The expanded touch bounds only works for stylus at this moment.
883 if (pointerType != PointerType.Stylus && pointerType != PointerType.Eraser) {
884 return false
885 }
886 dispatchForKind(Nodes.PointerInput) {
887 // We only check for the node itself or the first delegate PointerInputModifierNode.
888 val expansion = it.touchBoundsExpansion
889 return pointerPosition.x >= -expansion.computeLeft(layoutDirection) &&
890 pointerPosition.x < measuredWidth + expansion.computeRight(layoutDirection) &&
891 pointerPosition.y >= -expansion.top &&
892 pointerPosition.y < measuredHeight + expansion.bottom
893 }
894 return false
895 }
896
897 /** Do a [hitTest] on the children of this [NodeCoordinator]. */
898 open fun hitTestChild(
899 hitTestSource: HitTestSource,
900 pointerPosition: Offset,
901 hitTestResult: HitTestResult,
902 pointerType: PointerType,
903 isInLayer: Boolean
904 ) {
905 // Also, keep looking to see if we also might hit any children.
906 // This avoids checking layer bounds twice as when we call super.hitTest()
907 val wrapped = wrapped
908 if (wrapped != null) {
909 val positionInWrapped = wrapped.fromParentPosition(pointerPosition)
910 wrapped.hitTest(hitTestSource, positionInWrapped, hitTestResult, pointerType, isInLayer)
911 }
912 }
913
914 /** Returns the bounds of this [NodeCoordinator], including the minimum touch target. */
915 fun touchBoundsInRoot(): Rect {
916 if (!isAttached) {
917 return Rect.Zero
918 }
919
920 val root = findRootCoordinates()
921
922 val bounds = rectCache
923 val padding = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
924 bounds.left = -padding.width
925 bounds.top = -padding.height
926 bounds.right = measuredWidth + padding.width
927 bounds.bottom = measuredHeight + padding.height
928
929 var coordinator: NodeCoordinator = this
930 while (coordinator !== root) {
931 coordinator.rectInParent(
932 bounds,
933 clipBounds = false,
934 clipToMinimumTouchTargetSize = true
935 )
936 if (bounds.isEmpty) {
937 return Rect.Zero
938 }
939
940 coordinator = coordinator.wrappedBy!!
941 }
942 return bounds.toRect()
943 }
944
945 override fun screenToLocal(relativeToScreen: Offset): Offset {
946 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
947 val owner = layoutNode.requireOwner()
948 val positionInRoot = owner.screenToLocal(relativeToScreen)
949 val root = findRootCoordinates()
950 return localPositionOf(root, positionInRoot)
951 }
952
953 override fun localToScreen(relativeToLocal: Offset): Offset {
954 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
955 val positionInRoot = localToRoot(relativeToLocal)
956 val owner = layoutNode.requireOwner()
957 return owner.localToScreen(positionInRoot)
958 }
959
960 override fun windowToLocal(relativeToWindow: Offset): Offset {
961 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
962 val root = findRootCoordinates()
963 val positionInRoot =
964 layoutNode.requireOwner().calculateLocalPosition(relativeToWindow) -
965 root.positionInRoot()
966 return localPositionOf(root, positionInRoot)
967 }
968
969 override fun localToWindow(relativeToLocal: Offset): Offset {
970 val positionInRoot = localToRoot(relativeToLocal)
971 val owner = layoutNode.requireOwner()
972 return owner.calculatePositionInWindow(positionInRoot)
973 }
974
975 private fun LayoutCoordinates.toCoordinator() =
976 (this as? LookaheadLayoutCoordinates)?.coordinator ?: this as NodeCoordinator
977
978 override fun localPositionOf(
979 sourceCoordinates: LayoutCoordinates,
980 relativeToSource: Offset
981 ): Offset =
982 localPositionOf(
983 sourceCoordinates = sourceCoordinates,
984 relativeToSource = relativeToSource,
985 includeMotionFrameOfReference = true
986 )
987
988 override fun localPositionOf(
989 sourceCoordinates: LayoutCoordinates,
990 relativeToSource: Offset,
991 includeMotionFrameOfReference: Boolean
992 ): Offset {
993 if (sourceCoordinates is LookaheadLayoutCoordinates) {
994 sourceCoordinates.coordinator.onCoordinatesUsed()
995 return -sourceCoordinates.localPositionOf(
996 sourceCoordinates = this,
997 relativeToSource = -relativeToSource,
998 includeMotionFrameOfReference = includeMotionFrameOfReference
999 )
1000 }
1001
1002 val nodeCoordinator = sourceCoordinates.toCoordinator()
1003 nodeCoordinator.onCoordinatesUsed()
1004 val commonAncestor = findCommonAncestor(nodeCoordinator)
1005
1006 var position = relativeToSource
1007 var coordinator = nodeCoordinator
1008 while (coordinator !== commonAncestor) {
1009 position = coordinator.toParentPosition(position, includeMotionFrameOfReference)
1010 coordinator = coordinator.wrappedBy!!
1011 }
1012
1013 return ancestorToLocal(commonAncestor, position, includeMotionFrameOfReference)
1014 }
1015
1016 override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
1017 val coordinator = sourceCoordinates.toCoordinator()
1018 coordinator.onCoordinatesUsed()
1019 val commonAncestor = findCommonAncestor(coordinator)
1020
1021 matrix.reset()
1022 // Transform from the source to the common ancestor
1023 coordinator.transformToAncestor(commonAncestor, matrix)
1024 // Transform from the common ancestor to this
1025 transformFromAncestor(commonAncestor, matrix)
1026 }
1027
1028 override fun transformToScreen(matrix: Matrix) {
1029 val owner = layoutNode.requireOwner()
1030 val rootCoordinator = findRootCoordinates().toCoordinator()
1031 transformToAncestor(rootCoordinator, matrix)
1032 if (owner is MatrixPositionCalculator) {
1033 // Only Android owner supports direct matrix manipulations,
1034 // This API had to be Android-only in the first place.
1035 owner.localToScreen(matrix)
1036 } else {
1037 // Fallback: try to extract just position
1038 val screenPosition = rootCoordinator.positionOnScreen()
1039 if (screenPosition.isSpecified) {
1040 matrix.translate(screenPosition.x, screenPosition.y, 0f)
1041 }
1042 }
1043 }
1044
1045 private fun transformToAncestor(ancestor: NodeCoordinator, matrix: Matrix) {
1046 var wrapper = this
1047 while (wrapper != ancestor) {
1048 wrapper.layer?.transform(matrix)
1049 val position = wrapper.position
1050 if (position != IntOffset.Zero) {
1051 tmpMatrix.reset()
1052 tmpMatrix.translate(position.x.toFloat(), position.y.toFloat())
1053 matrix.timesAssign(tmpMatrix)
1054 }
1055 wrapper = wrapper.wrappedBy!!
1056 }
1057 }
1058
1059 private fun transformFromAncestor(ancestor: NodeCoordinator, matrix: Matrix) {
1060 if (ancestor != this) {
1061 wrappedBy!!.transformFromAncestor(ancestor, matrix)
1062 if (position != IntOffset.Zero) {
1063 tmpMatrix.reset()
1064 tmpMatrix.translate(-position.x.toFloat(), -position.y.toFloat())
1065 matrix.timesAssign(tmpMatrix)
1066 }
1067 layer?.inverseTransform(matrix)
1068 }
1069 }
1070
1071 override fun localBoundingBoxOf(
1072 sourceCoordinates: LayoutCoordinates,
1073 clipBounds: Boolean
1074 ): Rect {
1075 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
1076 checkPrecondition(sourceCoordinates.isAttached) {
1077 "LayoutCoordinates $sourceCoordinates is not attached!"
1078 }
1079 val srcCoordinator = sourceCoordinates.toCoordinator()
1080 srcCoordinator.onCoordinatesUsed()
1081 val commonAncestor = findCommonAncestor(srcCoordinator)
1082
1083 val bounds = rectCache
1084 bounds.left = 0f
1085 bounds.top = 0f
1086 bounds.right = sourceCoordinates.size.width.toFloat()
1087 bounds.bottom = sourceCoordinates.size.height.toFloat()
1088
1089 var coordinator = srcCoordinator
1090 while (coordinator !== commonAncestor) {
1091 coordinator.rectInParent(bounds, clipBounds)
1092 if (bounds.isEmpty) {
1093 return Rect.Zero
1094 }
1095
1096 coordinator = coordinator.wrappedBy!!
1097 }
1098
1099 ancestorToLocal(commonAncestor, bounds, clipBounds)
1100 return bounds.toRect()
1101 }
1102
1103 private fun ancestorToLocal(
1104 ancestor: NodeCoordinator,
1105 offset: Offset,
1106 includeMotionFrameOfReference: Boolean,
1107 ): Offset {
1108 if (ancestor === this) {
1109 return offset
1110 }
1111 val wrappedBy = wrappedBy
1112 if (wrappedBy == null || ancestor == wrappedBy) {
1113 return fromParentPosition(offset, includeMotionFrameOfReference)
1114 }
1115 return fromParentPosition(
1116 position = wrappedBy.ancestorToLocal(ancestor, offset, includeMotionFrameOfReference),
1117 includeMotionFrameOfReference = includeMotionFrameOfReference
1118 )
1119 }
1120
1121 private fun ancestorToLocal(ancestor: NodeCoordinator, rect: MutableRect, clipBounds: Boolean) {
1122 if (ancestor === this) {
1123 return
1124 }
1125 wrappedBy?.ancestorToLocal(ancestor, rect, clipBounds)
1126 return fromParentRect(rect, clipBounds)
1127 }
1128
1129 override fun localToRoot(relativeToLocal: Offset): Offset {
1130 checkPrecondition(isAttached) { ExpectAttachedLayoutCoordinates }
1131 onCoordinatesUsed()
1132 var coordinator: NodeCoordinator? = this
1133 var position = relativeToLocal
1134 while (coordinator != null) {
1135 position = coordinator.toParentPosition(position)
1136 coordinator = coordinator.wrappedBy
1137 }
1138 return position
1139 }
1140
1141 protected inline fun withPositionTranslation(canvas: Canvas, block: (Canvas) -> Unit) {
1142 val x = position.x.toFloat()
1143 val y = position.y.toFloat()
1144 canvas.translate(x, y)
1145 block(canvas)
1146 canvas.translate(-x, -y)
1147 }
1148
1149 /**
1150 * Converts [position] in the local coordinate system to a [Offset] in the
1151 * [parentLayoutCoordinates] coordinate system.
1152 */
1153 open fun toParentPosition(
1154 position: Offset,
1155 includeMotionFrameOfReference: Boolean = true
1156 ): Offset {
1157 val layer = layer
1158 val targetPosition = layer?.mapOffset(position, inverse = false) ?: position
1159 return if (!includeMotionFrameOfReference && isPlacedUnderMotionFrameOfReference) {
1160 targetPosition
1161 } else {
1162 targetPosition + this.position
1163 }
1164 }
1165
1166 /**
1167 * Converts [position] in the [parentLayoutCoordinates] coordinate system to a [Offset] in the
1168 * local coordinate system.
1169 */
1170 open fun fromParentPosition(
1171 position: Offset,
1172 includeMotionFrameOfReference: Boolean = true
1173 ): Offset {
1174 val relativeToPosition =
1175 if (!includeMotionFrameOfReference && this.isPlacedUnderMotionFrameOfReference) {
1176 position
1177 } else {
1178 position - this.position
1179 }
1180 val layer = layer
1181 return layer?.mapOffset(relativeToPosition, inverse = true) ?: relativeToPosition
1182 }
1183
1184 protected fun drawBorder(canvas: Canvas, paint: Paint) {
1185 canvas.drawRect(
1186 left = 0.5f,
1187 top = 0.5f,
1188 right = measuredSize.width.toFloat() - 0.5f,
1189 bottom = measuredSize.height.toFloat() - 0.5f,
1190 paint = paint
1191 )
1192 }
1193
1194 /**
1195 * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is attached
1196 * to the [Owner].
1197 */
1198 fun onLayoutNodeAttach() {
1199 // this call will update the parameters of the layer (alpha, scale, etc)
1200 updateLayerBlock(layerBlock, forceUpdateLayerParameters = true)
1201 // this call will invalidate the content of the layer
1202 layer?.invalidate()
1203 }
1204
1205 /**
1206 * This will be called when the [LayoutNode] associated with this [NodeCoordinator] is released
1207 * or when the [NodeCoordinator] is released (will not be used anymore).
1208 */
1209 fun onRelease() {
1210 released = true
1211 // It is important to call invalidateParentLayer() here, even though updateLayerBlock() may
1212 // call it. The reason is because we end up calling this from the bottom up, which means
1213 // that if we have two layout modifiers getting removed, where the parent one has a layer
1214 // and the bottom one doesn't, the parent layer gets invalidated but then removed, leaving
1215 // no layers invalidated. By always calling this, we ensure that after all nodes are
1216 // removed at least one layer is invalidated.
1217 invalidateParentLayer()
1218 releaseLayer()
1219 }
1220
1221 /**
1222 * Modifies bounds to be in the parent NodeCoordinator's coordinates, including clipping, if
1223 * [clipBounds] is true. If [clipToMinimumTouchTargetSize] is true and the layer clips, then the
1224 * clip bounds are extended to allow minimum touch target extended area.
1225 */
1226 internal fun rectInParent(
1227 bounds: MutableRect,
1228 clipBounds: Boolean,
1229 clipToMinimumTouchTargetSize: Boolean = false
1230 ) {
1231 val layer = layer
1232 if (layer != null) {
1233 if (isClipping) {
1234 if (clipToMinimumTouchTargetSize) {
1235 val minTouch = minimumTouchTargetSize
1236 val horz = minTouch.width / 2f
1237 val vert = minTouch.height / 2f
1238 bounds.intersect(
1239 -horz,
1240 -vert,
1241 size.width.toFloat() + horz,
1242 size.height.toFloat() + vert
1243 )
1244 } else if (clipBounds) {
1245 bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
1246 }
1247 if (bounds.isEmpty) {
1248 return
1249 }
1250 }
1251 layer.mapBounds(bounds, inverse = false)
1252 }
1253
1254 val x = position.x
1255 bounds.left += x
1256 bounds.right += x
1257
1258 val y = position.y
1259 bounds.top += y
1260 bounds.bottom += y
1261 }
1262
1263 /**
1264 * Modifies bounds in the parent's coordinates to be in this NodeCoordinator's coordinates,
1265 * including clipping, if [clipBounds] is true.
1266 */
1267 private fun fromParentRect(bounds: MutableRect, clipBounds: Boolean) {
1268 val x = position.x
1269 bounds.left -= x
1270 bounds.right -= x
1271
1272 val y = position.y
1273 bounds.top -= y
1274 bounds.bottom -= y
1275
1276 val layer = layer
1277 if (layer != null) {
1278 layer.mapBounds(bounds, inverse = true)
1279 if (isClipping && clipBounds) {
1280 bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
1281 if (bounds.isEmpty) {
1282 return
1283 }
1284 }
1285 }
1286 }
1287
1288 protected fun withinLayerBounds(pointerPosition: Offset): Boolean {
1289 if (!pointerPosition.isFinite) {
1290 return false
1291 }
1292 val layer = layer
1293 return layer == null || !isClipping || layer.isInLayer(pointerPosition)
1294 }
1295
1296 /**
1297 * Whether a pointer that is relative to the [NodeCoordinator] is in the bounds of this
1298 * NodeCoordinator.
1299 */
1300 protected fun isPointerInBounds(pointerPosition: Offset): Boolean {
1301 val x = pointerPosition.x
1302 val y = pointerPosition.y
1303 return x >= 0f && y >= 0f && x < measuredWidth && y < measuredHeight
1304 }
1305
1306 /** Invalidates the layer that this coordinator will draw into. */
1307 open fun invalidateLayer() {
1308 val layer = layer
1309 if (layer != null) {
1310 layer.invalidate()
1311 } else {
1312 wrappedBy?.invalidateLayer()
1313 }
1314 }
1315
1316 /**
1317 * Called when [LayoutNode.modifier] has changed and all the NodeCoordinators have been
1318 * configured.
1319 */
1320 open fun onLayoutModifierNodeChanged() {
1321 layer?.invalidate()
1322 }
1323
1324 internal fun findCommonAncestor(other: NodeCoordinator): NodeCoordinator {
1325 var ancestor1 = other.layoutNode
1326 var ancestor2 = layoutNode
1327 if (ancestor1 === ancestor2) {
1328 val otherNode = other.tail
1329 // They are on the same node, but we don't know which is the deeper of the two
1330 tail.visitLocalAncestors(Nodes.Layout.mask) { if (it === otherNode) return other }
1331 return this
1332 }
1333
1334 while (ancestor1.depth > ancestor2.depth) {
1335 ancestor1 = ancestor1.parent!!
1336 }
1337
1338 while (ancestor2.depth > ancestor1.depth) {
1339 ancestor2 = ancestor2.parent!!
1340 }
1341
1342 while (ancestor1 !== ancestor2) {
1343 val parent1 = ancestor1.parent
1344 val parent2 = ancestor2.parent
1345 if (parent1 == null || parent2 == null) {
1346 throw IllegalArgumentException("layouts are not part of the same hierarchy")
1347 }
1348 ancestor1 = parent1
1349 ancestor2 = parent2
1350 }
1351
1352 return when {
1353 ancestor2 === layoutNode -> this
1354 ancestor1 === other.layoutNode -> other
1355 else -> ancestor1.innerCoordinator
1356 }
1357 }
1358
1359 fun shouldSharePointerInputWithSiblings(): Boolean {
1360 val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
1361
1362 if (start.isAttached) {
1363 // We have to check both the self and local descendants, because the `start` can also
1364 // be a `PointerInputModifierNode` (when the first modifier node on the LayoutNode is
1365 // a `PointerInputModifierNode`).
1366 start.visitSelfAndLocalDescendants(Nodes.PointerInput) {
1367 if (it.sharePointerInputWithSiblings()) return true
1368 }
1369 }
1370
1371 return false
1372 }
1373
1374 private fun offsetFromEdge(pointerPosition: Offset): Offset {
1375 val x = pointerPosition.x
1376 val horizontal = maxOf(0f, if (x < 0) -x else x - measuredWidth)
1377 val y = pointerPosition.y
1378 val vertical = maxOf(0f, if (y < 0) -y else y - measuredHeight)
1379
1380 return Offset(horizontal, vertical)
1381 }
1382
1383 /**
1384 * Returns the additional amount on the horizontal and vertical dimensions that this extends
1385 * beyond [width] and [height] on all sides. This takes into account [minimumTouchTargetSize]
1386 * and [measuredSize] vs. [width] and [height].
1387 */
1388 protected fun calculateMinimumTouchTargetPadding(minimumTouchTargetSize: Size): Size {
1389 val widthDiff = minimumTouchTargetSize.width - measuredWidth.toFloat()
1390 val heightDiff = minimumTouchTargetSize.height - measuredHeight.toFloat()
1391 return Size(maxOf(0f, widthDiff / 2f), maxOf(0f, heightDiff / 2f))
1392 }
1393
1394 /**
1395 * The distance within the [minimumTouchTargetSize] of [pointerPosition] to the layout size. If
1396 * [pointerPosition] isn't within [minimumTouchTargetSize], then [Float.POSITIVE_INFINITY] is
1397 * returned.
1398 */
1399 protected fun distanceInMinimumTouchTarget(
1400 pointerPosition: Offset,
1401 minimumTouchTargetSize: Size
1402 ): Float {
1403 if (
1404 measuredWidth >= minimumTouchTargetSize.width &&
1405 measuredHeight >= minimumTouchTargetSize.height
1406 ) {
1407 // this layout is big enough that it doesn't qualify for minimum touch targets
1408 return Float.POSITIVE_INFINITY
1409 }
1410
1411 val (width, height) = calculateMinimumTouchTargetPadding(minimumTouchTargetSize)
1412 val offsetFromEdge = offsetFromEdge(pointerPosition)
1413
1414 return if (
1415 (width > 0f || height > 0f) && offsetFromEdge.x <= width && offsetFromEdge.y <= height
1416 ) {
1417 offsetFromEdge.getDistanceSquared()
1418 } else {
1419 Float.POSITIVE_INFINITY // miss
1420 }
1421 }
1422
1423 /**
1424 * [LayoutNode.hitTest] and [LayoutNode.hitTestSemantics] are very similar, but the data used in
1425 * their implementations are different. This extracts the differences between the two methods
1426 * into a single interface.
1427 */
1428 internal interface HitTestSource {
1429 /** Returns the [NodeKind] for the hit test target. */
1430 fun entityType(): NodeKind<*>
1431
1432 /**
1433 * Pointer input hit tests can intercept child hits when enabled. This returns `true` if the
1434 * modifier has requested intercepting.
1435 */
1436 fun interceptOutOfBoundsChildEvents(node: Modifier.Node): Boolean
1437
1438 /**
1439 * Returns false if the parent layout node has a state that suppresses hit testing of its
1440 * children.
1441 */
1442 fun shouldHitTestChildren(parentLayoutNode: LayoutNode): Boolean
1443
1444 /** Calls a hit test on [layoutNode]. */
1445 fun childHitTest(
1446 layoutNode: LayoutNode,
1447 pointerPosition: Offset,
1448 hitTestResult: HitTestResult,
1449 pointerType: PointerType,
1450 isInLayer: Boolean
1451 )
1452 }
1453
1454 internal companion object {
1455 const val ExpectAttachedLayoutCoordinates =
1456 "LayoutCoordinate operations are only valid " + "when isAttached is true"
1457 const val UnmeasuredError = "Asking for measurement result of unmeasured layout modifier"
1458 private val onCommitAffectingLayerParams: (NodeCoordinator) -> Unit = { coordinator ->
1459 if (coordinator.isValidOwnerScope) {
1460 // coordinator.layerPositionalProperties should always be non-null here, but
1461 // we'll just be careful with a null check.
1462 val positionalPropertiesChanged = coordinator.updateLayerParameters()
1463 if (positionalPropertiesChanged) {
1464 val layoutNode = coordinator.layoutNode
1465 val layoutDelegate = layoutNode.layoutDelegate
1466 if (layoutDelegate.childrenAccessingCoordinatesDuringPlacement > 0) {
1467 if (
1468 layoutDelegate.coordinatesAccessedDuringModifierPlacement ||
1469 layoutDelegate.coordinatesAccessedDuringPlacement
1470 ) {
1471 layoutNode.requestRelayout()
1472 }
1473 layoutDelegate.measurePassDelegate
1474 .notifyChildrenUsingCoordinatesWhilePlacing()
1475 }
1476 val owner = layoutNode.requireOwner()
1477 owner.rectManager.onLayoutLayerPositionalPropertiesChanged(layoutNode)
1478 owner.requestOnPositionedCallback(layoutNode)
1479 }
1480 }
1481 }
1482 private val onCommitAffectingLayer: (NodeCoordinator) -> Unit = { coordinator ->
1483 coordinator.layer?.invalidate()
1484 }
1485 private val graphicsLayerScope = ReusableGraphicsLayerScope()
1486 private val tmpLayerPositionalProperties = LayerPositionalProperties()
1487
1488 // Used for matrix calculations. It should not be used for anything that could lead to
1489 // reentrancy.
1490 private val tmpMatrix = Matrix()
1491
1492 /** Hit testing specifics for pointer input. */
1493 val PointerInputSource =
1494 object : HitTestSource {
1495 override fun entityType() = Nodes.PointerInput
1496
1497 override fun interceptOutOfBoundsChildEvents(node: Modifier.Node): Boolean {
1498 node.dispatchForKind(Nodes.PointerInput) {
1499 if (it.interceptOutOfBoundsChildEvents()) return true
1500 }
1501 return false
1502 }
1503
1504 override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) = true
1505
1506 override fun childHitTest(
1507 layoutNode: LayoutNode,
1508 pointerPosition: Offset,
1509 hitTestResult: HitTestResult,
1510 pointerType: PointerType,
1511 isInLayer: Boolean
1512 ) = layoutNode.hitTest(pointerPosition, hitTestResult, pointerType, isInLayer)
1513 }
1514
1515 /** Hit testing specifics for semantics. */
1516 val SemanticsSource =
1517 object : HitTestSource {
1518 override fun entityType() = Nodes.Semantics
1519
1520 override fun interceptOutOfBoundsChildEvents(node: Modifier.Node) = false
1521
1522 override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
1523 parentLayoutNode.semanticsConfiguration?.isClearingSemantics != true
1524
1525 override fun childHitTest(
1526 layoutNode: LayoutNode,
1527 pointerPosition: Offset,
1528 hitTestResult: HitTestResult,
1529 pointerType: PointerType,
1530 isInLayer: Boolean
1531 ) =
1532 layoutNode.hitTestSemantics(
1533 pointerPosition,
1534 hitTestResult,
1535 pointerType,
1536 isInLayer
1537 )
1538 }
1539 }
1540 }
1541
1542 @Suppress("PrimitiveInCollection")
compareEqualsnull1543 private fun compareEquals(
1544 a: MutableObjectIntMap<AlignmentLine>?,
1545 b: Map<AlignmentLine, Int>
1546 ): Boolean {
1547 if (a == null) return false
1548 if (a.size != b.size) return false
1549
1550 a.forEach { k, v -> if (b[k] != v) return false }
1551
1552 return true
1553 }
1554
1555 /**
1556 * These are the components of a layer that changes the position and may lead to an
1557 * OnGloballyPositionedCallback.
1558 */
1559 private class LayerPositionalProperties {
1560 private var scaleX: Float = 1f
1561 private var scaleY: Float = 1f
1562 private var translationX: Float = 0f
1563 private var translationY: Float = 0f
1564 private var rotationX: Float = 0f
1565 private var rotationY: Float = 0f
1566 private var rotationZ: Float = 0f
1567 private var cameraDistance: Float = DefaultCameraDistance
1568 private var transformOrigin: TransformOrigin = TransformOrigin.Center
1569
copyFromnull1570 fun copyFrom(other: LayerPositionalProperties) {
1571 scaleX = other.scaleX
1572 scaleY = other.scaleY
1573 translationX = other.translationX
1574 translationY = other.translationY
1575 rotationX = other.rotationX
1576 rotationY = other.rotationY
1577 rotationZ = other.rotationZ
1578 cameraDistance = other.cameraDistance
1579 transformOrigin = other.transformOrigin
1580 }
1581
copyFromnull1582 fun copyFrom(scope: GraphicsLayerScope) {
1583 scaleX = scope.scaleX
1584 scaleY = scope.scaleY
1585 translationX = scope.translationX
1586 translationY = scope.translationY
1587 rotationX = scope.rotationX
1588 rotationY = scope.rotationY
1589 rotationZ = scope.rotationZ
1590 cameraDistance = scope.cameraDistance
1591 transformOrigin = scope.transformOrigin
1592 }
1593
hasSameValuesAsnull1594 fun hasSameValuesAs(other: LayerPositionalProperties): Boolean {
1595 return scaleX == other.scaleX &&
1596 scaleY == other.scaleY &&
1597 translationX == other.translationX &&
1598 translationY == other.translationY &&
1599 rotationX == other.rotationX &&
1600 rotationY == other.rotationY &&
1601 rotationZ == other.rotationZ &&
1602 cameraDistance == other.cameraDistance &&
1603 transformOrigin == other.transformOrigin
1604 }
1605 }
1606
nextUntilnull1607 private fun DelegatableNode.nextUntil(type: NodeKind<*>, stopType: NodeKind<*>): Modifier.Node? {
1608 val child = node.child ?: return null
1609 if (child.aggregateChildKindSet and type.mask == 0) return null
1610 var next: Modifier.Node? = child
1611 while (next != null) {
1612 val kindSet = next.kindSet
1613 if (kindSet and stopType.mask != 0) return null
1614 if (kindSet and type.mask != 0) {
1615 return next
1616 }
1617 next = next.child
1618 }
1619 return null
1620 }
1621