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