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