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