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.compose.ui.Modifier
20 import androidx.compose.ui.graphics.Canvas
21 import androidx.compose.ui.graphics.Color
22 import androidx.compose.ui.graphics.GraphicsLayerScope
23 import androidx.compose.ui.graphics.Paint
24 import androidx.compose.ui.graphics.PaintingStyle
25 import androidx.compose.ui.graphics.layer.GraphicsLayer
26 import androidx.compose.ui.internal.checkPrecondition
27 import androidx.compose.ui.layout.AlignmentLine
28 import androidx.compose.ui.layout.ApproachLayoutModifierNode
29 import androidx.compose.ui.layout.ApproachMeasureScopeImpl
30 import androidx.compose.ui.layout.HorizontalAlignmentLine
31 import androidx.compose.ui.layout.LayoutModifier
32 import androidx.compose.ui.layout.MeasureResult
33 import androidx.compose.ui.layout.Placeable
34 import androidx.compose.ui.unit.Constraints
35 import androidx.compose.ui.unit.IntOffset
36 
37 internal class LayoutModifierNodeCoordinator(
38     layoutNode: LayoutNode,
39     measureNode: LayoutModifierNode,
40 ) : NodeCoordinator(layoutNode) {
41 
42     var layoutModifierNode: LayoutModifierNode = measureNode
43         internal set(value) {
44             if (value != field) {
45                 // Opt for a cheaper type check (via bit operation) before casting, as we anticipate
46                 // the node to not be ApproachLayoutModifierNode in most cases.
47                 if (value.node.isKind(Nodes.ApproachMeasure)) {
48                     value as ApproachLayoutModifierNode
49                     approachMeasureScope =
50                         approachMeasureScope?.also { it.approachNode = value }
51                             ?: ApproachMeasureScopeImpl(this, value)
52                 } else {
53                     approachMeasureScope = null
54                 }
55             }
56             field = value
57         }
58 
59     override val tail: Modifier.Node
60         get() = layoutModifierNode.node
61 
62     val wrappedNonNull: NodeCoordinator
63         get() = wrapped!!
64 
65     internal var lookaheadConstraints: Constraints? = null
66 
67     override var lookaheadDelegate: LookaheadDelegate? =
68         if (layoutNode.lookaheadRoot != null) LookaheadDelegateForLayoutModifierNode() else null
69 
70     /**
71      * Lazily initialized IntermediateMeasureScope. This is only initialized when the current
72      * modifier is an ApproachLayoutModifierNode.
73      */
74     private var approachMeasureScope: ApproachMeasureScopeImpl? =
75         // Opt for a cheaper type check (via bit operation) before casting, as we anticipate
76         // the node to not be ApproachLayoutModifierNode in most cases.
77         if (measureNode.node.isKind(Nodes.ApproachMeasure)) {
78             ApproachMeasureScopeImpl(this, measureNode as ApproachLayoutModifierNode)
79         } else null
80 
81     /**
82      * LookaheadDelegate impl for when the modifier is any [LayoutModifier] except
83      * IntermediateLayoutModifier. This impl will invoke [LayoutModifier.measure] for the lookahead
84      * measurement.
85      */
86     private inner class LookaheadDelegateForLayoutModifierNode :
87         LookaheadDelegate(this@LayoutModifierNodeCoordinator) {
88         // LookaheadMeasure
89         override fun measure(constraints: Constraints): Placeable =
90             performingMeasure(constraints) {
91                 this@LayoutModifierNodeCoordinator.lookaheadConstraints = constraints
92                 with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
93                     measure(
94                         // This allows `measure` calls in the modifier to be redirected to
95                         // calling lookaheadMeasure in wrapped.
96                         this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
97                         constraints
98                     )
99                 }
100             }
101 
102         override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
103             return calculateAlignmentAndPlaceChildAsNeeded(alignmentLine).also {
104                 cachedAlignmentLinesMap[alignmentLine] = it
105             }
106         }
107 
108         override fun minIntrinsicWidth(height: Int): Int =
109             with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
110                 minIntrinsicWidth(
111                     this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
112                     height
113                 )
114             }
115 
116         override fun maxIntrinsicWidth(height: Int): Int =
117             with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
118                 maxIntrinsicWidth(
119                     this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
120                     height
121                 )
122             }
123 
124         override fun minIntrinsicHeight(width: Int): Int =
125             with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
126                 minIntrinsicHeight(
127                     this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
128                     width
129                 )
130             }
131 
132         override fun maxIntrinsicHeight(width: Int): Int =
133             with(this@LayoutModifierNodeCoordinator.layoutModifierNode) {
134                 maxIntrinsicHeight(
135                     this@LayoutModifierNodeCoordinator.wrappedNonNull.lookaheadDelegate!!,
136                     width
137                 )
138             }
139     }
140 
141     override fun ensureLookaheadDelegateCreated() {
142         if (lookaheadDelegate == null) {
143             lookaheadDelegate = LookaheadDelegateForLayoutModifierNode()
144         }
145     }
146 
147     override fun measure(constraints: Constraints): Placeable {
148         @Suppress("NAME_SHADOWING")
149         val constraints =
150             if (forceMeasureWithLookaheadConstraints) {
151                 requireNotNull(lookaheadConstraints) {
152                     "Lookahead constraints cannot be null in approach pass."
153                 }
154             } else {
155                 constraints
156             }
157         performingMeasure(constraints) {
158             measureResult =
159                 approachMeasureScope?.let { scope ->
160                     // approachMeasureScope is created/updated when layoutModifierNode is set. An
161                     // ApproachLayoutModifierNode will lead to a non-null approachMeasureScope.
162                     with(scope.approachNode) {
163                         scope.approachMeasureRequired =
164                             isMeasurementApproachInProgress(scope.lookaheadSize) ||
165                                 constraints != lookaheadConstraints
166                         if (!scope.approachMeasureRequired) {
167                             // In the future we'll skip the invocation of this measure block when
168                             // no approach is needed. For now, we'll ignore the constraints change
169                             // in the measure block when it's declared approach complete.
170                             wrappedNonNull.forceMeasureWithLookaheadConstraints = true
171                         }
172                         val result = scope.approachMeasure(wrappedNonNull, constraints)
173                         wrappedNonNull.forceMeasureWithLookaheadConstraints = false
174                         val reachedLookaheadSize =
175                             result.width == lookaheadDelegate!!.width &&
176                                 result.height == lookaheadDelegate!!.height
177                         if (
178                             !scope.approachMeasureRequired &&
179                                 wrappedNonNull.size == wrappedNonNull.lookaheadDelegate?.size &&
180                                 !reachedLookaheadSize
181                         ) {
182                             object : MeasureResult by result {
183                                 override val width = lookaheadDelegate!!.width
184                                 override val height = lookaheadDelegate!!.height
185                             }
186                         } else {
187                             result
188                         }
189                     }
190                 } ?: with(layoutModifierNode) { measure(wrappedNonNull, constraints) }
191             this@LayoutModifierNodeCoordinator
192         }
193         onMeasured()
194         return this
195     }
196 
197     override fun minIntrinsicWidth(height: Int): Int =
198         approachMeasureScope?.run {
199             with(approachNode) {
200                 minApproachIntrinsicWidth(this@LayoutModifierNodeCoordinator.wrappedNonNull, height)
201             }
202         } ?: with(layoutModifierNode) { minIntrinsicWidth(wrappedNonNull, height) }
203 
204     override fun maxIntrinsicWidth(height: Int): Int =
205         approachMeasureScope?.run {
206             with(approachNode) {
207                 maxApproachIntrinsicWidth(this@LayoutModifierNodeCoordinator.wrappedNonNull, height)
208             }
209         } ?: with(layoutModifierNode) { maxIntrinsicWidth(wrappedNonNull, height) }
210 
211     override fun minIntrinsicHeight(width: Int): Int =
212         approachMeasureScope?.run {
213             with(approachNode) {
214                 minApproachIntrinsicHeight(this@LayoutModifierNodeCoordinator.wrappedNonNull, width)
215             }
216         } ?: with(layoutModifierNode) { minIntrinsicHeight(wrappedNonNull, width) }
217 
218     override fun maxIntrinsicHeight(width: Int): Int =
219         approachMeasureScope?.run {
220             with(approachNode) {
221                 maxApproachIntrinsicHeight(this@LayoutModifierNodeCoordinator.wrappedNonNull, width)
222             }
223         } ?: with(layoutModifierNode) { maxIntrinsicHeight(wrappedNonNull, width) }
224 
225     override fun placeAt(position: IntOffset, zIndex: Float, layer: GraphicsLayer) {
226         super.placeAt(position, zIndex, layer)
227         onAfterPlaceAt()
228     }
229 
230     override fun placeAt(
231         position: IntOffset,
232         zIndex: Float,
233         layerBlock: (GraphicsLayerScope.() -> Unit)?
234     ) {
235         super.placeAt(position, zIndex, layerBlock)
236         onAfterPlaceAt()
237     }
238 
239     private fun onAfterPlaceAt() {
240         // The coordinator only runs their placement block to obtain our position, which allows them
241         // to calculate the offset of an alignment line we have already provided a position for.
242         // No need to place our wrapped as well (we might have actually done this already in
243         // get(line), to obtain the position of the alignment line the coordinator currently needs
244         // our position in order ot know how to offset the value we provided).
245         if (isShallowPlacing) return
246         onPlaced()
247         approachMeasureScope?.let {
248             with(it.approachNode) {
249                 val approachComplete =
250                     with(placementScope) {
251                         !isPlacementApproachInProgress(
252                             lookaheadDelegate!!.lookaheadLayoutCoordinates
253                         ) &&
254                             !it.approachMeasureRequired &&
255                             size == lookaheadDelegate?.size &&
256                             wrappedNonNull.size == wrappedNonNull.lookaheadDelegate?.size
257                     }
258                 wrappedNonNull.forcePlaceWithLookaheadOffset = approachComplete
259             }
260         }
261         measureResult.placeChildren()
262         wrappedNonNull.forcePlaceWithLookaheadOffset = false
263     }
264 
265     override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
266         return lookaheadDelegate?.getCachedAlignmentLine(alignmentLine)
267             ?: calculateAlignmentAndPlaceChildAsNeeded(alignmentLine)
268     }
269 
270     override fun performDraw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
271         wrappedNonNull.draw(canvas, graphicsLayer)
272         if (layoutNode.requireOwner().showLayoutBounds) {
273             drawBorder(canvas, modifierBoundsPaint)
274         }
275     }
276 
277     internal companion object {
278         val modifierBoundsPaint =
279             Paint().also { paint ->
280                 paint.color = Color.Blue
281                 paint.strokeWidth = 1f
282                 paint.style = PaintingStyle.Stroke
283             }
284     }
285 }
286 
LookaheadCapablePlaceablenull287 private fun LookaheadCapablePlaceable.calculateAlignmentAndPlaceChildAsNeeded(
288     alignmentLine: AlignmentLine
289 ): Int {
290     val child = child
291     checkPrecondition(child != null) {
292         "Child of $this cannot be null when calculating alignment line"
293     }
294     if (measureResult.alignmentLines.containsKey(alignmentLine)) {
295         return measureResult.alignmentLines[alignmentLine] ?: AlignmentLine.Unspecified
296     }
297     val positionInWrapped = child[alignmentLine]
298     if (positionInWrapped == AlignmentLine.Unspecified) {
299         return AlignmentLine.Unspecified
300     }
301     // Place our wrapped to obtain their position inside ourselves.
302     child.isShallowPlacing = true
303     isPlacingForAlignment = true
304     replace()
305     child.isShallowPlacing = false
306     isPlacingForAlignment = false
307     return if (alignmentLine is HorizontalAlignmentLine) {
308         positionInWrapped + child.position.y
309     } else {
310         positionInWrapped + child.position.x
311     }
312 }
313