1 /*
<lambda>null2  * Copyright 2019 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.annotation.SuppressLint
20 import android.os.Handler
21 import android.os.Looper
22 import android.util.Log
23 import androidx.collection.IntIntPair
24 import androidx.compose.animation.core.Animatable
25 import androidx.compose.animation.core.AnimationSpec
26 import androidx.compose.animation.core.tween
27 import androidx.compose.foundation.Canvas
28 import androidx.compose.foundation.Image
29 import androidx.compose.foundation.background
30 import androidx.compose.foundation.layout.Box
31 import androidx.compose.foundation.layout.BoxScope
32 import androidx.compose.foundation.layout.LayoutScopeMarker
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.shape.RoundedCornerShape
35 import androidx.compose.foundation.text.BasicText
36 import androidx.compose.foundation.text.BasicTextField
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.LaunchedEffect
39 import androidx.compose.runtime.MutableState
40 import androidx.compose.runtime.RememberObserver
41 import androidx.compose.runtime.SideEffect
42 import androidx.compose.runtime.Stable
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableIntStateOf
45 import androidx.compose.runtime.mutableLongStateOf
46 import androidx.compose.runtime.mutableStateOf
47 import androidx.compose.runtime.neverEqualPolicy
48 import androidx.compose.runtime.remember
49 import androidx.compose.runtime.setValue
50 import androidx.compose.runtime.snapshots.SnapshotStateObserver
51 import androidx.compose.ui.Modifier
52 import androidx.compose.ui.draw.clip
53 import androidx.compose.ui.draw.scale
54 import androidx.compose.ui.geometry.Offset
55 import androidx.compose.ui.graphics.Color
56 import androidx.compose.ui.graphics.GraphicsLayerScope
57 import androidx.compose.ui.graphics.TransformOrigin
58 import androidx.compose.ui.graphics.drawscope.DrawScope
59 import androidx.compose.ui.layout.AlignmentLine
60 import androidx.compose.ui.layout.FirstBaseline
61 import androidx.compose.ui.layout.LayoutIdParentData
62 import androidx.compose.ui.layout.Measurable
63 import androidx.compose.ui.layout.MeasurePolicy
64 import androidx.compose.ui.layout.MultiMeasureLayout
65 import androidx.compose.ui.layout.ParentDataModifier
66 import androidx.compose.ui.layout.Placeable
67 import androidx.compose.ui.layout.layoutId
68 import androidx.compose.ui.node.Ref
69 import androidx.compose.ui.platform.InspectorValueInfo
70 import androidx.compose.ui.platform.LocalDensity
71 import androidx.compose.ui.platform.debugInspectorInfo
72 import androidx.compose.ui.res.painterResource
73 import androidx.compose.ui.semantics.semantics
74 import androidx.compose.ui.text.TextStyle
75 import androidx.compose.ui.unit.Constraints
76 import androidx.compose.ui.unit.Density
77 import androidx.compose.ui.unit.Dp
78 import androidx.compose.ui.unit.IntOffset
79 import androidx.compose.ui.unit.IntSize
80 import androidx.compose.ui.unit.LayoutDirection
81 import androidx.compose.ui.unit.TextUnit
82 import androidx.compose.ui.unit.dp
83 import androidx.compose.ui.unit.sp
84 import androidx.compose.ui.util.fastForEach
85 import androidx.compose.ui.util.fastForEachIndexed
86 import androidx.constraintlayout.core.parser.CLElement
87 import androidx.constraintlayout.core.parser.CLNumber
88 import androidx.constraintlayout.core.parser.CLObject
89 import androidx.constraintlayout.core.parser.CLParser
90 import androidx.constraintlayout.core.parser.CLParsingException
91 import androidx.constraintlayout.core.parser.CLString
92 import androidx.constraintlayout.core.state.ConstraintSetParser
93 import androidx.constraintlayout.core.state.Dimension.WRAP_DIMENSION
94 import androidx.constraintlayout.core.state.Registry
95 import androidx.constraintlayout.core.state.RegistryCallback
96 import androidx.constraintlayout.core.state.WidgetFrame
97 import androidx.constraintlayout.core.widgets.ConstraintWidget
98 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.FIXED
99 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT
100 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.MATCH_PARENT
101 import androidx.constraintlayout.core.widgets.ConstraintWidget.DimensionBehaviour.WRAP_CONTENT
102 import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_SPREAD
103 import androidx.constraintlayout.core.widgets.ConstraintWidget.MATCH_CONSTRAINT_WRAP
104 import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer
105 import androidx.constraintlayout.core.widgets.Guideline
106 import androidx.constraintlayout.core.widgets.HelperWidget
107 import androidx.constraintlayout.core.widgets.Optimizer
108 import androidx.constraintlayout.core.widgets.VirtualLayout
109 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure
110 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.TRY_GIVEN_DIMENSIONS
111 import androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.Measure.USE_GIVEN_DIMENSIONS
112 import kotlinx.coroutines.channels.Channel
113 import org.intellij.lang.annotations.Language
114 
115 /**
116  * Layout that positions its children according to the constraints between them.
117  *
118  * Constraints are defined within the content of this ConstraintLayout [Composable].
119  *
120  * Items in the layout that are to be constrained are initialized with
121  * [ConstraintLayoutScope.createRef]:
122  * ```
123  * val textRef = createRef()
124  * val imageRef = createRef()
125  * ```
126  *
127  * You may also use [ConstraintLayoutScope.createRefs] to declare up to 16 items using the
128  * destructuring declaration pattern:
129  * ```
130  * val (textRef, imageRef) = createRefs()
131  * ```
132  *
133  * Individual constraints are defined with
134  * [Modifier.constrainAs][ConstraintLayoutScope.constrainAs], this will also bind the Composable to
135  * the given [ConstrainedLayoutReference].
136  *
137  * So, a simple layout with a text in the middle and an image next to it may be declared like this
138  * (keep in mind, when using `center...`, `start` or `end` the layout direction will automatically
139  * change in RTL locales):
140  * ```
141  * ConstraintLayout(Modifier.fillMaxSize()) {
142  *     val (textRef, imageRef) = createRefs()
143  *     Text(
144  *         modifier = Modifier.constrainAs(textRef) {
145  *             centerTo(parent)
146  *         },
147  *         text = "Hello, World!"
148  *     )
149  *     Image(
150  *         modifier = Modifier.constrainAs(imageRef) {
151  *             centerVerticallyTo(textRef)
152  *             start.linkTo(textRef.end, margin = 8.dp)
153  *         },
154  *         imageVector = Icons.Default.Android,
155  *         contentDescription = null
156  *     )
157  * }
158  * ```
159  *
160  * See [ConstrainScope] to learn more about how to constrain elements together.
161  *
162  * ## Helpers
163  * You may also use helpers, a set of virtual (not shown on screen) components that provide special
164  * layout behaviors, you may find these in the [ConstraintLayoutScope] with the '`create...`'
165  * prefix, a few of these are **Guidelines**, **Chains** and **Barriers**.
166  *
167  * ### Guidelines
168  * Lines to which other [ConstrainedLayoutReference]s may be constrained to, these are defined at
169  * either a fixed or percent position from an anchor of the ConstraintLayout parent (top, bottom,
170  * start, end, absoluteLeft, absoluteRight).
171  *
172  * Example:
173  * ```
174  * val (textRef) = createRefs()
175  * val vG = createGuidelineFromStart(fraction = 0.3f)
176  * Text(
177  *     modifier = Modifier.constrainAs(textRef) {
178  *         centerVerticallyTo(parent)
179  *         centerAround(vG)
180  *     },
181  *     text = "Hello, World!"
182  * )
183  * ```
184  *
185  * See
186  * - [ConstraintLayoutScope.createGuidelineFromTop]
187  * - [ConstraintLayoutScope.createGuidelineFromBottom]
188  * - [ConstraintLayoutScope.createGuidelineFromStart]
189  * - [ConstraintLayoutScope.createGuidelineFromEnd]
190  * - [ConstraintLayoutScope.createGuidelineFromAbsoluteLeft]
191  * - [ConstraintLayoutScope.createGuidelineFromAbsoluteRight]
192  *
193  * ### Chains
194  * Chains may be either horizontal or vertical, these, take a set of [ConstrainedLayoutReference]s
195  * and create bi-directional constraints on each of them at the same orientation of the chain in the
196  * given order, meaning that an horizontal chain will create constraints between the start and end
197  * anchors.
198  *
199  * The result, a layout that evenly distributes the space within its elements.
200  *
201  * For example, to make a layout with three text elements distributed so that the spacing between
202  * them (and around them) is equal:
203  * ```
204  * val (textRef0, textRef1, textRef2) = createRefs()
205  * createHorizontalChain(textRef0, textRef1, textRef2, chainStyle = ChainStyle.Spread)
206  *
207  * Text(modifier = Modifier.constrainAs(textRef0) {}, text = "Hello")
208  * Text(modifier = Modifier.constrainAs(textRef1) {}, text = "Foo")
209  * Text(modifier = Modifier.constrainAs(textRef2) {}, text = "Bar")
210  * ```
211  *
212  * You may set margins within elements in a chain with [ConstraintLayoutScope.withChainParams]:
213  * ```
214  * val (textRef0, textRef1, textRef2) = createRefs()
215  * createHorizontalChain(
216  *     textRef0,
217  *     textRef1.withChainParams(startMargin = 100.dp, endMargin = 100.dp),
218  *     textRef2,
219  *     chainStyle = ChainStyle.Spread
220  * )
221  *
222  * Text(modifier = Modifier.constrainAs(textRef0) {}, text = "Hello")
223  * Text(modifier = Modifier.constrainAs(textRef1) {}, text = "Foo")
224  * Text(modifier = Modifier.constrainAs(textRef2) {}, text = "Bar")
225  * ```
226  *
227  * You can also change the way space is distributed, as chains have three different styles:
228  * - [ChainStyle.Spread] Layouts are evenly distributed after margins are accounted for (the space
229  *   around and between each item is even). This is the **default** style for chains.
230  * - [ChainStyle.SpreadInside] The first and last layouts are affixed to each end of the chain, and
231  *   the rest of the items are evenly distributed (after margins are accounted for). I.e.: Items are
232  *   spread from the inside, distributing the space between them with no space around the first and
233  *   last items.
234  * - [ChainStyle.Packed] The layouts are packed together after margins are accounted for, by
235  *   default, they're packed together at the middle, you can change this behavior with the **bias**
236  *   parameter of [ChainStyle.Packed].
237  * - Alternatively, you can make every Layout in the chain to be [Dimension.fillToConstraints] and
238  *   then set a particular weight to each of them to create a **weighted chain**.
239  *
240  * #### Weighted Chain
241  * Weighted chains are useful when you want the size of the elements to depend on the remaining size
242  * of the chain. As opposed to just distributing the space around and/or in-between the items.
243  *
244  * For example, to create a layout with three text elements in a row where each element takes the
245  * exact same size regardless of content, you can use a simple weighted chain where each item has
246  * the same weight:
247  * ```
248  * val (textRef0, textRef1, textRef2) = createRefs()
249  * createHorizontalChain(
250  *     textRef0.withChainParams(weight = 1f),
251  *     textRef1.withChainParams(weight = 1f),
252  *     textRef2.withChainParams(weight = 1f),
253  *     chainStyle = ChainStyle.Spread
254  * )
255  *
256  * Text(modifier = Modifier.background(Color.Cyan).constrainAs(textRef0) {
257  *     width = Dimension.fillToConstraints
258  * }, text = "Hello, World!")
259  * Text(modifier = Modifier.background(Color.Red).constrainAs(textRef1) {
260  *     width = Dimension.fillToConstraints
261  * }, text = "Foo")
262  * Text(modifier = Modifier.background(Color.Cyan).constrainAs(textRef2) {
263  *     width = Dimension.fillToConstraints
264  * }, text = "This text is six words long")
265  * ```
266  *
267  * This way, the texts will horizontally occupy the same space even if one of them is significantly
268  * larger than the others.
269  *
270  * Keep in mind that chains have a relatively high performance cost. For example, if you plan on
271  * having multiple chains one below the other, consider instead, applying just one chain and using
272  * it as a reference to constrain all other elements to the ones that match their position in that
273  * one chain. It may provide increased performance with no significant changes in the layout output.
274  *
275  * Alternatively, consider if other helpers such as [ConstraintLayoutScope.createGrid] can
276  * accomplish the same layout.
277  *
278  * See
279  * - [ConstraintLayoutScope.createHorizontalChain]
280  * - [ConstraintLayoutScope.createVerticalChain]
281  * - [ConstraintLayoutScope.withChainParams]
282  *
283  * ### Barriers
284  * Barriers take a set of [ConstrainedLayoutReference]s and creates the most further point in a
285  * given direction where other [ConstrainedLayoutReference] can constrain to.
286  *
287  * This is useful in situations where elements in a layout may have different sizes but you want to
288  * always constrain to the largest item, for example, if you have a text element on top of another
289  * and want an image to always be constrained to the end of them:
290  * ```
291  * val (textRef0, textRef1, imageRef) = createRefs()
292  *
293  * // Creates a point at the furthest end anchor from the elements in the barrier
294  * val endTextsBarrier = createEndBarrier(textRef0, textRef1)
295  *
296  * Text(
297  *     modifier = Modifier.constrainAs(textRef0) {
298  *         centerTo(parent)
299  *     },
300  *     text = "Hello, World!"
301  * )
302  * Text(
303  *     modifier = Modifier.constrainAs(textRef1) {
304  *         top.linkTo(textRef0.bottom)
305  *         start.linkTo(textRef0.start)
306  *     },
307  *     text = "Foo Bar"
308  * )
309  * Image(
310  *     modifier = Modifier.constrainAs(imageRef) {
311  *         top.linkTo(textRef0.top)
312  *         bottom.linkTo(textRef1.bottom)
313  *
314  *         // Image will always be at the end of both texts, regardless of their size
315  *         start.linkTo(endTextsBarrier, margin = 8.dp)
316  *     },
317  *     imageVector = Icons.Default.Android,
318  *     contentDescription = null
319  * )
320  * ```
321  *
322  * Be careful not to constrain a [ConstrainedLayoutReference] to a barrier that references it or
323  * that depends on it indirectly. This creates a cyclic dependency that results in unsupported
324  * layout behavior.
325  *
326  * See
327  * - [ConstraintLayoutScope.createTopBarrier]
328  * - [ConstraintLayoutScope.createBottomBarrier]
329  * - [ConstraintLayoutScope.createStartBarrier]
330  * - [ConstraintLayoutScope.createEndBarrier]
331  * - [ConstraintLayoutScope.createAbsoluteLeftBarrier]
332  * - [ConstraintLayoutScope.createAbsoluteRightBarrier]
333  *
334  * **Tip**: If you notice that you are creating many different constraints based on
335  * [State][androidx.compose.runtime.State] variables or configuration changes, consider using the
336  * [ConstraintSet] pattern instead, makes it clearer to distinguish different layouts and allows you
337  * to automatically animate the layout when the provided [ConstraintSet] is different.
338  *
339  * @param modifier Modifier to apply to this layout node.
340  * @param optimizationLevel Optimization flags for ConstraintLayout. The default is
341  *   [Optimizer.OPTIMIZATION_STANDARD].
342  * @param animateChangesSpec Null by default. Otherwise, ConstraintLayout will animate the layout if
343  *   there were any changes on the constraints during recomposition using the given [AnimationSpec].
344  *   If there's a change while the layout is still animating, the current animation will complete
345  *   before animating to the latest changes. For more control in the animation consider using
346  *   [MotionLayout] instead.
347  * @param finishedAnimationListener Lambda called whenever an animation due to [animateChangesSpec]
348  *   finishes.
349  * @param content Content of this layout node.
350  */
351 @Composable
352 inline fun ConstraintLayout(
353     modifier: Modifier = Modifier,
354     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
355     animateChangesSpec: AnimationSpec<Float>? = null,
356     noinline finishedAnimationListener: (() -> Unit)? = null,
357     crossinline content: @Composable ConstraintLayoutScope.() -> Unit
358 ) {
359     if (animateChangesSpec != null) {
360         val start: MutableState<ConstraintSet?> = remember { mutableStateOf(null) }
361         val end: MutableState<ConstraintSet?> = remember { mutableStateOf(null) }
362         val scope = remember { ConstraintLayoutScope().apply { isAnimateChanges = true } }
363         val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
364         val compositionSource = remember {
365             Ref<CompositionSource>().apply { value = CompositionSource.Unknown }
366         }
367         val channel = remember { Channel<ConstraintSet>(Channel.CONFLATED) }
368 
369         val contentDelegate: @Composable () -> Unit = {
370             // Perform a reassignment to the State tracker, this will force readers to recompose at
371             // the same pass as the content. The only expected reader is our MeasurePolicy.
372             contentTracker.value = Unit
373 
374             if (compositionSource.value == CompositionSource.Unknown) {
375                 // Set the content as the original composition source if the MotionLayout was not
376                 // recomposed by the caller or by itself
377                 compositionSource.value = CompositionSource.Content
378             }
379 
380             // Resetting the scope also resets the underlying ConstraintSet
381             scope.reset()
382             content(scope) // The ConstraintSet is built at this step
383 
384             SideEffect {
385                 // Extract a copy of the underlying ConstraintSet and send it through the channel
386                 // We do it within a SideEffect to avoid a recomposition loop from reading and
387                 // writing the State variables for `end` and `start`
388                 val cSet = RawConstraintSet(scope.containerObject.clone())
389                 if (start.value == null || end.value == null) {
390                     // guarantee first constraintSet here
391                     start.value = cSet
392                     end.value = start.value
393                 } else {
394                     // send to channel
395                     channel.trySend(cSet)
396                 }
397             }
398         }
399 
400         LateMotionLayout(
401             start = start,
402             end = end,
403             animationSpec = animateChangesSpec,
404             channel = channel,
405             contentTracker = contentTracker,
406             compositionSource = compositionSource,
407             optimizationLevel = optimizationLevel,
408             finishedAnimationListener = finishedAnimationListener,
409             modifier = modifier,
410             content = contentDelegate
411         )
412         return
413     }
414 
415     val density = LocalDensity.current
416     val measurer = remember { Measurer2(density) }
417     val scope = remember { ConstraintLayoutScope() }
418     val remeasureRequesterState = remember { mutableStateOf(false) }
419     val constraintSet = remember { ConstraintSetForInlineDsl(scope) }
420     val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
421 
422     val measurePolicy = MeasurePolicy { measurables, constraints ->
423         // Map to properly capture Placeables across Measure and Layout passes
424         val placeableMap = mutableMapOf<Measurable, Placeable>()
425 
426         // Call to invalidate measure on content recomposition
427         contentTracker.value
428         val layoutSize =
429             measurer.performMeasure(
430                 constraints = constraints,
431                 layoutDirection = layoutDirection,
432                 constraintSet = constraintSet,
433                 measurables = measurables,
434                 placeableMap = placeableMap,
435                 optimizationLevel = optimizationLevel
436             )
437         // We read the remeasurement requester state, to request remeasure when the value
438         // changes. This will happen when the scope helpers are changing at recomposition.
439         remeasureRequesterState.value
440 
441         layout(layoutSize.width, layoutSize.height) {
442             with(measurer) { performLayout(measurables = measurables, placeableMap = placeableMap) }
443         }
444     }
445 
446     val onHelpersChanged = {
447         // If the helpers have changed, we need to request remeasurement. To achieve this,
448         // we are changing this boolean state that is read during measurement.
449         remeasureRequesterState.value = !remeasureRequesterState.value
450         constraintSet.knownDirty = true
451     }
452 
453     @Suppress("Deprecation")
454     MultiMeasureLayout(
455         modifier = modifier.semantics { designInfoProvider = measurer },
456         measurePolicy = measurePolicy,
457         content = {
458             // Perform a reassignment to the State tracker, this will force readers to recompose at
459             // the same pass as the content. The only expected reader is our MeasurePolicy.
460             contentTracker.value = Unit
461             val previousHelpersHashCode = scope.helpersHashCode
462             scope.reset()
463             scope.content()
464             if (scope.helpersHashCode != previousHelpersHashCode) {
465                 // onHelpersChanged writes non-snapshot state so it can't be called directly from
466                 // composition. It also reads snapshot state, so calling it from composition causes
467                 // an extra recomposition.
468                 SideEffect(onHelpersChanged)
469             }
470         }
471     )
472 }
473 
474 @Deprecated(
475     message = "Prefer version that takes a nullable AnimationSpec to animate changes.",
476     level = DeprecationLevel.WARNING,
477     replaceWith =
478         ReplaceWith(
479             "ConstraintLayout(" +
480                 "modifier = modifier, " +
481                 "optimizationLevel = optimizationLevel, " +
482                 "animateChangesSpec = animationSpec, " +
483                 "finishedAnimationListener = finishedAnimationListener" +
484                 ") { content() }"
485         )
486 )
487 @Composable
ConstraintLayoutnull488 inline fun ConstraintLayout(
489     modifier: Modifier = Modifier,
490     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
491     animateChanges: Boolean = false,
492     animationSpec: AnimationSpec<Float> = tween<Float>(),
493     noinline finishedAnimationListener: (() -> Unit)? = null,
494     crossinline content: @Composable ConstraintLayoutScope.() -> Unit
495 ) {
496     ConstraintLayout(
497         modifier = modifier,
498         optimizationLevel = optimizationLevel,
499         animateChangesSpec = if (animateChanges) animationSpec else null,
500         finishedAnimationListener = finishedAnimationListener,
501         content = content
502     )
503 }
504 
505 @PublishedApi
506 internal class ConstraintSetForInlineDsl(val scope: ConstraintLayoutScope) :
507     ConstraintSet, RememberObserver {
508     private var handler: Handler? = null
<lambda>null509     private val observer = SnapshotStateObserver {
510         if (Looper.myLooper() == Looper.getMainLooper()) {
511             it()
512         } else {
513             val h = handler ?: Handler(Looper.getMainLooper()).also { h -> handler = h }
514             h.post(it)
515         }
516     }
517 
applyTonull518     override fun applyTo(state: State, measurables: List<Measurable>) {
519         previousDatas.clear()
520         observer.observeReads(Unit, onCommitAffectingConstrainLambdas) {
521             measurables.fastForEach { measurable ->
522                 val parentData = measurable.parentData as? ConstraintLayoutParentData
523                 // Run the constrainAs block of the child, to obtain its constraints.
524                 if (parentData != null) {
525                     val ref = parentData.ref
526                     val container = with(scope) { ref.asCLContainer() }
527                     val constrainScope = ConstrainScope(ref.id, container)
528                     parentData.constrain(constrainScope)
529                 }
530                 previousDatas.add(parentData)
531             }
532             scope.applyTo(state)
533         }
534         knownDirty = false
535     }
536 
537     var knownDirty = true
538 
_null539     private val onCommitAffectingConstrainLambdas = { _: Unit -> knownDirty = true }
540 
isDirtynull541     override fun isDirty(measurables: List<Measurable>): Boolean {
542         if (knownDirty || measurables.size != previousDatas.size) return true
543 
544         measurables.fastForEachIndexed { index, measurable ->
545             if (measurable.parentData as? ConstraintLayoutParentData != previousDatas[index]) {
546                 return true
547             }
548         }
549 
550         return false
551     }
552 
553     private val previousDatas = mutableListOf<ConstraintLayoutParentData?>()
554 
onRememberednull555     override fun onRemembered() {
556         observer.start()
557     }
558 
onForgottennull559     override fun onForgotten() {
560         observer.stop()
561         observer.clear()
562     }
563 
onAbandonednull564     override fun onAbandoned() {}
565 }
566 
567 /**
568  * Layout that positions its children according to the constraints between them.
569  *
570  * This [Composable] of [ConstraintLayout] takes a [ConstraintSet] where the layout is defined using
571  * references and constraints.
572  *
573  * Layouts referenced in the given [constraintSet] can be bound to immediate child Composables using
574  * [Modifier.layoutId], where the given layoutIds match each named reference.
575  *
576  * So, a simple layout with a text in the middle and an image next to it may be declared like this:
577  * ```
578  * // IDs
579  * val textId = "text"
580  * val imageId = "image"
581  *
582  * // Layout definition with references and constraints
583  * val constraintSet = remember {
584  *     ConstraintSet {
585  *         val (textRef, imageRef) = createRefsFor(textId, imageId)
586  *         constrain(textRef) {
587  *             centerTo(parent)
588  *         }
589  *         constrain(imageRef) {
590  *             centerVerticallyTo(textRef)
591  *             start.linkTo(textRef.end, margin = 8.dp)
592  *         }
593  *     }
594  * }
595  *
596  * // ConstraintLayout uses our given ConstraintSet
597  * ConstraintLayout(
598  *     constraintSet = constraintSet,
599  *     modifier = Modifier.fillMaxSize()
600  * ) {
601  *     // References are bound to Composables using Modifier.layoutId(Any)
602  *     Text(
603  *         modifier = Modifier.layoutId(textId),
604  *         text = "Hello, World!"
605  *     )
606  *     Image(
607  *         modifier = Modifier.layoutId(imageId),
608  *         imageVector = Icons.Default.Android,
609  *         contentDescription = null
610  *     )
611  * }
612  * ```
613  *
614  * See [ConstraintSet] to learn more on how to declare layouts using constraints.
615  *
616  * ### Handling of ConstraintSet objects
617  *
618  * You typically want to *`remember`* declared [ConstraintSet]s, to avoid unnecessary allocations on
619  * recomposition, if the [ConstraintSetScope] block consumes any
620  * [State][androidx.compose.runtime.State] variables, then something like *`remember {
621  * derivedStateOf { ConstraintSet { ... } } }`* would be more appropriate.
622  *
623  * However, note in the example above that our ConstraintSet is constant, so we can declare it at a
624  * top level, improving overall Composition performance:
625  * ```
626  * private const val TEXT_ID = "text"
627  * private const val IMAGE_ID = "image"
628  * private val mConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
629  *     ConstraintSet {
630  *         val (textRef, imageRef) = createRefsFor(TEXT_ID, IMAGE_ID)
631  *         constrain(textRef) {
632  *             centerTo(parent)
633  *         }
634  *         constrain(imageRef) {
635  *             centerVerticallyTo(textRef)
636  *             start.linkTo(textRef.end, margin = 8.dp)
637  *         }
638  *     }
639  * }
640  *
641  * @Preview
642  * @Composable
643  * fun ConstraintSetExample() {
644  *     ConstraintLayout(
645  *         constraintSet = mConstraintSet,
646  *         modifier = Modifier.fillMaxSize()
647  *     ) {
648  *         Text(
649  *             modifier = Modifier.layoutId(TEXT_ID),
650  *             text = "Hello, World!"
651  *         )
652  *         Image(
653  *             modifier = Modifier.layoutId(IMAGE_ID),
654  *             imageVector = Icons.Default.Android,
655  *             contentDescription = null
656  *         )
657  *     }
658  * }
659  * ```
660  *
661  * This pattern (as opposed to defining constraints with [ConstraintLayoutScope.constrainAs]) is
662  * preferred when you want different layouts to be produced on different
663  * [State][androidx.compose.runtime.State] variables or configuration changes. As it makes it easier
664  * to create distinguishable layouts, for example when building adaptive layouts based on Window
665  * size class:
666  * ```
667  * private const val NAV_BAR_ID = "navBar"
668  * private const val CONTENT_ID = "content"
669  *
670  * private val compactConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
671  *     ConstraintSet {
672  *         val (navBarRef, contentRef) = createRefsFor(NAV_BAR_ID, CONTENT_ID)
673  *
674  *         // Navigation bar at the bottom for Compact devices
675  *         constrain(navBarRef) {
676  *             width = Dimension.percent(1f)
677  *             height = 40.dp.asDimension()
678  *             bottom.linkTo(parent.bottom)
679  *         }
680  *
681  *         constrain(contentRef) {
682  *             width = Dimension.percent(1f)
683  *             height = Dimension.fillToConstraints
684  *
685  *             top.linkTo(parent.top)
686  *             bottom.linkTo(navBarRef.top)
687  *         }
688  *     }
689  * }
690  *
691  * private val mediumConstraintSet by lazy(LazyThreadSafetyMode.NONE) {
692  *     ConstraintSet {
693  *         val (navBarRef, contentRef) = createRefsFor(NAV_BAR_ID, CONTENT_ID)
694  *
695  *         // Navigation bar at the start on Medium class devices
696  *         constrain(navBarRef) {
697  *             width = 40.dp.asDimension()
698  *             height = Dimension.percent(1f)
699  *
700  *             start.linkTo(parent.start)
701  *         }
702  *
703  *         constrain(contentRef) {
704  *             width = Dimension.fillToConstraints
705  *             height = Dimension.percent(1f)
706  *
707  *             start.linkTo(navBarRef.end)
708  *             end.linkTo(parent.end)
709  *         }
710  *     }
711  * }
712  *
713  * @Composable
714  * fun MyAdaptiveLayout(
715  *     windowWidthSizeClass: WindowWidthSizeClass
716  * ) {
717  *     val constraintSet = if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
718  *         compactConstraintSet
719  *     }
720  *     else {
721  *         mediumConstraintSet
722  *     }
723  *     ConstraintLayout(
724  *         constraintSet = constraintSet,
725  *         modifier = Modifier.fillMaxSize()
726  *     ) {
727  *         Box(Modifier.background(Color.Blue).layoutId(NAV_BAR_ID))
728  *         Box(Modifier.background(Color.Red).layoutId(CONTENT_ID))
729  *     }
730  * }
731  * ```
732  *
733  * ### Animate Changes
734  *
735  * When using multiple discrete [ConstraintSet]s, you may pass non-null object to
736  * [animateChangesSpec]. With this, whenever ConstraintLayout is recomposed with a different
737  * [ConstraintSet] (by equality), it will animate all its children using the given [AnimationSpec].
738  *
739  * On the example above, using [animateChangesSpec] would result on the layout being animated when
740  * the device changes to non-compact window class, typical behavior in some Foldable devices.
741  *
742  * If more control is needed, we recommend using [MotionLayout] instead, which has a very similar
743  * pattern through the [MotionScene] object.
744  *
745  * @param constraintSet The [ConstraintSet] that describes the expected layout, defined references
746  *   should be bound to Composables with [Modifier.layoutId][androidx.compose.ui.layout.layoutId].
747  * @param modifier Modifier to apply to this layout node.
748  * @param optimizationLevel Optimization flags for ConstraintLayout. The default is
749  *   [Optimizer.OPTIMIZATION_STANDARD].
750  * @param animateChangesSpec Null by default. Otherwise, ConstraintLayout will animate the layout if
751  *   a different [ConstraintSet] is provided on recomposition using the given [AnimationSpec]. If
752  *   there's a change in [ConstraintSet] while the layout is still animating, the current animation
753  *   will complete before animating to the latest changes. For more control in the animation
754  *   consider using [MotionLayout] instead.
755  * @param finishedAnimationListener Lambda called whenever an animation due to [animateChangesSpec]
756  *   finishes.
757  * @param content Content of this layout node.
758  */
759 @OptIn(ExperimentalMotionApi::class) // To support animateChangesSpec
760 @Composable
761 inline fun ConstraintLayout(
762     constraintSet: ConstraintSet,
763     modifier: Modifier = Modifier,
764     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
765     animateChangesSpec: AnimationSpec<Float>? = null,
766     noinline finishedAnimationListener: (() -> Unit)? = null,
767     crossinline content: @Composable () -> Unit
768 ) {
769     if (animateChangesSpec != null) {
<lambda>null770         var startConstraint by remember { mutableStateOf(constraintSet) }
<lambda>null771         var endConstraint by remember { mutableStateOf(constraintSet) }
<lambda>null772         val progress = remember { Animatable(0.0f) }
<lambda>null773         val channel = remember { Channel<ConstraintSet>(Channel.CONFLATED) }
<lambda>null774         val direction = remember { mutableIntStateOf(1) }
775 
<lambda>null776         SideEffect { channel.trySend(constraintSet) }
777 
<lambda>null778         LaunchedEffect(channel) {
779             for (constraints in channel) {
780                 val newConstraints = channel.tryReceive().getOrNull() ?: constraints
781                 val currentConstraints =
782                     if (direction.intValue == 1) startConstraint else endConstraint
783                 if (newConstraints != currentConstraints) {
784                     if (direction.intValue == 1) {
785                         endConstraint = newConstraints
786                     } else {
787                         startConstraint = newConstraints
788                     }
789                     progress.animateTo(direction.intValue.toFloat(), animateChangesSpec)
790                     direction.intValue = if (direction.intValue == 1) 0 else 1
791                     finishedAnimationListener?.invoke()
792                 }
793             }
794         }
795         MotionLayout(
796             start = startConstraint,
797             end = endConstraint,
798             progress = progress.value,
799             modifier = modifier,
<lambda>null800             content = { content() }
801         )
802     } else {
<lambda>null803         val needsUpdate = remember { mutableLongStateOf(0L) }
804 
<lambda>null805         val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
806         val density = LocalDensity.current
<lambda>null807         val measurer = remember { Measurer2(density) }
measurablesnull808         val measurePolicy = MeasurePolicy { measurables, constraints ->
809             // Map to properly capture Placeables across Measure and Layout passes
810             val placeableMap = mutableMapOf<Measurable, Placeable>()
811 
812             // Call to invalidate measure on content recomposition
813             contentTracker.value
814             val layoutSize =
815                 measurer.performMeasure(
816                     constraints = constraints,
817                     layoutDirection = layoutDirection,
818                     constraintSet = constraintSet,
819                     measurables = measurables,
820                     placeableMap = placeableMap,
821                     optimizationLevel = optimizationLevel
822                 )
823             layout(layoutSize.width, layoutSize.height) {
824                 with(measurer) {
825                     performLayout(measurables = measurables, placeableMap = placeableMap)
826                 }
827             }
828         }
829         if (constraintSet is EditableJSONLayout) {
830             constraintSet.setUpdateFlag(needsUpdate)
831         }
832         measurer.addLayoutInformationReceiver(constraintSet as? LayoutInformationReceiver)
833 
834         val forcedScaleFactor = measurer.forcedScaleFactor
835         if (!forcedScaleFactor.isNaN()) {
836             val mod = modifier.scale(measurer.forcedScaleFactor)
<lambda>null837             Box {
838                 @Suppress("DEPRECATION")
839                 MultiMeasureLayout(
840                     modifier = mod.semantics { designInfoProvider = measurer },
841                     measurePolicy = measurePolicy,
842                     content = @SuppressLint("UnnecessaryLambdaCreation") { content() }
843                 )
844             }
845         } else {
846             @Suppress("DEPRECATION")
847             MultiMeasureLayout(
<lambda>null848                 modifier = modifier.semantics { designInfoProvider = measurer },
849                 measurePolicy = measurePolicy,
<lambda>null850                 content = {
851                     // Perform a reassignment to the State tracker, this will force readers to
852                     // recompose at the same pass as the content. The only expected reader is our
853                     // MeasurePolicy.
854                     contentTracker.value = Unit
855                     content()
856                 }
857             )
858         }
859     }
860 }
861 
862 @Deprecated(
863     message = "Prefer version that takes a nullable AnimationSpec to animate changes.",
864     level = DeprecationLevel.WARNING,
865     replaceWith =
866         ReplaceWith(
867             "ConstraintLayout(" +
868                 "constraintSet = constraintSet, " +
869                 "modifier = modifier, " +
870                 "optimizationLevel = optimizationLevel, " +
871                 "animateChangesSpec = animationSpec, " +
872                 "finishedAnimationListener = finishedAnimationListener" +
873                 ") { content() }"
874         )
875 )
876 @Composable
877 inline fun ConstraintLayout(
878     constraintSet: ConstraintSet,
879     modifier: Modifier = Modifier,
880     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
881     animateChanges: Boolean = false,
882     animationSpec: AnimationSpec<Float> = tween<Float>(),
883     noinline finishedAnimationListener: (() -> Unit)? = null,
884     crossinline content: @Composable () -> Unit
885 ) {
886     ConstraintLayout(
887         constraintSet = constraintSet,
888         modifier = modifier,
889         optimizationLevel = optimizationLevel,
890         animateChangesSpec = if (animateChanges) animationSpec else null,
891         finishedAnimationListener = finishedAnimationListener,
892         content = content
893     )
894 }
895 
896 /** Scope used by the inline DSL of [ConstraintLayout]. */
897 @LayoutScopeMarker
898 class ConstraintLayoutScope @PublishedApi internal constructor() : ConstraintLayoutBaseScope(null) {
899     /**
900      * Creates one [ConstrainedLayoutReference], which needs to be assigned to a layout within the
901      * [ConstraintLayout] as part of [Modifier.constrainAs]. To create more references at the same
902      * time, see [createRefs].
903      */
createRefnull904     fun createRef(): ConstrainedLayoutReference =
905         childrenRefs.getOrNull(childId++)
906             ?: ConstrainedLayoutReference(childId).also { childrenRefs.add(it) }
907 
908     /**
909      * Convenient way to create multiple [ConstrainedLayoutReference]s, which need to be assigned to
910      * layouts within the [ConstraintLayout] as part of [Modifier.constrainAs]. To create just one
911      * reference, see [createRef].
912      */
913     @Stable
createRefsnull914     fun createRefs(): ConstraintLayoutScope.ConstrainedLayoutReferences =
915         referencesObject ?: ConstrainedLayoutReferences().also { referencesObject = it }
916 
917     /**
918      * Indicates whether we expect to animate changes. This is important since normally
919      * ConstraintLayout evaluates constraints at the measure step, but MotionLayout needs to know
920      * the constraints to enter the measure step.
921      */
922     @PublishedApi internal var isAnimateChanges = false
923 
924     private var referencesObject: ConstrainedLayoutReferences? = null
925 
926     private val ChildrenStartIndex = 0
927     private var childId = ChildrenStartIndex
928     private val childrenRefs = ArrayList<ConstrainedLayoutReference>()
929 
resetnull930     override fun reset() {
931         super.reset()
932         childId = ChildrenStartIndex
933     }
934 
935     /** Convenience API for creating multiple [ConstrainedLayoutReference] via [createRefs]. */
936     inner class ConstrainedLayoutReferences internal constructor() {
component1null937         operator fun component1(): ConstrainedLayoutReference = createRef()
938 
939         operator fun component2(): ConstrainedLayoutReference = createRef()
940 
941         operator fun component3(): ConstrainedLayoutReference = createRef()
942 
943         operator fun component4(): ConstrainedLayoutReference = createRef()
944 
945         operator fun component5(): ConstrainedLayoutReference = createRef()
946 
947         operator fun component6(): ConstrainedLayoutReference = createRef()
948 
949         operator fun component7(): ConstrainedLayoutReference = createRef()
950 
951         operator fun component8(): ConstrainedLayoutReference = createRef()
952 
953         operator fun component9(): ConstrainedLayoutReference = createRef()
954 
955         operator fun component10(): ConstrainedLayoutReference = createRef()
956 
957         operator fun component11(): ConstrainedLayoutReference = createRef()
958 
959         operator fun component12(): ConstrainedLayoutReference = createRef()
960 
961         operator fun component13(): ConstrainedLayoutReference = createRef()
962 
963         operator fun component14(): ConstrainedLayoutReference = createRef()
964 
965         operator fun component15(): ConstrainedLayoutReference = createRef()
966 
967         operator fun component16(): ConstrainedLayoutReference = createRef()
968     }
969 
970     /**
971      * [Modifier] that defines the constraints, as part of a [ConstraintLayout], of the layout
972      * element.
973      */
974     @Stable
975     fun Modifier.constrainAs(
976         ref: ConstrainedLayoutReference,
977         constrainBlock: ConstrainScope.() -> Unit
978     ): Modifier {
979         if (isAnimateChanges) {
980             // When we are expecting to animate changes, we need to preemptively obtain the
981             // constraints from the DSL since MotionLayout is not designed to evaluate the DSL
982             val container = ref.asCLContainer()
983             ConstrainScope(ref.id, container).constrainBlock()
984         }
985         return this.then(ConstrainAsModifier(ref, constrainBlock))
986     }
987 
988     @Stable
989     private class ConstrainAsModifier(
990         private val ref: ConstrainedLayoutReference,
991         private val constrainBlock: ConstrainScope.() -> Unit
992     ) :
993         ParentDataModifier,
994         InspectorValueInfo(
<lambda>null995             debugInspectorInfo {
996                 name = "constrainAs"
997                 properties["ref"] = ref
998                 properties["constrainBlock"] = constrainBlock
999             }
1000         ) {
modifyParentDatanull1001         override fun Density.modifyParentData(parentData: Any?) =
1002             ConstraintLayoutParentData(ref, constrainBlock)
1003 
1004         override fun hashCode() = constrainBlock.hashCode()
1005 
1006         override fun equals(other: Any?) =
1007             constrainBlock === (other as? ConstrainAsModifier)?.constrainBlock
1008     }
1009 }
1010 
1011 /** Scope used by the [ConstraintSet] DSL. */
1012 @LayoutScopeMarker
1013 class ConstraintSetScope internal constructor(extendFrom: CLObject?) :
1014     ConstraintLayoutBaseScope(extendFrom) {
1015     private var generatedCount = 0
1016 
1017     /**
1018      * Generate an ID to be used as fallback if the user didn't provide enough parameters to
1019      * [createRefsFor].
1020      *
1021      * Not intended to be used, but helps prevent runtime issues.
1022      */
1023     private fun nextId() = "androidx.constraintlayout.id" + generatedCount++
1024 
1025     /**
1026      * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element with
1027      * [id].
1028      */
1029     fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id)
1030 
1031     /**
1032      * Convenient way to create multiple [ConstrainedLayoutReference] with one statement, the [ids]
1033      * provided should match Composables within ConstraintLayout using [Modifier.layoutId].
1034      *
1035      * Example:
1036      * ```
1037      * val (box, text, button) = createRefsFor("box", "text", "button")
1038      * ```
1039      *
1040      * Note that the number of ids should match the number of variables assigned.
1041      *
1042      * To create a singular [ConstrainedLayoutReference] see [createRefFor].
1043      */
1044     fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
1045         ConstrainedLayoutReferences(arrayOf(*ids))
1046 
1047     inner class ConstrainedLayoutReferences internal constructor(private val ids: Array<Any>) {
1048         operator fun component1(): ConstrainedLayoutReference =
1049             ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
1050 
1051         operator fun component2(): ConstrainedLayoutReference =
1052             createRefFor(ids.getOrElse(1) { nextId() })
1053 
1054         operator fun component3(): ConstrainedLayoutReference =
1055             createRefFor(ids.getOrElse(2) { nextId() })
1056 
1057         operator fun component4(): ConstrainedLayoutReference =
1058             createRefFor(ids.getOrElse(3) { nextId() })
1059 
1060         operator fun component5(): ConstrainedLayoutReference =
1061             createRefFor(ids.getOrElse(4) { nextId() })
1062 
1063         operator fun component6(): ConstrainedLayoutReference =
1064             createRefFor(ids.getOrElse(5) { nextId() })
1065 
1066         operator fun component7(): ConstrainedLayoutReference =
1067             createRefFor(ids.getOrElse(6) { nextId() })
1068 
1069         operator fun component8(): ConstrainedLayoutReference =
1070             createRefFor(ids.getOrElse(7) { nextId() })
1071 
1072         operator fun component9(): ConstrainedLayoutReference =
1073             createRefFor(ids.getOrElse(8) { nextId() })
1074 
1075         operator fun component10(): ConstrainedLayoutReference =
1076             createRefFor(ids.getOrElse(9) { nextId() })
1077 
1078         operator fun component11(): ConstrainedLayoutReference =
1079             createRefFor(ids.getOrElse(10) { nextId() })
1080 
1081         operator fun component12(): ConstrainedLayoutReference =
1082             createRefFor(ids.getOrElse(11) { nextId() })
1083 
1084         operator fun component13(): ConstrainedLayoutReference =
1085             createRefFor(ids.getOrElse(12) { nextId() })
1086 
1087         operator fun component14(): ConstrainedLayoutReference =
1088             createRefFor(ids.getOrElse(13) { nextId() })
1089 
1090         operator fun component15(): ConstrainedLayoutReference =
1091             createRefFor(ids.getOrElse(14) { nextId() })
1092 
1093         operator fun component16(): ConstrainedLayoutReference =
1094             createRefFor(ids.getOrElse(15) { nextId() })
1095     }
1096 }
1097 
1098 /** Parent data provided by `Modifier.constrainAs`. */
1099 @Stable
1100 private class ConstraintLayoutParentData(
1101     val ref: ConstrainedLayoutReference,
1102     val constrain: ConstrainScope.() -> Unit
1103 ) : LayoutIdParentData {
1104     override val layoutId: Any = ref.id
1105 
equalsnull1106     override fun equals(other: Any?) =
1107         other is ConstraintLayoutParentData &&
1108             ref.id == other.ref.id &&
1109             constrain === other.constrain
1110 
1111     override fun hashCode() = ref.id.hashCode() * 31 + constrain.hashCode()
1112 }
1113 
1114 /**
1115  * Convenience for creating ids corresponding to layout references that cannot be referred to from
1116  * the outside of the scope (e.g. barriers, layout references in the modifier-based API, etc.).
1117  */
1118 internal fun createId() = object : Any() {}
1119 
1120 /**
1121  * Represents a dimension that can be assigned to the width or height of a [ConstraintLayout]
1122  * [child][ConstrainedLayoutReference].
1123  */
1124 // TODO(popam, b/157781841): It is unfortunate that this interface is top level in
1125 // `foundation-layout`. This will be ok if we move constraint layout to its own module or at
1126 // least subpackage.
1127 interface Dimension {
1128     /** A [Dimension] that can be assigned both min and max bounds. */
1129     interface Coercible : Dimension
1130 
1131     /** A [Dimension] that can be assigned a min bound. */
1132     interface MinCoercible : Dimension
1133 
1134     /** A [Dimension] that can be assigned a max bound. */
1135     interface MaxCoercible : Dimension
1136 
1137     companion object {
1138         /**
1139          * Links should be specified from both sides corresponding to this dimension, in order for
1140          * this to work.
1141          *
1142          * Creates a [Dimension] such that if the constraints allow it, will have the size given by
1143          * [dp], otherwise will take the size remaining within the constraints.
1144          *
1145          * This is effectively a shorthand for [fillToConstraints] with a max value.
1146          *
1147          * To make the value fixed (respected regardless the [ConstraintSet]), [value] should be
1148          * used instead.
1149          */
preferredValuenull1150         fun preferredValue(dp: Dp): Dimension.MinCoercible =
1151             DimensionDescription("spread").apply { max.update(dp) }
1152 
1153         /**
1154          * Creates a [Dimension] representing a fixed dp size. The size will not change according to
1155          * the constraints in the [ConstraintSet].
1156          */
valuenull1157         fun value(dp: Dp): Dimension = DimensionDescription(dp)
1158 
1159         /**
1160          * Sets the dimensions to be defined as a ratio of the width and height. The assigned
1161          * dimension will be considered to also be [fillToConstraints].
1162          *
1163          * The string to define a ratio is defined by the format: 'W:H'. Where H is the height as a
1164          * proportion of W (the width).
1165          *
1166          * Eg: width = Dimension.ratio('1:2') sets the width to be half as large as the height.
1167          *
1168          * Note that only one dimension should be defined as a ratio.
1169          */
1170         fun ratio(ratio: String): Dimension = DimensionDescription(ratio)
1171 
1172         /**
1173          * Links should be specified from both sides corresponding to this dimension, in order for
1174          * this to work.
1175          *
1176          * A [Dimension] with suggested wrap content behavior. The wrap content size will be
1177          * respected unless the constraints in the [ConstraintSet] do not allow it. To make the
1178          * value fixed (respected regardless the [ConstraintSet]), [wrapContent] should be used
1179          * instead.
1180          */
1181         val preferredWrapContent: Dimension.Coercible
1182             get() = DimensionDescription("preferWrap")
1183 
1184         /**
1185          * A fixed [Dimension] with wrap content behavior. The size will not change according to the
1186          * constraints in the [ConstraintSet].
1187          */
1188         val wrapContent: Dimension
1189             get() = DimensionDescription("wrap")
1190 
1191         /**
1192          * A fixed [Dimension] that matches the dimensions of the root ConstraintLayout. The size
1193          * will not change accoring to the constraints in the [ConstraintSet].
1194          */
1195         val matchParent: Dimension
1196             get() = DimensionDescription("parent")
1197 
1198         /**
1199          * Links should be specified from both sides corresponding to this dimension, in order for
1200          * this to work.
1201          *
1202          * A [Dimension] that spreads to match constraints.
1203          */
1204         val fillToConstraints: Dimension.Coercible
1205             get() = DimensionDescription("spread")
1206 
1207         /**
1208          * A [Dimension] that is a percent of the parent in the corresponding direction.
1209          *
1210          * Where 1f is 100% and 0f is 0%.
1211          */
1212         fun percent(percent: Float): Dimension = DimensionDescription("${percent * 100f}%")
1213     }
1214 }
1215 
1216 /** Sets the lower bound of the current [Dimension] to be the wrap content size of the child. */
1217 val Dimension.Coercible.atLeastWrapContent: Dimension.MaxCoercible
1218     get() = (this as DimensionDescription).also { it.min.update("wrap") }
1219 
1220 /** Sets the lower bound of the current [Dimension] to a fixed [dp] value. */
atLeastnull1221 fun Dimension.Coercible.atLeast(dp: Dp): Dimension.MaxCoercible =
1222     (this as DimensionDescription).also { it.min.update(dp) }
1223 
1224 /** Sets the upper bound of the current [Dimension] to a fixed [dp] value. */
Dimensionnull1225 fun Dimension.Coercible.atMost(dp: Dp): Dimension.MinCoercible =
1226     (this as DimensionDescription).also { it.max.update(dp) }
1227 
1228 /** Sets the upper bound of the current [Dimension] to be the wrap content size of the child. */
1229 val Dimension.Coercible.atMostWrapContent: Dimension.MinCoercible
<lambda>null1230     get() = (this as DimensionDescription).also { it.max.update("wrap") }
1231 
1232 /** Sets the lower bound of the current [Dimension] to a fixed [dp] value. */
1233 @Deprecated(
1234     message = "Unintended method name, use atLeast(dp) instead",
1235     replaceWith = ReplaceWith("this.atLeast(dp)", "androidx.constraintlayout.compose.atLeast")
1236 )
atLeastWrapContentnull1237 fun Dimension.MinCoercible.atLeastWrapContent(dp: Dp): Dimension =
1238     (this as DimensionDescription).also { it.min.update(dp) }
1239 
1240 /** Sets the lower bound of the current [Dimension] to a fixed [dp] value. */
atLeastnull1241 fun Dimension.MinCoercible.atLeast(dp: Dp): Dimension =
1242     (this as DimensionDescription).also { it.min.update(dp) }
1243 
1244 /** Sets the lower bound of the current [Dimension] to be the wrap content size of the child. */
1245 val Dimension.MinCoercible.atLeastWrapContent: Dimension
<lambda>null1246     get() = (this as DimensionDescription).also { it.min.update("wrap") }
1247 
1248 /** Sets the upper bound of the current [Dimension] to a fixed [dp] value. */
Dimensionnull1249 fun Dimension.MaxCoercible.atMost(dp: Dp): Dimension =
1250     (this as DimensionDescription).also { it.max.update(dp) }
1251 
1252 /** Sets the upper bound of the current [Dimension] to be the [WRAP_DIMENSION] size of the child. */
1253 val Dimension.MaxCoercible.atMostWrapContent: Dimension
<lambda>null1254     get() = (this as DimensionDescription).also { it.max.update("wrap") }
1255 
1256 /**
1257  * Describes a sizing behavior that can be applied to the width or height of a [ConstraintLayout]
1258  * child. The content of this class should not be instantiated directly; helpers available in the
1259  * [Dimension]'s companion object should be used.
1260  */
1261 internal class DimensionDescription private constructor(value: Dp?, valueSymbol: String?) :
1262     Dimension.Coercible, Dimension.MinCoercible, Dimension.MaxCoercible, Dimension {
1263     constructor(value: Dp) : this(value, null)
1264 
1265     constructor(valueSymbol: String) : this(null, valueSymbol)
1266 
1267     private val valueSymbol = DimensionSymbol(value, valueSymbol, "base")
1268     internal val min = DimensionSymbol(null, null, "min")
1269     internal val max = DimensionSymbol(null, null, "max")
1270 
1271     /**
1272      * Returns the [DimensionDescription] as a [CLElement].
1273      *
1274      * The specific implementation of the element depends on the properties. If only the base value
1275      * is provided, the resulting element will be either [CLString] or [CLNumber], but, if either
1276      * the [max] or [min] were defined, it'll return a [CLObject] with the defined properties.
1277      */
asCLElementnull1278     internal fun asCLElement(): CLElement =
1279         if (min.isUndefined() && max.isUndefined()) {
1280             valueSymbol.asCLElement()
1281         } else {
<lambda>null1282             CLObject(charArrayOf()).apply {
1283                 if (!min.isUndefined()) {
1284                     put("min", min.asCLElement())
1285                 }
1286                 if (!max.isUndefined()) {
1287                     put("max", max.asCLElement())
1288                 }
1289                 put("value", valueSymbol.asCLElement())
1290             }
1291         }
1292 }
1293 
1294 /**
1295  * Dimension that may be represented by either a fixed [Dp] value or a symbol of a specific behavior
1296  * (such as "wrap", "spread", "parent", etc).
1297  *
1298  * [asCLElement] may be used to parse the symbol into it's corresponding [CLElement], depending if
1299  * the dimension is represented by a value ([CLNumber]) or a symbol ([CLString]).
1300  */
1301 internal class DimensionSymbol(
1302     private var value: Dp?,
1303     private var symbol: String?,
1304     private val debugName: String
1305 ) {
updatenull1306     fun update(dp: Dp) {
1307         value = dp
1308         symbol = null
1309     }
1310 
updatenull1311     fun update(symbol: String) {
1312         value = null
1313         this.symbol = symbol
1314     }
1315 
isUndefinednull1316     fun isUndefined() = value == null && symbol == null
1317 
1318     fun asCLElement(): CLElement {
1319         value?.let {
1320             return CLNumber(it.value)
1321         }
1322         symbol?.let {
1323             return CLString.from(it)
1324         }
1325         // No valid element to return, default to wrapContent
1326         Log.e("CCL", "DimensionDescription: Null value & symbol for $debugName. Using WrapContent.")
1327         return CLString.from("wrap")
1328     }
1329 }
1330 
1331 /**
1332  * Parses [content] into a [ConstraintSet] and sets the variables defined in the `Variables` block
1333  * with the values of [overrideVariables].
1334  *
1335  * Eg:
1336  *
1337  * For `Variables: { margin: { from: 'initialMargin', step: 10 } }`
1338  *
1339  * overrideVariables = `"{ 'initialMargin' = 50 }"`
1340  *
1341  * Will create a ConstraintSet where `initialMargin` is 50.
1342  */
1343 @SuppressLint("ComposableNaming")
1344 @Composable
ConstraintSetnull1345 fun ConstraintSet(
1346     @Language("json5") content: String,
1347     @Language("json5") overrideVariables: String? = null
1348 ): ConstraintSet {
1349     val constraintset =
1350         remember(content, overrideVariables) { JSONConstraintSet(content, overrideVariables) }
1351     return constraintset
1352 }
1353 
1354 /** Handles update back to the composable */
1355 @PublishedApi
1356 internal abstract class EditableJSONLayout(@Language("json5") content: String) :
1357     LayoutInformationReceiver {
1358     private var forcedWidth: Int = Int.MIN_VALUE
1359     private var forcedHeight: Int = Int.MIN_VALUE
1360     private var forcedDrawDebug: MotionLayoutDebugFlags = MotionLayoutDebugFlags.UNKNOWN
1361     private var updateFlag: MutableState<Long>? = null
1362     private var layoutInformationMode: LayoutInfoFlags = LayoutInfoFlags.NONE
1363     private var layoutInformation = ""
1364     private var last = System.nanoTime()
1365     private var debugName: String? = null
1366 
1367     private var currentContent = content
1368 
initializationnull1369     protected fun initialization() {
1370         try {
1371             onNewContent(currentContent)
1372             if (debugName != null) {
1373                 val callback =
1374                     object : RegistryCallback {
1375                         override fun onNewMotionScene(content: String?) {
1376                             if (content == null) {
1377                                 return
1378                             }
1379                             onNewContent(content)
1380                         }
1381 
1382                         override fun onProgress(progress: Float) {
1383                             onNewProgress(progress)
1384                         }
1385 
1386                         override fun onDimensions(width: Int, height: Int) {
1387                             onNewDimensions(width, height)
1388                         }
1389 
1390                         override fun currentMotionScene(): String {
1391                             return currentContent
1392                         }
1393 
1394                         override fun currentLayoutInformation(): String {
1395                             return layoutInformation
1396                         }
1397 
1398                         override fun setLayoutInformationMode(mode: Int) {
1399                             onLayoutInformation(mode)
1400                         }
1401 
1402                         override fun getLastModified(): Long {
1403                             return last
1404                         }
1405 
1406                         override fun setDrawDebug(debugMode: Int) {
1407                             onDrawDebug(debugMode)
1408                         }
1409                     }
1410                 val registry = Registry.getInstance()
1411                 registry.register(debugName, callback)
1412             }
1413         } catch (_: CLParsingException) {}
1414     }
1415 
1416     // region Accessors
setUpdateFlagnull1417     override fun setUpdateFlag(needsUpdate: MutableState<Long>) {
1418         updateFlag = needsUpdate
1419     }
1420 
signalUpdatenull1421     protected fun signalUpdate() {
1422         if (updateFlag != null) {
1423             updateFlag!!.value = updateFlag!!.value + 1
1424         }
1425     }
1426 
setCurrentContentnull1427     fun setCurrentContent(content: String) {
1428         onNewContent(content)
1429     }
1430 
getCurrentContentnull1431     fun getCurrentContent(): String {
1432         return currentContent
1433     }
1434 
setDebugNamenull1435     fun setDebugName(name: String?) {
1436         debugName = name
1437     }
1438 
getDebugNamenull1439     fun getDebugName(): String? {
1440         return debugName
1441     }
1442 
getForcedDrawDebugnull1443     override fun getForcedDrawDebug(): MotionLayoutDebugFlags {
1444         return forcedDrawDebug
1445     }
1446 
getForcedWidthnull1447     override fun getForcedWidth(): Int {
1448         return forcedWidth
1449     }
1450 
getForcedHeightnull1451     override fun getForcedHeight(): Int {
1452         return forcedHeight
1453     }
1454 
setLayoutInformationnull1455     override fun setLayoutInformation(information: String) {
1456         last = System.nanoTime()
1457         layoutInformation = information
1458     }
1459 
getLayoutInformationnull1460     fun getLayoutInformation(): String {
1461         return layoutInformation
1462     }
1463 
getLayoutInformationModenull1464     override fun getLayoutInformationMode(): LayoutInfoFlags {
1465         return layoutInformationMode
1466     }
1467 
1468     // endregion
1469 
1470     // region on update methods
onNewContentnull1471     protected open fun onNewContent(content: String) {
1472         currentContent = content
1473         try {
1474             val json = CLParser.parse(currentContent)
1475             if (json is CLObject) {
1476                 val firstTime = debugName == null
1477                 if (firstTime) {
1478                     val debug = json.getObjectOrNull("Header")
1479                     if (debug != null) {
1480                         debugName = debug.getStringOrNull("exportAs")
1481                         layoutInformationMode = LayoutInfoFlags.BOUNDS
1482                     }
1483                 }
1484                 if (!firstTime) {
1485                     signalUpdate()
1486                 }
1487             }
1488         } catch (e: CLParsingException) {
1489             // nothing (content might be invalid, sent by live edit)
1490         } catch (e: Exception) {
1491             // nothing (content might be invalid, sent by live edit)
1492         }
1493     }
1494 
onNewDimensionsnull1495     fun onNewDimensions(width: Int, height: Int) {
1496         forcedWidth = width
1497         forcedHeight = height
1498         signalUpdate()
1499     }
1500 
onLayoutInformationnull1501     protected fun onLayoutInformation(mode: Int) {
1502         when (mode) {
1503             LayoutInfoFlags.NONE.ordinal -> layoutInformationMode = LayoutInfoFlags.NONE
1504             LayoutInfoFlags.BOUNDS.ordinal -> layoutInformationMode = LayoutInfoFlags.BOUNDS
1505         }
1506         signalUpdate()
1507     }
1508 
onDrawDebugnull1509     protected fun onDrawDebug(debugMode: Int) {
1510         forcedDrawDebug =
1511             when (debugMode) {
1512                 MotionLayoutDebugFlags.UNKNOWN.ordinal -> MotionLayoutDebugFlags.UNKNOWN
1513                 MotionLayoutDebugFlags.NONE.ordinal -> MotionLayoutDebugFlags.NONE
1514                 MotionLayoutDebugFlags.SHOW_ALL.ordinal -> MotionLayoutDebugFlags.SHOW_ALL
1515                 -1 -> MotionLayoutDebugFlags.UNKNOWN
1516                 else -> MotionLayoutDebugFlags.UNKNOWN
1517             }
1518         signalUpdate()
1519     }
1520     // endregion
1521 }
1522 
1523 internal data class DesignElement(
1524     var id: String,
1525     var type: String,
1526     var params: HashMap<String, String>
1527 )
1528 
1529 /**
1530  * Parses the given JSON5 into a [ConstraintSet].
1531  *
1532  * See the official
1533  * [GitHub Wiki](https://github.com/androidx/constraintlayout/wiki/ConstraintSet-JSON5-syntax) to
1534  * learn the syntax.
1535  */
ConstraintSetnull1536 fun ConstraintSet(@Language(value = "json5") jsonContent: String): ConstraintSet =
1537     JSONConstraintSet(content = jsonContent)
1538 
1539 /**
1540  * Creates a [ConstraintSet] from a [jsonContent] string that extends the changes applied by
1541  * [extendConstraintSet].
1542  */
1543 fun ConstraintSet(
1544     extendConstraintSet: ConstraintSet,
1545     @Language(value = "json5") jsonContent: String
1546 ): ConstraintSet = JSONConstraintSet(content = jsonContent, extendFrom = extendConstraintSet)
1547 
1548 /**
1549  * Creates a [ConstraintSet] with the constraints defined in the [description] block.
1550  *
1551  * See [ConstraintSet] to learn how to define constraints.
1552  */
1553 fun ConstraintSet(description: ConstraintSetScope.() -> Unit): ConstraintSet =
1554     DslConstraintSet(description)
1555 
1556 /**
1557  * Creates a [ConstraintSet] that extends the changes applied by [extendConstraintSet].
1558  *
1559  * See [ConstraintSet] to learn how to define constraints.
1560  */
1561 fun ConstraintSet(
1562     extendConstraintSet: ConstraintSet,
1563     description: ConstraintSetScope.() -> Unit
1564 ): ConstraintSet = DslConstraintSet(description, extendConstraintSet)
1565 
1566 /** The state of the [ConstraintLayout] solver. */
1567 class State(val density: Density) : SolverState() {
1568     var rootIncomingConstraints: Constraints = Constraints()
1569     @Deprecated("Use #isLtr instead") var layoutDirection: LayoutDirection = LayoutDirection.Ltr
1570 
1571     init {
1572         setDpToPixel { dp -> density.density * dp }
1573     }
1574 
1575     override fun convertDimension(value: Any?): Int {
1576         return if (value is Dp) {
1577             with(density) { value.roundToPx() }
1578         } else {
1579             super.convertDimension(value)
1580         }
1581     }
1582 
1583     internal fun getKeyId(helperWidget: HelperWidget): Any? {
1584         return mHelperReferences.entries.firstOrNull { it.value.helperWidget == helperWidget }?.key
1585     }
1586 }
1587 
1588 interface LayoutInformationReceiver {
setLayoutInformationnull1589     fun setLayoutInformation(information: String)
1590 
1591     fun getLayoutInformationMode(): LayoutInfoFlags
1592 
1593     fun getForcedWidth(): Int
1594 
1595     fun getForcedHeight(): Int
1596 
1597     fun setUpdateFlag(needsUpdate: MutableState<Long>)
1598 
1599     fun getForcedDrawDebug(): MotionLayoutDebugFlags
1600 
1601     /** reset the force progress flag */
1602     fun resetForcedProgress()
1603 
1604     /** Get the progress of the force progress */
1605     fun getForcedProgress(): Float
1606 
1607     fun onNewProgress(progress: Float)
1608 }
1609 
1610 @Deprecated(
1611     message = "Replace with Measurer2 instead for proper Measure/Layout handling.",
1612     replaceWith = ReplaceWith("Measurer2")
1613 )
1614 @PublishedApi
1615 internal open class Measurer(
1616     density: Density // TODO: Change to a variable since density may change
1617 ) : BasicMeasure.Measurer, DesignInfoProvider {
1618     private var computedLayoutResult: String = ""
1619     protected var layoutInformationReceiver: LayoutInformationReceiver? = null
1620     protected val root = ConstraintWidgetContainer(0, 0).also { it.measurer = this }
1621     protected val placeables = mutableMapOf<Measurable, Placeable>()
1622     private val lastMeasures = mutableMapOf<String, Array<Int>>()
1623     protected val frameCache = mutableMapOf<Measurable, WidgetFrame>()
1624 
1625     protected val state = State(density)
1626 
1627     private val widthConstraintsHolder = IntArray(2)
1628     private val heightConstraintsHolder = IntArray(2)
1629 
1630     var forcedScaleFactor = Float.NaN
1631     val layoutCurrentWidth: Int
1632         get() = root.width
1633 
1634     val layoutCurrentHeight: Int
1635         get() = root.height
1636 
1637     /**
1638      * Method called by Compose tooling. Returns a JSON string that represents the Constraints
1639      * defined for this ConstraintLayout Composable.
1640      */
1641     override fun getDesignInfo(startX: Int, startY: Int, args: String) =
1642         parseConstraintsToJson(root, state, startX, startY, args)
1643 
1644     /** Measure the given [constraintWidget] with the specs defined by [measure]. */
1645     override fun measure(constraintWidget: ConstraintWidget, measure: BasicMeasure.Measure) {
1646         val widgetId = constraintWidget.stringId
1647 
1648         if (DEBUG) {
1649             Log.d("CCL", "Measuring $widgetId with: " + constraintWidget.toDebugString() + "\n")
1650         }
1651 
1652         val measurableLastMeasures = lastMeasures[widgetId]
1653         obtainConstraints(
1654             measure.horizontalBehavior,
1655             measure.horizontalDimension,
1656             constraintWidget.mMatchConstraintDefaultWidth,
1657             measure.measureStrategy,
1658             (measurableLastMeasures?.get(1) ?: 0) == constraintWidget.height,
1659             constraintWidget.isResolvedHorizontally,
1660             state.rootIncomingConstraints.maxWidth,
1661             widthConstraintsHolder
1662         )
1663         obtainConstraints(
1664             measure.verticalBehavior,
1665             measure.verticalDimension,
1666             constraintWidget.mMatchConstraintDefaultHeight,
1667             measure.measureStrategy,
1668             (measurableLastMeasures?.get(0) ?: 0) == constraintWidget.width,
1669             constraintWidget.isResolvedVertically,
1670             state.rootIncomingConstraints.maxHeight,
1671             heightConstraintsHolder
1672         )
1673 
1674         var constraints =
1675             Constraints(
1676                 widthConstraintsHolder[0],
1677                 widthConstraintsHolder[1],
1678                 heightConstraintsHolder[0],
1679                 heightConstraintsHolder[1]
1680             )
1681 
1682         if (
1683             (measure.measureStrategy == TRY_GIVEN_DIMENSIONS ||
1684                 measure.measureStrategy == USE_GIVEN_DIMENSIONS) ||
1685                 !(measure.horizontalBehavior == MATCH_CONSTRAINT &&
1686                     constraintWidget.mMatchConstraintDefaultWidth == MATCH_CONSTRAINT_SPREAD &&
1687                     measure.verticalBehavior == MATCH_CONSTRAINT &&
1688                     constraintWidget.mMatchConstraintDefaultHeight == MATCH_CONSTRAINT_SPREAD)
1689         ) {
1690             if (DEBUG) {
1691                 Log.d("CCL", "Measuring $widgetId with $constraints")
1692             }
1693             val result = measureWidget(constraintWidget, constraints)
1694             constraintWidget.isMeasureRequested = false
1695             if (DEBUG) {
1696                 Log.d("CCL", "$widgetId is size ${result.first} ${result.second}")
1697             }
1698 
1699             val coercedWidth =
1700                 result.first.coerceIn(
1701                     constraintWidget.mMatchConstraintMinWidth.takeIf { it > 0 },
1702                     constraintWidget.mMatchConstraintMaxWidth.takeIf { it > 0 }
1703                 )
1704             val coercedHeight =
1705                 result.second.coerceIn(
1706                     constraintWidget.mMatchConstraintMinHeight.takeIf { it > 0 },
1707                     constraintWidget.mMatchConstraintMaxHeight.takeIf { it > 0 }
1708                 )
1709 
1710             var remeasure = false
1711             if (coercedWidth != result.first) {
1712                 constraints =
1713                     Constraints(
1714                         minWidth = coercedWidth,
1715                         minHeight = constraints.minHeight,
1716                         maxWidth = coercedWidth,
1717                         maxHeight = constraints.maxHeight
1718                     )
1719                 remeasure = true
1720             }
1721             if (coercedHeight != result.second) {
1722                 constraints =
1723                     Constraints(
1724                         minWidth = constraints.minWidth,
1725                         minHeight = coercedHeight,
1726                         maxWidth = constraints.maxWidth,
1727                         maxHeight = coercedHeight
1728                     )
1729                 remeasure = true
1730             }
1731             if (remeasure) {
1732                 if (DEBUG) {
1733                     Log.d("CCL", "Remeasuring coerced $widgetId with $constraints")
1734                 }
1735                 measureWidget(constraintWidget, constraints)
1736                 constraintWidget.isMeasureRequested = false
1737             }
1738         }
1739 
1740         val currentPlaceable = placeables[constraintWidget.companionWidget]
1741         measure.measuredWidth = currentPlaceable?.width ?: constraintWidget.width
1742         measure.measuredHeight = currentPlaceable?.height ?: constraintWidget.height
1743         val baseline =
1744             if (currentPlaceable != null && state.isBaselineNeeded(constraintWidget)) {
1745                 currentPlaceable[FirstBaseline]
1746             } else {
1747                 AlignmentLine.Unspecified
1748             }
1749         measure.measuredHasBaseline = baseline != AlignmentLine.Unspecified
1750         measure.measuredBaseline = baseline
1751         lastMeasures
1752             .getOrPut(widgetId) { arrayOf(0, 0, AlignmentLine.Unspecified) }
1753             .copyFrom(measure)
1754 
1755         measure.measuredNeedsSolverPass =
1756             measure.measuredWidth != measure.horizontalDimension ||
1757                 measure.measuredHeight != measure.verticalDimension
1758     }
1759 
1760     fun addLayoutInformationReceiver(layoutReceiver: LayoutInformationReceiver?) {
1761         layoutInformationReceiver = layoutReceiver
1762         layoutInformationReceiver?.setLayoutInformation(computedLayoutResult)
1763     }
1764 
1765     open fun computeLayoutResult() {
1766         val json = StringBuilder()
1767         json.append("{ ")
1768         json.append("  root: {")
1769         json.append("interpolated: { left:  0,")
1770         json.append("  top:  0,")
1771         json.append("  right:   ${root.width} ,")
1772         json.append("  bottom:  ${root.height} ,")
1773         json.append(" } }")
1774 
1775         @Suppress("ListIterator")
1776         for (child in root.children) {
1777             val measurable = child.companionWidget
1778             if (measurable !is Measurable) {
1779                 if (child is Guideline) {
1780                     json.append(" ${child.stringId}: {")
1781                     if (child.orientation == ConstraintWidget.HORIZONTAL) {
1782                         json.append(" type: 'hGuideline', ")
1783                     } else {
1784                         json.append(" type: 'vGuideline', ")
1785                     }
1786                     json.append(" interpolated: ")
1787                     json.append(
1788                         " { left: ${child.x}, top: ${child.y}, " +
1789                             "right: ${child.x + child.width}, " +
1790                             "bottom: ${child.y + child.height} }"
1791                     )
1792                     json.append("}, ")
1793                 }
1794                 continue
1795             }
1796             if (child.stringId == null) {
1797                 val id = measurable.layoutId ?: measurable.constraintLayoutId
1798                 child.stringId = id?.toString()
1799             }
1800             val frame = frameCache[measurable]?.widget?.frame
1801             if (frame == null) {
1802                 continue
1803             }
1804             json.append(" ${child.stringId}: {")
1805             json.append(" interpolated : ")
1806             frame.serialize(json, true)
1807             json.append("}, ")
1808         }
1809         json.append(" }")
1810         computedLayoutResult = json.toString()
1811         layoutInformationReceiver?.setLayoutInformation(computedLayoutResult)
1812     }
1813 
1814     /**
1815      * Calculates the [Constraints] in one direction that should be used to measure a child, based
1816      * on the solver measure request. Returns `true` if the constraints correspond to a wrap content
1817      * measurement.
1818      */
1819     private fun obtainConstraints(
1820         dimensionBehaviour: ConstraintWidget.DimensionBehaviour,
1821         dimension: Int,
1822         matchConstraintDefaultDimension: Int,
1823         measureStrategy: Int,
1824         otherDimensionResolved: Boolean,
1825         currentDimensionResolved: Boolean,
1826         rootMaxConstraint: Int,
1827         outConstraints: IntArray
1828     ): Boolean =
1829         when (dimensionBehaviour) {
1830             FIXED -> {
1831                 outConstraints[0] = dimension
1832                 outConstraints[1] = dimension
1833                 false
1834             }
1835             WRAP_CONTENT -> {
1836                 outConstraints[0] = 0
1837                 outConstraints[1] = rootMaxConstraint
1838                 true
1839             }
1840             MATCH_CONSTRAINT -> {
1841                 if (DEBUG) {
1842                     Log.d("CCL", "Measure strategy $measureStrategy")
1843                     Log.d("CCL", "DW $matchConstraintDefaultDimension")
1844                     Log.d("CCL", "ODR $otherDimensionResolved")
1845                     Log.d("CCL", "IRH $currentDimensionResolved")
1846                 }
1847                 val useDimension =
1848                     currentDimensionResolved ||
1849                         (measureStrategy == TRY_GIVEN_DIMENSIONS ||
1850                             measureStrategy == USE_GIVEN_DIMENSIONS) &&
1851                             (measureStrategy == USE_GIVEN_DIMENSIONS ||
1852                                 matchConstraintDefaultDimension != MATCH_CONSTRAINT_WRAP ||
1853                                 otherDimensionResolved)
1854                 if (DEBUG) {
1855                     Log.d("CCL", "UD $useDimension")
1856                 }
1857                 outConstraints[0] = if (useDimension) dimension else 0
1858                 outConstraints[1] = if (useDimension) dimension else rootMaxConstraint
1859                 !useDimension
1860             }
1861             MATCH_PARENT -> {
1862                 outConstraints[0] = rootMaxConstraint
1863                 outConstraints[1] = rootMaxConstraint
1864                 false
1865             }
1866         }
1867 
1868     private fun Array<Int>.copyFrom(measure: BasicMeasure.Measure) {
1869         this[0] = measure.measuredWidth
1870         this[1] = measure.measuredHeight
1871         this[2] = measure.measuredBaseline
1872     }
1873 
1874     fun performMeasure(
1875         constraints: Constraints,
1876         layoutDirection: LayoutDirection,
1877         constraintSet: ConstraintSet,
1878         measurables: List<Measurable>,
1879         optimizationLevel: Int
1880     ): IntSize {
1881         if (measurables.isEmpty()) {
1882             // TODO(b/335524398): Behavior with zero children is unexpected. It's also inconsistent
1883             //      with ViewGroup, so this is a workaround to handle those cases the way it seems
1884             //      right for this implementation.
1885             return IntSize(constraints.minWidth, constraints.minHeight)
1886         }
1887 
1888         // Define the size of the ConstraintLayout.
1889         state.width(
1890             if (constraints.hasFixedWidth) {
1891                 SolverDimension.createFixed(constraints.maxWidth)
1892             } else {
1893                 SolverDimension.createWrap().min(constraints.minWidth)
1894             }
1895         )
1896         state.height(
1897             if (constraints.hasFixedHeight) {
1898                 SolverDimension.createFixed(constraints.maxHeight)
1899             } else {
1900                 SolverDimension.createWrap().min(constraints.minHeight)
1901             }
1902         )
1903         state.mParent.width.apply(state, root, ConstraintWidget.HORIZONTAL)
1904         state.mParent.height.apply(state, root, ConstraintWidget.VERTICAL)
1905         // Build constraint set and apply it to the state.
1906         state.rootIncomingConstraints = constraints
1907         state.isRtl = layoutDirection == LayoutDirection.Rtl
1908         resetMeasureState()
1909         if (constraintSet.isDirty(measurables)) {
1910             state.reset()
1911             constraintSet.applyTo(state, measurables)
1912             buildMapping(state, measurables)
1913             state.apply(root)
1914         } else {
1915             buildMapping(state, measurables)
1916         }
1917 
1918         applyRootSize(constraints)
1919         root.updateHierarchy()
1920 
1921         if (DEBUG) {
1922             root.debugName = "ConstraintLayout"
1923             root.children.fastForEach { child ->
1924                 child.debugName =
1925                     (child.companionWidget as? Measurable)?.layoutId?.toString() ?: "NOTAG"
1926             }
1927             Log.d("CCL", "ConstraintLayout is asked to measure with $constraints")
1928             Log.d("CCL", root.toDebugString())
1929             root.children.fastForEach { child -> Log.d("CCL", child.toDebugString()) }
1930         }
1931 
1932         // No need to set sizes and size modes as we passed them to the state above.
1933         root.optimizationLevel = optimizationLevel
1934         root.measure(root.optimizationLevel, 0, 0, 0, 0, 0, 0, 0, 0)
1935 
1936         if (DEBUG) {
1937             Log.d("CCL", "ConstraintLayout is at the end ${root.width} ${root.height}")
1938         }
1939         return IntSize(root.width, root.height)
1940     }
1941 
1942     internal fun resetMeasureState() {
1943         placeables.clear()
1944         lastMeasures.clear()
1945         frameCache.clear()
1946     }
1947 
1948     protected fun applyRootSize(constraints: Constraints) {
1949         root.width = constraints.maxWidth
1950         root.height = constraints.maxHeight
1951         forcedScaleFactor = Float.NaN
1952         if (
1953             layoutInformationReceiver != null &&
1954                 layoutInformationReceiver?.getForcedWidth() != Int.MIN_VALUE
1955         ) {
1956             val forcedWidth = layoutInformationReceiver!!.getForcedWidth()
1957             if (forcedWidth > root.width) {
1958                 val scale = root.width / forcedWidth.toFloat()
1959                 forcedScaleFactor = scale
1960             } else {
1961                 forcedScaleFactor = 1f
1962             }
1963             root.width = forcedWidth
1964         }
1965         if (
1966             layoutInformationReceiver != null &&
1967                 layoutInformationReceiver?.getForcedHeight() != Int.MIN_VALUE
1968         ) {
1969             val forcedHeight = layoutInformationReceiver!!.getForcedHeight()
1970             var scaleFactor = 1f
1971             if (forcedScaleFactor.isNaN()) {
1972                 forcedScaleFactor = 1f
1973             }
1974             if (forcedHeight > root.height) {
1975                 scaleFactor = root.height / forcedHeight.toFloat()
1976             }
1977             if (scaleFactor < forcedScaleFactor) {
1978                 forcedScaleFactor = scaleFactor
1979             }
1980             root.height = forcedHeight
1981         }
1982     }
1983 
1984     fun Placeable.PlacementScope.performLayout(measurables: List<Measurable>) {
1985         if (frameCache.isEmpty()) {
1986             root.children.fastForEach { child ->
1987                 val measurable = child.companionWidget
1988                 if (measurable !is Measurable) return@fastForEach
1989                 val frame = WidgetFrame(child.frame.update())
1990                 frameCache[measurable] = frame
1991             }
1992         }
1993         measurables.fastForEach { measurable ->
1994             val matchedMeasurable: Measurable =
1995                 if (!frameCache.containsKey(measurable)) {
1996                     // TODO: Workaround for lookaheadLayout, the measurable is a different instance
1997                     frameCache.keys.firstOrNull {
1998                         it.layoutId != null && it.layoutId == measurable.layoutId
1999                     } ?: return@fastForEach
2000                 } else {
2001                     measurable
2002                 }
2003             val frame = frameCache[matchedMeasurable] ?: return
2004             val placeable = placeables[matchedMeasurable] ?: return
2005             if (!frameCache.containsKey(measurable)) {
2006                 // TODO: Workaround for lookaheadLayout, the measurable is a different instance and
2007                 //   the placeable should be a result of the given measurable
2008                 placeWithFrameTransform(
2009                     measurable.measure(Constraints.fixed(placeable.width, placeable.height)),
2010                     frame
2011                 )
2012             } else {
2013                 placeWithFrameTransform(placeable, frame)
2014             }
2015         }
2016         if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags.BOUNDS) {
2017             computeLayoutResult()
2018         }
2019     }
2020 
2021     override fun didMeasures() {}
2022 
2023     /**
2024      * Measure a [ConstraintWidget] with the given [constraints].
2025      *
2026      * Note that the [constraintWidget] could correspond to either a Composable or a Helper, which
2027      * need to be measured differently.
2028      *
2029      * Returns a [Pair] with the result of the measurement, the first and second values are the
2030      * measured width and height respectively.
2031      */
2032     private fun measureWidget(
2033         constraintWidget: ConstraintWidget,
2034         constraints: Constraints
2035     ): IntIntPair {
2036         val measurable = constraintWidget.companionWidget
2037         val widgetId = constraintWidget.stringId
2038         return when {
2039             constraintWidget is VirtualLayout -> {
2040                 // TODO: This step should really be performed within ConstraintWidgetContainer,
2041                 //  compose-ConstraintLayout should only have to measure Composables/Measurables
2042                 val widthMode =
2043                     when {
2044                         constraints.hasFixedWidth -> BasicMeasure.EXACTLY
2045                         constraints.hasBoundedWidth -> BasicMeasure.AT_MOST
2046                         else -> BasicMeasure.UNSPECIFIED
2047                     }
2048                 val heightMode =
2049                     when {
2050                         constraints.hasFixedHeight -> BasicMeasure.EXACTLY
2051                         constraints.hasBoundedHeight -> BasicMeasure.AT_MOST
2052                         else -> BasicMeasure.UNSPECIFIED
2053                     }
2054                 constraintWidget.measure(
2055                     widthMode,
2056                     constraints.maxWidth,
2057                     heightMode,
2058                     constraints.maxHeight
2059                 )
2060                 IntIntPair(constraintWidget.measuredWidth, constraintWidget.measuredHeight)
2061             }
2062             measurable is Measurable -> {
2063                 val result = measurable.measure(constraints).also { placeables[measurable] = it }
2064                 IntIntPair(result.width, result.height)
2065             }
2066             else -> {
2067                 Log.w("CCL", "Nothing to measure for widget: $widgetId")
2068                 IntIntPair(0, 0)
2069             }
2070         }
2071     }
2072 
2073     @Composable
2074     fun BoxScope.drawDebugBounds(forcedScaleFactor: Float) {
2075         Canvas(modifier = Modifier.matchParentSize()) { drawDebugBounds(forcedScaleFactor) }
2076     }
2077 
2078     fun DrawScope.drawDebugBounds(forcedScaleFactor: Float) {
2079         val w = layoutCurrentWidth * forcedScaleFactor
2080         val h = layoutCurrentHeight * forcedScaleFactor
2081         var dx = (size.width - w) / 2f
2082         var dy = (size.height - h) / 2f
2083         var color = Color.White
2084         drawLine(color, Offset(dx, dy), Offset(dx + w, dy))
2085         drawLine(color, Offset(dx + w, dy), Offset(dx + w, dy + h))
2086         drawLine(color, Offset(dx + w, dy + h), Offset(dx, dy + h))
2087         drawLine(color, Offset(dx, dy + h), Offset(dx, dy))
2088         dx += 1
2089         dy += 1
2090         color = Color.Black
2091         drawLine(color, Offset(dx, dy), Offset(dx + w, dy))
2092         drawLine(color, Offset(dx + w, dy), Offset(dx + w, dy + h))
2093         drawLine(color, Offset(dx + w, dy + h), Offset(dx, dy + h))
2094         drawLine(color, Offset(dx, dy + h), Offset(dx, dy))
2095     }
2096 
2097     private var designElements = arrayListOf<ConstraintSetParser.DesignElement>()
2098 
2099     private fun getColor(str: String?, defaultColor: Color = Color.Black): Color {
2100         if (str != null && str.startsWith('#')) {
2101             var str2 = str.substring(1)
2102             if (str2.length == 6) {
2103                 str2 = "FF$str2"
2104             }
2105             try {
2106                 return Color(java.lang.Long.parseLong(str2, 16).toInt())
2107             } catch (e: Exception) {
2108                 return defaultColor
2109             }
2110         }
2111         return defaultColor
2112     }
2113 
2114     private fun getTextStyle(params: HashMap<String, String>): TextStyle {
2115         val fontSizeString = params["size"]
2116         var fontSize = TextUnit.Unspecified
2117         if (fontSizeString != null) {
2118             fontSize = fontSizeString.toFloat().sp
2119         }
2120         var textColor = getColor(params["color"])
2121         return TextStyle(fontSize = fontSize, color = textColor)
2122     }
2123 
2124     @Composable
2125     fun createDesignElements() {
2126         designElements.fastForEach { element ->
2127             var id = element.id
2128             var function = DesignElements.map[element.type]
2129             if (function != null) {
2130                 function(id, element.params)
2131             } else {
2132                 when (element.type) {
2133                     "button" -> {
2134                         val text = element.params["text"] ?: "text"
2135                         val colorBackground =
2136                             getColor(element.params["backgroundColor"], Color.LightGray)
2137                         BasicText(
2138                             modifier =
2139                                 Modifier.layoutId(id)
2140                                     .clip(RoundedCornerShape(20))
2141                                     .background(colorBackground)
2142                                     .padding(8.dp),
2143                             text = text,
2144                             style = getTextStyle(element.params)
2145                         )
2146                     }
2147                     "box" -> {
2148                         val text = element.params["text"] ?: ""
2149                         val colorBackground =
2150                             getColor(element.params["backgroundColor"], Color.LightGray)
2151                         Box(modifier = Modifier.layoutId(id).background(colorBackground)) {
2152                             BasicText(
2153                                 modifier = Modifier.padding(8.dp),
2154                                 text = text,
2155                                 style = getTextStyle(element.params)
2156                             )
2157                         }
2158                     }
2159                     "text" -> {
2160                         val text = element.params["text"] ?: "text"
2161                         BasicText(
2162                             modifier = Modifier.layoutId(id),
2163                             text = text,
2164                             style = getTextStyle(element.params)
2165                         )
2166                     }
2167                     "textfield" -> {
2168                         val text = element.params["text"] ?: "text"
2169                         BasicTextField(
2170                             modifier = Modifier.layoutId(id),
2171                             value = text,
2172                             onValueChange = {}
2173                         )
2174                     }
2175                     "image" -> {
2176                         Image(
2177                             modifier = Modifier.layoutId(id),
2178                             painter = painterResource(id = android.R.drawable.ic_menu_gallery),
2179                             contentDescription = "Placeholder Image"
2180                         )
2181                     }
2182                 }
2183             }
2184         }
2185     }
2186 
2187     fun parseDesignElements(constraintSet: ConstraintSet) {
2188         if (constraintSet is JSONConstraintSet) {
2189             constraintSet.emitDesignElements(designElements)
2190         }
2191     }
2192 }
2193 
placeWithFrameTransformnull2194 internal fun Placeable.PlacementScope.placeWithFrameTransform(
2195     placeable: Placeable,
2196     frame: WidgetFrame,
2197     offset: IntOffset = IntOffset.Zero
2198 ) {
2199     if (frame.visibility == ConstraintWidget.GONE) {
2200         if (DEBUG) {
2201             Log.d("CCL", "Widget: ${frame.id} is Gone. Skipping placement.")
2202         }
2203         return
2204     }
2205     if (frame.isDefaultTransform) {
2206         val x = frame.left - offset.x
2207         val y = frame.top - offset.y
2208         placeable.place(IntOffset(x, y))
2209     } else {
2210         val layerBlock: GraphicsLayerScope.() -> Unit = {
2211             if (!frame.pivotX.isNaN() || !frame.pivotY.isNaN()) {
2212                 val pivotX = if (frame.pivotX.isNaN()) 0.5f else frame.pivotX
2213                 val pivotY = if (frame.pivotY.isNaN()) 0.5f else frame.pivotY
2214                 transformOrigin = TransformOrigin(pivotX, pivotY)
2215             }
2216             if (!frame.rotationX.isNaN()) {
2217                 rotationX = frame.rotationX
2218             }
2219             if (!frame.rotationY.isNaN()) {
2220                 rotationY = frame.rotationY
2221             }
2222             if (!frame.rotationZ.isNaN()) {
2223                 rotationZ = frame.rotationZ
2224             }
2225             if (!frame.translationX.isNaN()) {
2226                 translationX = frame.translationX
2227             }
2228             if (!frame.translationY.isNaN()) {
2229                 translationY = frame.translationY
2230             }
2231             if (!frame.translationZ.isNaN()) {
2232                 shadowElevation = frame.translationZ
2233             }
2234             if (!frame.scaleX.isNaN() || !frame.scaleY.isNaN()) {
2235                 scaleX = if (frame.scaleX.isNaN()) 1f else frame.scaleX
2236                 scaleY = if (frame.scaleY.isNaN()) 1f else frame.scaleY
2237             }
2238             if (!frame.alpha.isNaN()) {
2239                 alpha = frame.alpha
2240             }
2241         }
2242         val x = frame.left - offset.x
2243         val y = frame.top - offset.y
2244         val zIndex = if (frame.translationZ.isNaN()) 0f else frame.translationZ
2245         placeable.placeWithLayer(x = x, y = y, layerBlock = layerBlock, zIndex = zIndex)
2246     }
2247 }
2248 
2249 object DesignElements {
2250     var map = HashMap<String, @Composable (String, HashMap<String, String>) -> Unit>()
2251 
definenull2252     fun define(name: String, function: @Composable (String, HashMap<String, String>) -> Unit) {
2253         map[name] = function
2254     }
2255 }
2256 
2257 /**
2258  * Maps ID and Tag to each compose [Measurable] into [state].
2259  *
2260  * The ID could be provided from [androidx.compose.ui.layout.layoutId],
2261  * [ConstraintLayoutParentData.ref] or [ConstraintLayoutTagParentData.constraintLayoutId].
2262  *
2263  * The Tag is set from [ConstraintLayoutTagParentData.constraintLayoutTag].
2264  *
2265  * This should always be performed for every Measure call, since there's no guarantee that the
2266  * [Measurable]s will be the same instance, even if there's seemingly no changes. Should be called
2267  * before applying the [State] or, if there's no need to apply it, should be called before
2268  * measuring.
2269  */
buildMappingnull2270 internal fun buildMapping(state: State, measurables: List<Measurable>) {
2271     measurables.fastForEach { measurable ->
2272         val id = measurable.layoutId ?: measurable.constraintLayoutId ?: createId()
2273         // Map the id and the measurable, to be retrieved later during measurement.
2274         state.map(id.toString(), measurable)
2275         val tag = measurable.constraintLayoutTag
2276         if (tag != null && tag is String && id is String) {
2277             state.setTag(id, tag)
2278         }
2279     }
2280 }
2281 
2282 internal typealias SolverDimension = androidx.constraintlayout.core.state.Dimension
2283 
2284 internal typealias SolverState = androidx.constraintlayout.core.state.State
2285 
2286 private const val DEBUG = false
2287 
toDebugStringnull2288 internal fun ConstraintWidget.toDebugString() =
2289     "$debugName " +
2290         "width $width minWidth $minWidth maxWidth $maxWidth " +
2291         "height $height minHeight $minHeight maxHeight $maxHeight " +
2292         "HDB $horizontalDimensionBehaviour VDB $verticalDimensionBehaviour " +
2293         "MCW $mMatchConstraintDefaultWidth MCH $mMatchConstraintDefaultHeight " +
2294         "percentW $mMatchConstraintPercentWidth percentH $mMatchConstraintPercentHeight"
2295 
2296 enum class LayoutInfoFlags {
2297     NONE,
2298     BOUNDS
2299 }
2300