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