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.annotation.FloatRange
20 import androidx.compose.foundation.layout.LayoutScopeMarker
21 import androidx.compose.runtime.Stable
22 import androidx.compose.ui.graphics.TransformOrigin
23 import androidx.compose.ui.layout.FirstBaseline
24 import androidx.compose.ui.unit.Dp
25 import androidx.compose.ui.unit.dp
26 import androidx.constraintlayout.core.parser.CLArray
27 import androidx.constraintlayout.core.parser.CLNumber
28 import androidx.constraintlayout.core.parser.CLObject
29 import androidx.constraintlayout.core.parser.CLString
30 import kotlin.properties.ObservableProperty
31 import kotlin.reflect.KProperty
32 
33 /**
34  * Scope that can be used to constrain a layout.
35  *
36  * Used within `Modifier.constrainAs` from the inline DSL API. And within `constrain` from the
37  * ConstraintSet-based API.
38  */
39 @LayoutScopeMarker
40 @Stable
41 class ConstrainScope
42 internal constructor(internal val id: Any, internal val containerObject: CLObject) {
43     /**
44      * Reference to the [ConstraintLayout] itself, which can be used to specify constraints between
45      * itself and its children.
46      */
47     val parent = ConstrainedLayoutReference("parent")
48 
49     /** The start anchor of the layout - can be constrained using [VerticalAnchorable.linkTo]. */
50     val start: VerticalAnchorable = ConstraintVerticalAnchorable(-2, containerObject)
51 
52     /** The left anchor of the layout - can be constrained using [VerticalAnchorable.linkTo]. */
53     val absoluteLeft: VerticalAnchorable = ConstraintVerticalAnchorable(0, containerObject)
54 
55     /** The top anchor of the layout - can be constrained using [HorizontalAnchorable.linkTo]. */
56     val top: HorizontalAnchorable = ConstraintHorizontalAnchorable(0, containerObject)
57 
58     /** The end anchor of the layout - can be constrained using [VerticalAnchorable.linkTo]. */
59     val end: VerticalAnchorable = ConstraintVerticalAnchorable(-1, containerObject)
60 
61     /** The right anchor of the layout - can be constrained using [VerticalAnchorable.linkTo]. */
62     val absoluteRight: VerticalAnchorable = ConstraintVerticalAnchorable(1, containerObject)
63 
64     /** The bottom anchor of the layout - can be constrained using [HorizontalAnchorable.linkTo]. */
65     val bottom: HorizontalAnchorable = ConstraintHorizontalAnchorable(1, containerObject)
66 
67     /** The [FirstBaseline] of the layout - can be constrained using [BaselineAnchorable.linkTo]. */
68     val baseline: BaselineAnchorable = ConstraintBaselineAnchorable(containerObject)
69 
70     /** The width of the [ConstraintLayout] child. */
71     var width: Dimension by DimensionProperty(Dimension.wrapContent)
72 
73     /** The height of the [ConstraintLayout] child. */
74     var height: Dimension by DimensionProperty(Dimension.wrapContent)
75 
76     /**
77      * The overall visibility of the [ConstraintLayout] child.
78      *
79      * [Visibility.Visible] by default.
80      */
81     var visibility: Visibility by
82         object : ObservableProperty<Visibility>(Visibility.Visible) {
afterChangenull83             override fun afterChange(
84                 property: KProperty<*>,
85                 oldValue: Visibility,
86                 newValue: Visibility
87             ) {
88                 containerObject.putString(property.name, newValue.name)
89             }
90         }
91 
92     /** The transparency value when rendering the content. */
93     @FloatRange(from = 0.0, to = 1.0)
94     var alpha: Float = 1.0f
95         set(value) {
96             // FloatRange annotation doesn't work with delegate objects
97             field = value
98             if (!value.isNaN()) {
99                 containerObject.putNumber("alpha", value)
100             }
101         }
102 
103     /** The percent scaling value on the horizontal axis. Where 1 is 100%. */
104     var scaleX: Float by FloatProperty(1.0f)
105 
106     /** The percent scaling value on the vertical axis. Where 1 is 100%. */
107     var scaleY: Float by FloatProperty(1.0f)
108 
109     /** The degrees to rotate the content over the horizontal axis. */
110     var rotationX: Float by FloatProperty(0.0f)
111 
112     /** The degrees to rotate the content over the vertical axis. */
113     var rotationY: Float by FloatProperty(0.0f)
114 
115     /** The degrees to rotate the content on the screen plane. */
116     var rotationZ: Float by FloatProperty(0.0f)
117 
118     /** The distance to offset the content over the X axis. */
119     var translationX: Dp by DpProperty(0.dp)
120 
121     /** The distance to offset the content over the Y axis. */
122     var translationY: Dp by DpProperty(0.dp)
123 
124     /** The distance to offset the content over the Z axis. */
125     var translationZ: Dp by DpProperty(0.dp)
126 
127     /**
128      * The X axis offset percent where the content is rotated and scaled.
129      *
130      * @see [TransformOrigin]
131      */
132     var pivotX: Float by FloatProperty(0.5f)
133 
134     /**
135      * The Y axis offset percent where the content is rotated and scaled.
136      *
137      * @see [TransformOrigin]
138      */
139     var pivotY: Float by FloatProperty(0.5f)
140 
141     /**
142      * Whenever the width is not fixed, this weight may be used by an horizontal Chain to decide how
143      * much space assign to this widget.
144      */
145     var horizontalChainWeight: Float by FloatProperty(Float.NaN, "hWeight")
146 
147     /**
148      * Whenever the height is not fixed, this weight may be used by a vertical Chain to decide how
149      * much space assign to this widget.
150      */
151     var verticalChainWeight: Float by FloatProperty(Float.NaN, "vWeight")
152 
153     /**
154      * Applied when the widget has constraints on the [start] and [end] anchors. It defines the
155      * position of the widget relative to the space within the constraints, where `0f` is the
156      * left-most position and `1f` is the right-most position.
157      *
158      * When layout direction is RTL, the value of the bias is effectively inverted.
159      *
160      * E.g.: For `horizontalBias = 0.3f`, `0.7f` is used for RTL.
161      *
162      * Note that the bias may also be applied with calls such as [linkTo].
163      */
164     @FloatRange(from = 0.0, to = 1.0)
165     var horizontalBias: Float = 0.5f
166         set(value) {
167             // FloatRange annotation doesn't work with delegate objects
168             field = value
169             if (!value.isNaN()) {
170                 containerObject.putNumber("hBias", value)
171             }
172         }
173 
174     /**
175      * Applied when the widget has constraints on the [top] and [bottom] anchors. It defines the
176      * position of the widget relative to the space within the constraints, where `0f` is the
177      * top-most position and `1f` is the bottom-most position.
178      */
179     @FloatRange(from = 0.0, to = 1.0)
180     var verticalBias: Float = 0.5f
181         set(value) {
182             // FloatRange annotation doesn't work with delegate objects
183             field = value
184             if (!value.isNaN()) {
185                 containerObject.putNumber("vBias", value)
186             }
187         }
188 
189     /** Adds both start and end links towards other [ConstraintLayoutBaseScope.VerticalAnchor]s. */
linkTonull190     fun linkTo(
191         start: ConstraintLayoutBaseScope.VerticalAnchor,
192         end: ConstraintLayoutBaseScope.VerticalAnchor,
193         startMargin: Dp = 0.dp,
194         endMargin: Dp = 0.dp,
195         startGoneMargin: Dp = 0.dp,
196         endGoneMargin: Dp = 0.dp,
197         @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
198     ) {
199         this@ConstrainScope.start.linkTo(
200             anchor = start,
201             margin = startMargin,
202             goneMargin = startGoneMargin
203         )
204         this@ConstrainScope.end.linkTo(anchor = end, margin = endMargin, goneMargin = endGoneMargin)
205         containerObject.putNumber("hRtlBias", bias)
206     }
207 
208     /**
209      * Adds both top and bottom links towards other [ConstraintLayoutBaseScope.HorizontalAnchor]s.
210      */
linkTonull211     fun linkTo(
212         top: ConstraintLayoutBaseScope.HorizontalAnchor,
213         bottom: ConstraintLayoutBaseScope.HorizontalAnchor,
214         topMargin: Dp = 0.dp,
215         bottomMargin: Dp = 0.dp,
216         topGoneMargin: Dp = 0.dp,
217         bottomGoneMargin: Dp = 0.dp,
218         @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
219     ) {
220         this@ConstrainScope.top.linkTo(anchor = top, margin = topMargin, goneMargin = topGoneMargin)
221         this@ConstrainScope.bottom.linkTo(
222             anchor = bottom,
223             margin = bottomMargin,
224             goneMargin = bottomGoneMargin
225         )
226         containerObject.putNumber("vBias", bias)
227     }
228 
229     /**
230      * Adds all start, top, end, bottom links towards other
231      * [ConstraintLayoutBaseScope.HorizontalAnchor]s.
232      */
linkTonull233     fun linkTo(
234         start: ConstraintLayoutBaseScope.VerticalAnchor,
235         top: ConstraintLayoutBaseScope.HorizontalAnchor,
236         end: ConstraintLayoutBaseScope.VerticalAnchor,
237         bottom: ConstraintLayoutBaseScope.HorizontalAnchor,
238         startMargin: Dp = 0.dp,
239         topMargin: Dp = 0.dp,
240         endMargin: Dp = 0.dp,
241         bottomMargin: Dp = 0.dp,
242         startGoneMargin: Dp = 0.dp,
243         topGoneMargin: Dp = 0.dp,
244         endGoneMargin: Dp = 0.dp,
245         bottomGoneMargin: Dp = 0.dp,
246         @FloatRange(from = 0.0, to = 1.0) horizontalBias: Float = 0.5f,
247         @FloatRange(from = 0.0, to = 1.0) verticalBias: Float = 0.5f
248     ) {
249         linkTo(
250             start = start,
251             end = end,
252             startMargin = startMargin,
253             endMargin = endMargin,
254             startGoneMargin = startGoneMargin,
255             endGoneMargin = endGoneMargin,
256             bias = horizontalBias
257         )
258         linkTo(
259             top = top,
260             bottom = bottom,
261             topMargin = topMargin,
262             bottomMargin = bottomMargin,
263             topGoneMargin = topGoneMargin,
264             bottomGoneMargin = bottomGoneMargin,
265             bias = verticalBias
266         )
267     }
268 
269     /**
270      * Adds all start, top, end, bottom links towards the corresponding anchors of [other]. This
271      * will center the current layout inside or around (depending on size) [other].
272      */
centerTonull273     fun centerTo(other: ConstrainedLayoutReference) {
274         linkTo(other.start, other.top, other.end, other.bottom)
275     }
276 
277     /**
278      * Adds start and end links towards the corresponding anchors of [other]. This will center
279      * horizontally the current layout inside or around (depending on size) [other].
280      */
centerHorizontallyTonull281     fun centerHorizontallyTo(
282         other: ConstrainedLayoutReference,
283         @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
284     ) {
285         linkTo(start = other.start, end = other.end, bias = bias)
286     }
287 
288     /**
289      * Adds top and bottom links towards the corresponding anchors of [other]. This will center
290      * vertically the current layout inside or around (depending on size) [other].
291      */
centerVerticallyTonull292     fun centerVerticallyTo(
293         other: ConstrainedLayoutReference,
294         @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
295     ) {
296         linkTo(other.top, other.bottom, bias = bias)
297     }
298 
299     /**
300      * Adds start and end links towards a vertical [anchor]. This will center the current layout
301      * around the vertical [anchor].
302      */
centerAroundnull303     fun centerAround(anchor: ConstraintLayoutBaseScope.VerticalAnchor) {
304         linkTo(anchor, anchor)
305     }
306 
307     /**
308      * Adds top and bottom links towards a horizontal [anchor]. This will center the current layout
309      * around the horizontal [anchor].
310      */
centerAroundnull311     fun centerAround(anchor: ConstraintLayoutBaseScope.HorizontalAnchor) {
312         linkTo(anchor, anchor)
313     }
314 
315     /**
316      * Set a circular constraint relative to the center of [other]. This will position the current
317      * widget at a relative angle and distance from [other].
318      */
circularnull319     fun circular(other: ConstrainedLayoutReference, angle: Float, distance: Dp) {
320         val circularParams =
321             CLArray(charArrayOf()).apply {
322                 add(CLString.from(other.id.toString()))
323                 add(CLNumber(angle))
324                 add(CLNumber(distance.value))
325             }
326         containerObject.put("circular", circularParams)
327     }
328 
329     /**
330      * Clear the constraints on the horizontal axis (left, right, start, end).
331      *
332      * Useful when extending another [ConstraintSet] with unwanted constraints on this axis.
333      */
clearHorizontalnull334     fun clearHorizontal() {
335         containerObject.remove("left")
336         containerObject.remove("right")
337         containerObject.remove("start")
338         containerObject.remove("end")
339     }
340 
341     /**
342      * Clear the constraints on the vertical axis (top, bottom, baseline).
343      *
344      * Useful when extending another [ConstraintSet] with unwanted constraints on this axis.
345      */
clearVerticalnull346     fun clearVertical() {
347         containerObject.remove("top")
348         containerObject.remove("bottom")
349         containerObject.remove("baseline")
350     }
351 
352     /**
353      * Clear all constraints (vertical, horizontal, circular).
354      *
355      * Useful when extending another [ConstraintSet] with unwanted constraints applied.
356      */
clearConstraintsnull357     fun clearConstraints() {
358         clearHorizontal()
359         clearVertical()
360         containerObject.remove("circular")
361     }
362 
363     /**
364      * Resets the [width] and [height] to their default values.
365      *
366      * Useful when extending another [ConstraintSet] with unwanted dimensions.
367      */
resetDimensionsnull368     fun resetDimensions() {
369         width = Dimension.wrapContent
370         height = Dimension.wrapContent
371     }
372 
373     /**
374      * Reset all render-time transforms of the content to their default values.
375      *
376      * Does not modify the [visibility] property.
377      *
378      * Useful when extending another [ConstraintSet] with unwanted transforms applied.
379      */
resetTransformsnull380     fun resetTransforms() {
381         containerObject.remove("alpha")
382         containerObject.remove("scaleX")
383         containerObject.remove("scaleY")
384         containerObject.remove("rotationX")
385         containerObject.remove("rotationY")
386         containerObject.remove("rotationZ")
387         containerObject.remove("translationX")
388         containerObject.remove("translationY")
389         containerObject.remove("translationZ")
390         containerObject.remove("pivotX")
391         containerObject.remove("pivotY")
392     }
393 
394     /**
395      * Convenience extension method to parse a [Dp] as a [Dimension] object.
396      *
397      * @see Dimension.value
398      */
asDimensionnull399     fun Dp.asDimension(): Dimension = Dimension.value(this)
400 
401     private inner class DimensionProperty(initialValue: Dimension) :
402         ObservableProperty<Dimension>(initialValue) {
403         override fun afterChange(property: KProperty<*>, oldValue: Dimension, newValue: Dimension) {
404             containerObject.put(property.name, (newValue as DimensionDescription).asCLElement())
405         }
406     }
407 
408     private inner class FloatProperty(
409         initialValue: Float,
410         private val nameOverride: String? = null
411     ) : ObservableProperty<Float>(initialValue) {
afterChangenull412         override fun afterChange(property: KProperty<*>, oldValue: Float, newValue: Float) {
413             if (!newValue.isNaN()) {
414                 containerObject.putNumber(nameOverride ?: property.name, newValue)
415             }
416         }
417     }
418 
419     private inner class DpProperty(initialValue: Dp, private val nameOverride: String? = null) :
420         ObservableProperty<Dp>(initialValue) {
afterChangenull421         override fun afterChange(property: KProperty<*>, oldValue: Dp, newValue: Dp) {
422             if (!newValue.value.isNaN()) {
423                 containerObject.putNumber(nameOverride ?: property.name, newValue.value)
424             }
425         }
426     }
427 }
428 
429 /**
430  * Represents a vertical side of a layout (i.e start and end) that can be anchored using [linkTo] in
431  * their `Modifier.constrainAs` blocks.
432  */
433 private class ConstraintVerticalAnchorable constructor(index: Int, containerObject: CLObject) :
434     BaseVerticalAnchorable(containerObject, index)
435 
436 /**
437  * Represents a horizontal side of a layout (i.e top and bottom) that can be anchored using [linkTo]
438  * in their `Modifier.constrainAs` blocks.
439  */
440 private class ConstraintHorizontalAnchorable constructor(index: Int, containerObject: CLObject) :
441     BaseHorizontalAnchorable(containerObject, index)
442 
443 /**
444  * Represents the [FirstBaseline] of a layout that can be anchored using [linkTo] in their
445  * `Modifier.constrainAs` blocks.
446  */
447 private class ConstraintBaselineAnchorable constructor(private val containerObject: CLObject) :
448     BaselineAnchorable {
449     /** Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor]. */
linkTonull450     override fun linkTo(
451         anchor: ConstraintLayoutBaseScope.BaselineAnchor,
452         margin: Dp,
453         goneMargin: Dp
454     ) {
455         val constraintArray =
456             CLArray(charArrayOf()).apply {
457                 add(CLString.from(anchor.id.toString()))
458                 add(CLString.from("baseline"))
459                 add(CLNumber(margin.value))
460                 add(CLNumber(goneMargin.value))
461             }
462         containerObject.put("baseline", constraintArray)
463     }
464 
465     /** Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor]. */
linkTonull466     override fun linkTo(
467         anchor: ConstraintLayoutBaseScope.HorizontalAnchor,
468         margin: Dp,
469         goneMargin: Dp
470     ) {
471         val targetAnchorName = AnchorFunctions.horizontalAnchorIndexToAnchorName(anchor.index)
472         val constraintArray =
473             CLArray(charArrayOf()).apply {
474                 add(CLString.from(anchor.id.toString()))
475                 add(CLString.from(targetAnchorName))
476                 add(CLNumber(margin.value))
477                 add(CLNumber(goneMargin.value))
478             }
479         containerObject.put("baseline", constraintArray)
480     }
481 }
482