1 /*
2  * Copyright (C) 2021 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 androidx.compose.runtime.Immutable
20 import androidx.compose.ui.layout.Measurable
21 import androidx.constraintlayout.core.parser.CLObject
22 import androidx.constraintlayout.core.state.ConstraintSetParser
23 import androidx.constraintlayout.core.state.ConstraintSetParser.LayoutVariables
24 import androidx.constraintlayout.core.state.Transition
25 
26 @JvmDefaultWithCompatibility
27 /**
28  * Immutable description of the constraints used to layout the children of a [ConstraintLayout].
29  *
30  * Layout items are defined with [ConstraintSetScope.createRefFor], each layout item must be defined
31  * with a unique ID from other items in the same scope:
32  * ```
33  * val textRef = createRefFor("text")
34  * val imageRef = createRefFor("image")
35  * ```
36  *
37  * You may also use [ConstraintSetScope.createRefsFor] to declare up to 16 items at a time using the
38  * destructuring declaration pattern:
39  * ```
40  * val (textRef, imageRef) = createRefsFor("text", "image")
41  * ```
42  *
43  * Individual constraints are defined with [ConstraintSetScope.constrain]. Where you can tell each
44  * layout reference how to constrain to other references including the **`parent`**:
45  * ```
46  * constrain(textRef) {
47  *     centerTo(parent)
48  * }
49  * constrain(imageRef) {
50  *     centerVerticallyTo(textRef)
51  *     start.linkTo(textRef.end, margin = 8.dp)
52  * }
53  * ```
54  *
55  * Here, we constrain the *textRef* to the center of the *parent*, while the image is centered
56  * vertically to *textRef* and is horizontally placed to the right of its end anchor with a margin
57  * (keep in mind, when using `center...`, `start` or `end` the layout direction will automatically
58  * change in RTL locales).
59  *
60  * See [ConstrainScope] to learn more about how to constrain elements together.
61  *
62  * In the ConstraintLayout or MotionLayout Composables, the children must be bound using
63  * [Modifier.layoutId][androidx.compose.ui.layout.layoutId].
64  *
65  * So, the whole snippet with ConstraintLayout would look like this:
66  * ```
67  * val textId = "text"
68  * val imageId = "image"
69  * ConstraintLayout(
70  *     constraintSet = ConstraintSet {
71  *         val (textRef, imageRef) = createRefsFor(textId, imageId)
72  *         constrain(textRef) {
73  *             centerTo(parent)
74  *         }
75  *         constrain(imageRef) {
76  *             centerVerticallyTo(textRef)
77  *             start.linkTo(textRef.end, margin = 8.dp)
78  *         }
79  *     },
80  *     modifier = Modifier.fillMaxSize()
81  * ) {
82  *     Text(
83  *         modifier = Modifier.layoutId(textId),
84  *         text = "Hello, World!"
85  *     )
86  *     Image(
87  *         modifier = Modifier.layoutId(imageId),
88  *         imageVector = Icons.Default.Android,
89  *         contentDescription = null
90  *     )
91  * }
92  * ```
93  *
94  * ## Helpers
95  * You may also use helpers, a set of virtual (not shown on screen) components that provide special
96  * layout behaviors, you may find these in the [ConstraintSetScope] with the '`create...`' prefix, a
97  * few of these are **Guidelines**, **Chains** and **Barriers**.
98  *
99  * ### Guidelines
100  * Lines to which other [ConstrainedLayoutReference]s may be constrained to, these are defined at
101  * either a fixed or percent position from an anchor of the ConstraintLayout parent (top, bottom,
102  * start, end, absoluteLeft, absoluteRight).
103  *
104  * Example:
105  * ```
106  * val (textRef) = createRefsFor(textId)
107  * val vG = createGuidelineFromStart(fraction = 0.3f)
108  *
109  * constrain(textRef) {
110  *     centerVerticallyTo(parent)
111  *     centerAround(vG)
112  * }
113  * ```
114  *
115  * See
116  * - [ConstraintSetScope.createGuidelineFromTop]
117  * - [ConstraintSetScope.createGuidelineFromBottom]
118  * - [ConstraintSetScope.createGuidelineFromStart]
119  * - [ConstraintSetScope.createGuidelineFromEnd]
120  * - [ConstraintSetScope.createGuidelineFromAbsoluteLeft]
121  * - [ConstraintSetScope.createGuidelineFromAbsoluteRight]
122  *
123  * ### Chains
124  * Chains may be either horizontal or vertical, these, take a set of [ConstrainedLayoutReference]s
125  * and create bi-directional constraints on each of them at the same orientation of the chain in the
126  * given order, meaning that an horizontal chain will create constraints between the start and end
127  * anchors.
128  *
129  * The result, a layout that evenly distributes the space within its elements.
130  *
131  * For example, to make a layout with three text elements distributed so that the spacing between
132  * them (and around them) is equal:
133  * ```
134  * val (textRef0, textRef1, textRef2) = createRefsFor("text0", "text1", "text2")
135  * createHorizontalChain(textRef0, textRef1, textRef2, chainStyle = ChainStyle.Spread)
136  * ```
137  *
138  * You may set margins within elements in a chain with [ConstraintLayoutScope.withChainParams]:
139  * ```
140  * val (textRef0, textRef1, textRef2) = createRefsFor("text0", "text1", "text2")
141  * createHorizontalChain(
142  *     textRef0,
143  *     textRef1.withChainParams(startMargin = 100.dp, endMargin = 100.dp),
144  *     textRef2,
145  *     chainStyle = ChainStyle.Spread
146  * )
147  * ```
148  *
149  * You can also change the way space is distributed, as chains have three different styles:
150  * - [ChainStyle.Spread] Layouts are evenly distributed after margins are accounted for (the space
151  *   around and between each item is even). This is the **default** style for chains.
152  * - [ChainStyle.SpreadInside] The first and last layouts are affixed to each end of the chain, and
153  *   the rest of the items are evenly distributed (after margins are accounted for). I.e.: Items are
154  *   spread from the inside, distributing the space between them with no space around the first and
155  *   last items.
156  * - [ChainStyle.Packed] The layouts are packed together after margins are accounted for, by
157  *   default, they're packed together at the middle, you can change this behavior with the **bias**
158  *   parameter of [ChainStyle.Packed].
159  * - Alternatively, you can make every Layout in the chain to be [Dimension.fillToConstraints] and
160  *   then set a particular weight to each of them to create a **weighted chain**.
161  *
162  * #### Weighted Chain
163  * Weighted chains are useful when you want the size of the elements to depend on the remaining size
164  * of the chain. As opposed to just distributing the space around and/or in-between the items.
165  *
166  * For example, to create a layout with three text elements in a row where each element takes the
167  * exact same size regardless of content, you can use a simple weighted chain where each item has
168  * the same weight:
169  * ```
170  * val (textRef0, textRef1, textRef2) = createRefsFor("text0", "text1", "text2")
171  * createHorizontalChain(
172  *     textRef0.withChainParams(weight = 1f),
173  *     textRef1.withChainParams(weight = 1f),
174  *     textRef2.withChainParams(weight = 1f),
175  *     chainStyle = ChainStyle.Spread
176  * )
177  *
178  * constrain(textRef0, textRef1, textRef2) {
179  *     width = Dimension.fillToConstraints
180  * }
181  * ```
182  *
183  * This way, the texts will horizontally occupy the same space even if one of them is significantly
184  * larger than the others.
185  *
186  * Also note that when using [ConstraintSetScope] you can apply the same constrains to multiple
187  * references at a time.
188  *
189  * Keep in mind that chains have a relatively high performance cost. For example, if you plan on
190  * having multiple chains one below the other, consider instead, applying just one chain and using
191  * it as a reference to constrain all other elements to the ones that match their position in that
192  * one chain. It may provide increased performance with no significant changes in the layout output.
193  *
194  * Alternatively, consider if other helpers such as [ConstraintSetScope.createGrid] can accomplish
195  * the same layout.
196  *
197  * See
198  * - [ConstraintSetScope.createHorizontalChain]
199  * - [ConstraintSetScope.createVerticalChain]
200  * - [ConstraintSetScope.withChainParams]
201  *
202  * ### Barriers
203  * Barriers take a set of [ConstrainedLayoutReference]s and creates the most further point in a
204  * given direction where other [ConstrainedLayoutReference] can constrain to.
205  *
206  * This is useful in situations where elements in a layout may have different sizes but you want to
207  * always constrain to the largest item, for example, if you have a text element on top of another
208  * and want an image to always be constrained to the end of them:
209  * ```
210  * val (textRef0, textRef1, imageRef) = createRefsFor("text0", "text1", "image")
211  *
212  * // Creates a point at the furthest end anchor from the elements in the barrier
213  * val endTextsBarrier = createEndBarrier(textRef0, textRef1)
214  *
215  * constrain(textRef0) {
216  *     centerTo(parent)
217  * }
218  * constrain(textRef1) {
219  *     top.linkTo(textRef0.bottom)
220  *     start.linkTo(textRef0.start)
221  * }
222  *
223  * constrain(imageRef) {
224  *     top.linkTo(textRef0.top)
225  *     bottom.linkTo(textRef1.bottom)
226  *
227  *     // Image will always be at the end of both texts, regardless of their size
228  *     start.linkTo(endTextsBarrier, margin = 8.dp)
229  * }
230  * ```
231  *
232  * Be careful not to constrain a [ConstrainedLayoutReference] to a barrier that references it or
233  * that depends on it indirectly. This creates a cyclic dependency that results in unsupported
234  * layout behavior.
235  *
236  * See
237  * - [ConstraintSetScope.createTopBarrier]
238  * - [ConstraintSetScope.createBottomBarrier]
239  * - [ConstraintSetScope.createStartBarrier]
240  * - [ConstraintSetScope.createEndBarrier]
241  * - [ConstraintSetScope.createAbsoluteLeftBarrier]
242  * - [ConstraintSetScope.createAbsoluteRightBarrier]
243  */
244 @Immutable
245 interface ConstraintSet {
246     /** Applies the [ConstraintSet] to a state. */
applyTonull247     fun applyTo(state: State, measurables: List<Measurable>)
248 
249     fun override(name: String, value: Float) = this
250 
251     fun applyTo(transition: Transition, type: Int) {
252         // nothing here, used in MotionLayout
253     }
254 
isDirtynull255     fun isDirty(measurables: List<Measurable>): Boolean = true
256 }
257 
258 @JvmDefaultWithCompatibility
259 @Immutable
260 internal interface DerivedConstraintSet : ConstraintSet {
261     /**
262      * [ConstraintSet] that this instance will derive its constraints from.
263      *
264      * This means that the constraints from [extendFrom] will be applied before the constraints of
265      * this [DerivedConstraintSet] instance.
266      */
267     val extendFrom: ConstraintSet?
268 
269     override fun applyTo(state: State, measurables: List<Measurable>) {
270         extendFrom?.applyTo(state, measurables)
271         applyToState(state)
272     }
273 
274     /**
275      * Inheritors should implement this function so that derived constraints are applied properly.
276      */
277     fun applyToState(state: State)
278 }
279 
280 /**
281  * [ConstraintSet] defined solely on the given [clObject]. Only meant to be used to extract a copy
282  * of the underlying ConstraintSet of ConstraintLayout with the inline Modifier DSL.
283  *
284  * You likely don't mean to use this.
285  */
286 @Immutable
287 @PublishedApi
288 internal class RawConstraintSet(private val clObject: CLObject) : ConstraintSet {
289     private val layoutVariables = LayoutVariables()
290 
applyTonull291     override fun applyTo(state: State, measurables: List<Measurable>) {
292         ConstraintSetParser.populateState(clObject, state, layoutVariables)
293     }
294 
equalsnull295     override fun equals(other: Any?): Boolean {
296         if (this === other) return true
297         if (javaClass != other?.javaClass) return false
298 
299         other as RawConstraintSet
300 
301         if (clObject != other.clObject) return false
302 
303         return true
304     }
305 
hashCodenull306     override fun hashCode(): Int {
307         return clObject.hashCode()
308     }
309 }
310