1 /*
<lambda>null2  * Copyright 2025 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.constraintlayout.compose
18 
19 import android.util.Log
20 import androidx.collection.IntIntPair
21 import androidx.compose.ui.layout.AlignmentLine
22 import androidx.compose.ui.layout.FirstBaseline
23 import androidx.compose.ui.layout.LayoutIdParentData
24 import androidx.compose.ui.layout.Measurable
25 import androidx.compose.ui.layout.Placeable
26 import androidx.compose.ui.layout.layoutId
27 import androidx.compose.ui.unit.Constraints
28 import androidx.compose.ui.unit.Density
29 import androidx.compose.ui.unit.IntSize
30 import androidx.compose.ui.unit.LayoutDirection
31 import androidx.compose.ui.util.fastForEach
32 import androidx.constraintlayout.core.state.WidgetFrame
33 import androidx.constraintlayout.core.widgets.ConstraintWidget
34 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.FIXED
35 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
36 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_PARENT
37 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.WRAP_CONTENT
38 import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_SPREAD
39 import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_WRAP
40 import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer
41 import androidx.constraintlayout.core.widgets.Guideline
42 import androidx.constraintlayout.core.widgets.VirtualLayout
43 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure
44 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.TRY_GIVEN_DIMENSIONS
45 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.USE_GIVEN_DIMENSIONS
46 
47 private const val DEBUG = false
48 
49 /**
50  * Returns the Id set from either [LayoutIdParentData] or [ConstraintLayoutParentData]. Otherwise
51  * returns "null".
52  */
53 internal val Measurable.anyOrNullId: String
54     get() = (this.layoutId ?: this.constraintLayoutId)?.toString() ?: "null"
55 
56 /**
57  * Measurer "bridge" for ConstraintLayout in Compose.
58  *
59  * Takes ConstraintSets and Measurables and passes it to [ConstraintWidgetContainer] for measurement
60  * (Measurables are measured in the [measure] callback).
61  */
62 @PublishedApi
63 internal open class Measurer2(
64     density: Density // TODO: Change to a variable since density may change
65 ) : BasicMeasure.Measurer, DesignInfoProvider {
66     private var computedLayoutResult: String = ""
67     protected var layoutInformationReceiver: LayoutInformationReceiver? = null
68     protected val root = ConstraintWidgetContainer(0, 0).also { it.measurer = this }
69     /**
70      * Mapping between [Measurable]s and their corresponding [Placeable] result.
71      *
72      * Due to Lookahead measure pass, any object holding Measurables should not be instantiated
73      * internally. Instead, this object is expected to be instantiated from the MeasurePolicy, and
74      * passed to update this reference for each the Measure and Layout steps ([performMeasure] and
75      * [performLayout]).
76      *
77      * This way, we have different containers tracking the Measurable states for Lookahead and
78      * non-Lookahead pass.
79      */
80     protected var placeables = mutableMapOf<Measurable, Placeable>()
81 
82     /** Mapping of Id to last width and height measurements. */
83     private val lastMeasures = mutableMapOf<String, Array<Int>>()
84 
85     /** Mapping of Id to interpolated frame. */
86     protected val frameCache = mutableMapOf<String, WidgetFrame>()
87 
88     protected val state = State(density)
89 
90     private val widthConstraintsHolder = IntArray(2)
91     private val heightConstraintsHolder = IntArray(2)
92 
93     var forcedScaleFactor = Float.NaN
94     val layoutCurrentWidth: Int
95         get() = root.width
96 
97     val layoutCurrentHeight: Int
98         get() = root.height
99 
100     /**
101      * Method called by Compose tooling. Returns a JSON string that represents the Constraints
102      * defined for this ConstraintLayout Composable.
103      */
104     override fun getDesignInfo(startX: Int, startY: Int, args: String) =
105         parseConstraintsToJson(root, state, startX, startY, args)
106 
107     /** Measure the given [constraintWidget] with the specs defined by [measure]. */
108     override fun measure(constraintWidget: ConstraintWidget, measure: BasicMeasure.Measure) {
109         val widgetId = constraintWidget.stringId
110 
111         if (DEBUG) {
112             Log.d("CCL", "Measuring $widgetId with: " + constraintWidget.toDebugString() + "\n")
113         }
114 
115         val measurableLastMeasures = lastMeasures[widgetId]
116         obtainConstraints(
117             measure.horizontalBehavior,
118             measure.horizontalDimension,
119             constraintWidget.mMatchConstraintDefaultWidth,
120             measure.measureStrategy,
121             (measurableLastMeasures?.get(1) ?: 0) == constraintWidget.height,
122             constraintWidget.isResolvedHorizontally,
123             state.rootIncomingConstraints.maxWidth,
124             widthConstraintsHolder
125         )
126         obtainConstraints(
127             measure.verticalBehavior,
128             measure.verticalDimension,
129             constraintWidget.mMatchConstraintDefaultHeight,
130             measure.measureStrategy,
131             (measurableLastMeasures?.get(0) ?: 0) == constraintWidget.width,
132             constraintWidget.isResolvedVertically,
133             state.rootIncomingConstraints.maxHeight,
134             heightConstraintsHolder
135         )
136 
137         var constraints =
138             Constraints(
139                 widthConstraintsHolder[0],
140                 widthConstraintsHolder[1],
141                 heightConstraintsHolder[0],
142                 heightConstraintsHolder[1]
143             )
144 
145         if (
146             (measure.measureStrategy == TRY_GIVEN_DIMENSIONS ||
147                 measure.measureStrategy == USE_GIVEN_DIMENSIONS) ||
148                 !(measure.horizontalBehavior == MATCH_CONSTRAINT &&
149                     constraintWidget.mMatchConstraintDefaultWidth == MATCH_CONSTRAINT_SPREAD &&
150                     measure.verticalBehavior == MATCH_CONSTRAINT &&
151                     constraintWidget.mMatchConstraintDefaultHeight == MATCH_CONSTRAINT_SPREAD)
152         ) {
153             if (DEBUG) {
154                 Log.d("CCL", "Measuring $widgetId with $constraints")
155             }
156             val result = measureWidget(constraintWidget, constraints)
157             constraintWidget.isMeasureRequested = false
158             if (DEBUG) {
159                 Log.d("CCL", "$widgetId is size ${result.first} ${result.second}")
160             }
161 
162             val coercedWidth =
163                 result.first.coerceIn(
164                     constraintWidget.mMatchConstraintMinWidth.takeIf { it > 0 },
165                     constraintWidget.mMatchConstraintMaxWidth.takeIf { it > 0 }
166                 )
167             val coercedHeight =
168                 result.second.coerceIn(
169                     constraintWidget.mMatchConstraintMinHeight.takeIf { it > 0 },
170                     constraintWidget.mMatchConstraintMaxHeight.takeIf { it > 0 }
171                 )
172 
173             var remeasure = false
174             if (coercedWidth != result.first) {
175                 constraints =
176                     Constraints(
177                         minWidth = coercedWidth,
178                         minHeight = constraints.minHeight,
179                         maxWidth = coercedWidth,
180                         maxHeight = constraints.maxHeight
181                     )
182                 remeasure = true
183             }
184             if (coercedHeight != result.second) {
185                 constraints =
186                     Constraints(
187                         minWidth = constraints.minWidth,
188                         minHeight = coercedHeight,
189                         maxWidth = constraints.maxWidth,
190                         maxHeight = coercedHeight
191                     )
192                 remeasure = true
193             }
194             if (remeasure) {
195                 if (DEBUG) {
196                     Log.d("CCL", "Remeasuring coerced $widgetId with $constraints")
197                 }
198                 measureWidget(constraintWidget, constraints)
199                 constraintWidget.isMeasureRequested = false
200             }
201         }
202 
203         val currentPlaceable = placeables[constraintWidget.companionWidget]
204         measure.measuredWidth = currentPlaceable?.width ?: constraintWidget.width
205         measure.measuredHeight = currentPlaceable?.height ?: constraintWidget.height
206         val baseline =
207             if (currentPlaceable != null && state.isBaselineNeeded(constraintWidget)) {
208                 currentPlaceable[FirstBaseline]
209             } else {
210                 AlignmentLine.Unspecified
211             }
212         measure.measuredHasBaseline = baseline != AlignmentLine.Unspecified
213         measure.measuredBaseline = baseline
214         lastMeasures
215             .getOrPut(widgetId) { arrayOf(0, 0, AlignmentLine.Unspecified) }
216             .copyFrom(measure)
217 
218         measure.measuredNeedsSolverPass =
219             measure.measuredWidth != measure.horizontalDimension ||
220                 measure.measuredHeight != measure.verticalDimension
221     }
222 
223     fun addLayoutInformationReceiver(layoutReceiver: LayoutInformationReceiver?) {
224         layoutInformationReceiver = layoutReceiver
225         layoutInformationReceiver?.setLayoutInformation(computedLayoutResult)
226     }
227 
228     open fun computeLayoutResult() {
229         val json = StringBuilder()
230         json.append("{ ")
231         json.append("  root: {")
232         json.append("interpolated: { left:  0,")
233         json.append("  top:  0,")
234         json.append("  right:   ${root.width} ,")
235         json.append("  bottom:  ${root.height} ,")
236         json.append(" } }")
237 
238         @Suppress("ListIterator")
239         for (child in root.children) {
240             val measurable = child.companionWidget
241             if (measurable !is Measurable) {
242                 if (child is Guideline) {
243                     json.append(" ${child.stringId}: {")
244                     if (child.orientation == ConstraintWidget.HORIZONTAL) {
245                         json.append(" type: 'hGuideline', ")
246                     } else {
247                         json.append(" type: 'vGuideline', ")
248                     }
249                     json.append(" interpolated: ")
250                     json.append(
251                         " { left: ${child.x}, top: ${child.y}, " +
252                             "right: ${child.x + child.width}, " +
253                             "bottom: ${child.y + child.height} }"
254                     )
255                     json.append("}, ")
256                 }
257                 continue
258             }
259             if (child.stringId == null) {
260                 val id = measurable.layoutId ?: measurable.constraintLayoutId
261                 child.stringId = id?.toString()
262             }
263             val frame = frameCache[measurable.anyOrNullId]?.widget?.frame
264             if (frame == null) {
265                 continue
266             }
267             json.append(" ${child.stringId}: {")
268             json.append(" interpolated : ")
269             frame.serialize(json, true)
270             json.append("}, ")
271         }
272         json.append(" }")
273         computedLayoutResult = json.toString()
274         layoutInformationReceiver?.setLayoutInformation(computedLayoutResult)
275     }
276 
277     /**
278      * Calculates the [Constraints] in one direction that should be used to measure a child, based
279      * on the solver measure request. Returns `true` if the constraints correspond to a wrap content
280      * measurement.
281      */
282     private fun obtainConstraints(
283         dimensionBehaviour: ConstraintWidget.DimensionBehaviour,
284         dimension: Int,
285         matchConstraintDefaultDimension: Int,
286         measureStrategy: Int,
287         otherDimensionResolved: Boolean,
288         currentDimensionResolved: Boolean,
289         rootMaxConstraint: Int,
290         outConstraints: IntArray
291     ): Boolean =
292         when (dimensionBehaviour) {
293             FIXED -> {
294                 outConstraints[0] = dimension
295                 outConstraints[1] = dimension
296                 false
297             }
298             WRAP_CONTENT -> {
299                 outConstraints[0] = 0
300                 outConstraints[1] = rootMaxConstraint
301                 true
302             }
303             MATCH_CONSTRAINT -> {
304                 if (DEBUG) {
305                     Log.d("CCL", "Measure strategy $measureStrategy")
306                     Log.d("CCL", "DW $matchConstraintDefaultDimension")
307                     Log.d("CCL", "ODR $otherDimensionResolved")
308                     Log.d("CCL", "IRH $currentDimensionResolved")
309                 }
310                 val useDimension =
311                     currentDimensionResolved ||
312                         (measureStrategy == TRY_GIVEN_DIMENSIONS ||
313                             measureStrategy == USE_GIVEN_DIMENSIONS) &&
314                             (measureStrategy == USE_GIVEN_DIMENSIONS ||
315                                 matchConstraintDefaultDimension != MATCH_CONSTRAINT_WRAP ||
316                                 otherDimensionResolved)
317                 if (DEBUG) {
318                     Log.d("CCL", "UD $useDimension")
319                 }
320                 outConstraints[0] = if (useDimension) dimension else 0
321                 outConstraints[1] = if (useDimension) dimension else rootMaxConstraint
322                 !useDimension
323             }
324             MATCH_PARENT -> {
325                 outConstraints[0] = rootMaxConstraint
326                 outConstraints[1] = rootMaxConstraint
327                 false
328             }
329         }
330 
331     private fun Array<Int>.copyFrom(measure: BasicMeasure.Measure) {
332         this[0] = measure.measuredWidth
333         this[1] = measure.measuredHeight
334         this[2] = measure.measuredBaseline
335     }
336 
337     fun performMeasure(
338         constraints: Constraints,
339         layoutDirection: LayoutDirection,
340         constraintSet: ConstraintSet,
341         measurables: List<Measurable>,
342         placeableMap: MutableMap<Measurable, Placeable>, // Initialized by caller, filled by us
343         optimizationLevel: Int,
344     ): IntSize {
345         this.placeables = placeableMap
346         if (measurables.isEmpty()) {
347             // TODO(b/335524398): Behavior with zero children is unexpected. It's also inconsistent
348             //      with ViewGroup, so this is a workaround to handle those cases the way it seems
349             //      right for this implementation.
350             return IntSize(constraints.minWidth, constraints.minHeight)
351         }
352 
353         // Define the size of the ConstraintLayout.
354         state.width(
355             if (constraints.hasFixedWidth) {
356                 SolverDimension.createFixed(constraints.maxWidth)
357             } else {
358                 SolverDimension.createWrap().min(constraints.minWidth)
359             }
360         )
361         state.height(
362             if (constraints.hasFixedHeight) {
363                 SolverDimension.createFixed(constraints.maxHeight)
364             } else {
365                 SolverDimension.createWrap().min(constraints.minHeight)
366             }
367         )
368         state.mParent.width.apply(state, root, ConstraintWidget.HORIZONTAL)
369         state.mParent.height.apply(state, root, ConstraintWidget.VERTICAL)
370         // Build constraint set and apply it to the state.
371         state.rootIncomingConstraints = constraints
372         state.isRtl = layoutDirection == LayoutDirection.Rtl
373         resetMeasureState()
374         if (constraintSet.isDirty(measurables)) {
375             state.reset()
376             constraintSet.applyTo(state, measurables)
377             buildMapping(state, measurables)
378             state.apply(root)
379         } else {
380             buildMapping(state, measurables)
381         }
382 
383         applyRootSize(constraints)
384         root.updateHierarchy()
385 
386         if (DEBUG) {
387             root.debugName = "ConstraintLayout"
388             root.children.fastForEach { child ->
389                 child.debugName =
390                     (child.companionWidget as? Measurable)?.layoutId?.toString() ?: "NOTAG"
391             }
392             Log.d("CCL", "ConstraintLayout is asked to measure with $constraints")
393             Log.d("CCL", root.toDebugString())
394             root.children.fastForEach { child -> Log.d("CCL", child.toDebugString()) }
395         }
396 
397         // No need to set sizes and size modes as we passed them to the state above.
398         root.optimizationLevel = optimizationLevel
399         root.measure(root.optimizationLevel, 0, 0, 0, 0, 0, 0, 0, 0)
400 
401         if (DEBUG) {
402             Log.d("CCL", "ConstraintLayout is at the end ${root.width} ${root.height}")
403         }
404         return IntSize(root.width, root.height)
405     }
406 
407     internal fun resetMeasureState() {
408         placeables.clear()
409         lastMeasures.clear()
410         frameCache.clear()
411     }
412 
413     protected fun applyRootSize(constraints: Constraints) {
414         root.width = constraints.maxWidth
415         root.height = constraints.maxHeight
416         forcedScaleFactor = Float.NaN
417         if (
418             layoutInformationReceiver != null &&
419                 layoutInformationReceiver?.getForcedWidth() != Int.MIN_VALUE
420         ) {
421             val forcedWidth = layoutInformationReceiver!!.getForcedWidth()
422             if (forcedWidth > root.width) {
423                 val scale = root.width / forcedWidth.toFloat()
424                 forcedScaleFactor = scale
425             } else {
426                 forcedScaleFactor = 1f
427             }
428             root.width = forcedWidth
429         }
430         if (
431             layoutInformationReceiver != null &&
432                 layoutInformationReceiver?.getForcedHeight() != Int.MIN_VALUE
433         ) {
434             val forcedHeight = layoutInformationReceiver!!.getForcedHeight()
435             var scaleFactor = 1f
436             if (forcedScaleFactor.isNaN()) {
437                 forcedScaleFactor = 1f
438             }
439             if (forcedHeight > root.height) {
440                 scaleFactor = root.height / forcedHeight.toFloat()
441             }
442             if (scaleFactor < forcedScaleFactor) {
443                 forcedScaleFactor = scaleFactor
444             }
445             root.height = forcedHeight
446         }
447     }
448 
449     fun Placeable.PlacementScope.performLayout(
450         measurables: List<Measurable>,
451         placeableMap: MutableMap<Measurable, Placeable>
452     ) {
453         placeables = placeableMap
454         if (frameCache.isEmpty()) {
455             root.children.fastForEach { child ->
456                 val measurable = child.companionWidget
457                 if (measurable !is Measurable) return@fastForEach
458                 val frame = WidgetFrame(child.frame.update())
459                 frameCache[measurable.anyOrNullId] = frame
460             }
461         }
462         measurables.fastForEach { measurable ->
463             val frame = frameCache[measurable.anyOrNullId] ?: return@fastForEach
464             val placeable = placeables[measurable] ?: return@fastForEach
465             placeWithFrameTransform(placeable, frame)
466         }
467         if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags.BOUNDS) {
468             computeLayoutResult()
469         }
470     }
471 
472     override fun didMeasures() {}
473 
474     /**
475      * Measure a [ConstraintWidget] with the given [constraints].
476      *
477      * Note that the [constraintWidget] could correspond to either a Composable or a Helper, which
478      * need to be measured differently.
479      *
480      * Returns a [Pair] with the result of the measurement, the first and second values are the
481      * measured width and height respectively.
482      */
483     private fun measureWidget(
484         constraintWidget: ConstraintWidget,
485         constraints: Constraints
486     ): IntIntPair {
487         val measurable = constraintWidget.companionWidget
488         val widgetId = constraintWidget.stringId
489         return when {
490             constraintWidget is VirtualLayout -> {
491                 // TODO: This step should really be performed within ConstraintWidgetContainer,
492                 //  compose-ConstraintLayout should only have to measure Composables/Measurables
493                 val widthMode =
494                     when {
495                         constraints.hasFixedWidth -> BasicMeasure.EXACTLY
496                         constraints.hasBoundedWidth -> BasicMeasure.AT_MOST
497                         else -> BasicMeasure.UNSPECIFIED
498                     }
499                 val heightMode =
500                     when {
501                         constraints.hasFixedHeight -> BasicMeasure.EXACTLY
502                         constraints.hasBoundedHeight -> BasicMeasure.AT_MOST
503                         else -> BasicMeasure.UNSPECIFIED
504                     }
505                 constraintWidget.measure(
506                     widthMode,
507                     constraints.maxWidth,
508                     heightMode,
509                     constraints.maxHeight
510                 )
511                 IntIntPair(constraintWidget.measuredWidth, constraintWidget.measuredHeight)
512             }
513             measurable is Measurable -> {
514                 val result = measurable.measure(constraints).also { placeables[measurable] = it }
515                 IntIntPair(result.width, result.height)
516             }
517             else -> {
518                 Log.w("CCL", "Nothing to measure for widget: $widgetId")
519                 IntIntPair(0, 0)
520             }
521         }
522     }
523 }
524