1 /* <lambda>null2 * Copyright 2020 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.content.Context 20 import android.os.Build 21 import androidx.compose.animation.core.tween 22 import androidx.compose.foundation.background 23 import androidx.compose.foundation.layout.Box 24 import androidx.compose.foundation.layout.Column 25 import androidx.compose.foundation.layout.IntrinsicSize 26 import androidx.compose.foundation.layout.aspectRatio 27 import androidx.compose.foundation.layout.fillMaxSize 28 import androidx.compose.foundation.layout.fillMaxWidth 29 import androidx.compose.foundation.layout.height 30 import androidx.compose.foundation.layout.padding 31 import androidx.compose.foundation.layout.size 32 import androidx.compose.foundation.layout.width 33 import androidx.compose.foundation.layout.wrapContentHeight 34 import androidx.compose.foundation.rememberScrollState 35 import androidx.compose.foundation.verticalScroll 36 import androidx.compose.runtime.CompositionLocalProvider 37 import androidx.compose.runtime.getValue 38 import androidx.compose.runtime.mutableStateOf 39 import androidx.compose.runtime.remember 40 import androidx.compose.runtime.setValue 41 import androidx.compose.ui.Modifier 42 import androidx.compose.ui.geometry.Offset 43 import androidx.compose.ui.graphics.Color 44 import androidx.compose.ui.graphics.asAndroidBitmap 45 import androidx.compose.ui.graphics.toArgb 46 import androidx.compose.ui.layout.FirstBaseline 47 import androidx.compose.ui.layout.LookaheadScope 48 import androidx.compose.ui.layout.boundsInParent 49 import androidx.compose.ui.layout.layout 50 import androidx.compose.ui.layout.layoutId 51 import androidx.compose.ui.layout.onGloballyPositioned 52 import androidx.compose.ui.layout.onPlaced 53 import androidx.compose.ui.layout.positionInParent 54 import androidx.compose.ui.layout.positionInRoot 55 import androidx.compose.ui.node.Ref 56 import androidx.compose.ui.platform.InspectableValue 57 import androidx.compose.ui.platform.LocalLayoutDirection 58 import androidx.compose.ui.platform.ValueElement 59 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled 60 import androidx.compose.ui.platform.testTag 61 import androidx.compose.ui.test.assertHeightIsEqualTo 62 import androidx.compose.ui.test.assertPositionInRootIsEqualTo 63 import androidx.compose.ui.test.assertWidthIsEqualTo 64 import androidx.compose.ui.test.captureToImage 65 import androidx.compose.ui.test.junit4.createComposeRule 66 import androidx.compose.ui.test.onNodeWithTag 67 import androidx.compose.ui.unit.Constraints 68 import androidx.compose.ui.unit.Dp 69 import androidx.compose.ui.unit.IntOffset 70 import androidx.compose.ui.unit.IntSize 71 import androidx.compose.ui.unit.LayoutDirection 72 import androidx.compose.ui.unit.dp 73 import androidx.compose.ui.unit.round 74 import androidx.compose.ui.util.fastRoundToInt 75 import androidx.test.core.app.ApplicationProvider 76 import androidx.test.ext.junit.runners.AndroidJUnit4 77 import androidx.test.filters.MediumTest 78 import androidx.test.filters.SdkSuppress 79 import kotlin.math.roundToInt 80 import kotlin.test.assertNotEquals 81 import kotlin.test.assertTrue 82 import org.junit.After 83 import org.junit.Assert.assertEquals 84 import org.junit.Assert.assertNull 85 import org.junit.Before 86 import org.junit.Ignore 87 import org.junit.Rule 88 import org.junit.Test 89 import org.junit.runner.RunWith 90 91 @MediumTest 92 @RunWith(AndroidJUnit4::class) 93 class ConstraintLayoutTest { 94 @get:Rule val rule = createComposeRule() 95 96 private var displaySize: IntSize = IntSize.Zero 97 98 // region sizing tests 99 100 @Before 101 fun before() { 102 isDebugInspectorInfoEnabled = true 103 displaySize = 104 ApplicationProvider.getApplicationContext<Context>().resources.displayMetrics.let { 105 IntSize(it.widthPixels, it.heightPixels) 106 } 107 } 108 109 @After 110 fun after() { 111 isDebugInspectorInfoEnabled = false 112 } 113 114 @Test 115 fun dividerMatchTextHeight_spread() = 116 with(rule.density) { 117 val aspectRatioBoxSize = Ref<IntSize>() 118 val dividerSize = Ref<IntSize>() 119 120 rule.setContent { 121 ConstraintLayout( 122 // Make CL fixed width and wrap content height. 123 modifier = Modifier.fillMaxWidth() 124 ) { 125 val (aspectRatioBox, divider) = createRefs() 126 val guideline = createGuidelineFromAbsoluteLeft(0.5f) 127 128 Box( 129 Modifier.constrainAs(aspectRatioBox) { 130 centerTo(parent) 131 start.linkTo(guideline) 132 width = Dimension.preferredWrapContent 133 height = Dimension.wrapContent 134 } 135 // Try to be large to make wrap content impossible. 136 .width((displaySize.width).toDp()) 137 // This could be any (width in height out child) e.g. text 138 .aspectRatio(2f) 139 .onGloballyPositioned { coordinates -> 140 aspectRatioBoxSize.value = coordinates.size 141 } 142 ) 143 Box( 144 Modifier.constrainAs(divider) { 145 centerTo(parent) 146 width = Dimension.value(1.dp) 147 height = Dimension.fillToConstraints 148 } 149 .onGloballyPositioned { coordinates -> 150 dividerSize.value = coordinates.size 151 } 152 ) 153 } 154 } 155 156 rule.runOnIdle { 157 // The aspect ratio could not wrap and it is wrap suggested, so it respects 158 // constraints. 159 assertEquals( 160 (displaySize.width / 2f).roundToInt(), 161 aspectRatioBoxSize.value!!.width 162 ) 163 // Aspect ratio is preserved. 164 assertEquals( 165 (displaySize.width / 2f / 2f).roundToInt(), 166 aspectRatioBoxSize.value!!.height 167 ) 168 // Divider has fixed width 1.dp in constraint set. 169 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width) 170 // Divider has spread height so it should spread to fill the height of the CL, 171 // which in turns is given by the size of the aspect ratio box. 172 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height) 173 } 174 } 175 176 @Test 177 fun dividerMatchTextHeight_spread_withPreferredWrapHeightText() = 178 with(rule.density) { 179 val aspectRatioBoxSize = Ref<IntSize>() 180 val dividerSize = Ref<IntSize>() 181 rule.setContent { 182 ConstraintLayout( 183 // Make CL fixed width and wrap content height. 184 modifier = Modifier.fillMaxWidth() 185 ) { 186 val (aspectRatioBox, divider) = createRefs() 187 val guideline = createGuidelineFromAbsoluteLeft(0.5f) 188 189 Box( 190 Modifier.constrainAs(aspectRatioBox) { 191 centerTo(parent) 192 start.linkTo(guideline) 193 width = Dimension.preferredWrapContent 194 height = Dimension.preferredWrapContent 195 } 196 // Try to be large to make wrap content impossible. 197 .width((displaySize.width).toDp()) 198 // This could be any (width in height out child) e.g. text 199 .aspectRatio(2f) 200 .onGloballyPositioned { coordinates -> 201 aspectRatioBoxSize.value = coordinates.size 202 } 203 ) 204 Box( 205 Modifier.constrainAs(divider) { 206 centerTo(parent) 207 width = Dimension.value(1.dp) 208 height = Dimension.fillToConstraints 209 } 210 .onGloballyPositioned { coordinates -> 211 dividerSize.value = coordinates.size 212 } 213 ) 214 } 215 } 216 217 rule.runOnIdle { 218 // The aspect ratio could not wrap and it is wrap suggested, so it respects 219 // constraints. 220 assertEquals( 221 (displaySize.width / 2f).roundToInt(), 222 aspectRatioBoxSize.value!!.width 223 ) 224 // Aspect ratio is preserved. 225 assertEquals( 226 (displaySize.width / 2f / 2f).roundToInt(), 227 aspectRatioBoxSize.value!!.height 228 ) 229 // Divider has fixed width 1.dp in constraint set. 230 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width) 231 // Divider has spread height so it should spread to fill the height of the CL, 232 // which in turns is given by the size of the aspect ratio box. 233 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height) 234 } 235 } 236 237 @Test 238 fun dividerMatchTextHeight_percent() = 239 with(rule.density) { 240 val aspectRatioBoxSize = Ref<IntSize>() 241 val dividerSize = Ref<IntSize>() 242 rule.setContent { 243 ConstraintLayout( 244 // Make CL fixed width and wrap content height. 245 modifier = Modifier.fillMaxWidth() 246 ) { 247 val (aspectRatioBox, divider) = createRefs() 248 val guideline = createGuidelineFromAbsoluteLeft(0.5f) 249 250 Box( 251 Modifier.constrainAs(aspectRatioBox) { 252 centerTo(parent) 253 start.linkTo(guideline) 254 width = Dimension.preferredWrapContent 255 height = Dimension.wrapContent 256 } 257 // Try to be large to make wrap content impossible. 258 .width((displaySize.width).toDp()) 259 // This could be any (width in height out child) e.g. text 260 .aspectRatio(2f) 261 .onGloballyPositioned { coordinates -> 262 aspectRatioBoxSize.value = coordinates.size 263 } 264 ) 265 Box( 266 Modifier.constrainAs(divider) { 267 centerTo(parent) 268 width = Dimension.value(1.dp) 269 height = Dimension.percent(0.8f) 270 } 271 .onGloballyPositioned { coordinates -> 272 dividerSize.value = coordinates.size 273 } 274 ) 275 } 276 } 277 278 rule.runOnIdle { 279 // The aspect ratio could not wrap and it is wrap suggested, so it respects 280 // constraints. 281 assertEquals( 282 (displaySize.width / 2f).roundToInt(), 283 aspectRatioBoxSize.value!!.width 284 ) 285 // Aspect ratio is preserved. 286 assertEquals( 287 (displaySize.width / 2f / 2f).roundToInt(), 288 aspectRatioBoxSize.value!!.height 289 ) 290 // Divider has fixed width 1.dp in constraint set. 291 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width) 292 // Divider has percent height so it should spread to fill 0.8 of the height of the 293 // CL, 294 // which in turns is given by the size of the aspect ratio box. 295 assertEquals( 296 (aspectRatioBoxSize.value!!.height * 0.8f).roundToInt(), 297 dividerSize.value!!.height 298 ) 299 } 300 } 301 302 @Test 303 @Ignore 304 fun dividerMatchTextHeight_inWrapConstraintLayout_longText() = 305 with(rule.density) { 306 val aspectRatioBoxSize = Ref<IntSize>() 307 val dividerSize = Ref<IntSize>() 308 rule.setContent { 309 // CL is wrap content. 310 ConstraintLayout { 311 val (aspectRatioBox, divider) = createRefs() 312 val guideline = createGuidelineFromAbsoluteLeft(0.5f) 313 314 Box( 315 Modifier.constrainAs(aspectRatioBox) { 316 centerTo(parent) 317 start.linkTo(guideline) 318 width = Dimension.preferredWrapContent 319 height = Dimension.wrapContent 320 } 321 // Try to be large to make wrap content impossible. 322 .width((displaySize.width).toDp()) 323 // This could be any (width in height out child) e.g. text 324 .aspectRatio(2f) 325 .onGloballyPositioned { coordinates -> 326 aspectRatioBoxSize.value = coordinates.size 327 } 328 ) 329 Box( 330 Modifier.constrainAs(divider) { 331 centerTo(parent) 332 width = Dimension.value(1.dp) 333 height = Dimension.percent(0.8f) 334 } 335 .onGloballyPositioned { coordinates -> 336 dividerSize.value = coordinates.size 337 } 338 ) 339 } 340 } 341 342 rule.runOnIdle { 343 // The aspect ratio could not wrap and it is wrap suggested, so it respects 344 // constraints. 345 assertEquals((displaySize.width / 2), aspectRatioBoxSize.value!!.width) 346 // Aspect ratio is preserved. 347 assertEquals((displaySize.width / 2 / 2), aspectRatioBoxSize.value!!.height) 348 // Divider has fixed width 1.dp in constraint set. 349 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width) 350 // Divider has percent height so it should spread to fill 0.8 of the height of the 351 // CL, 352 // which in turns is given by the size of the aspect ratio box. 353 // TODO(popam; b/150277566): uncomment 354 assertEquals( 355 "broken, display size ${displaySize.width}x${displaySize.height} aspect" + 356 " height ${aspectRatioBoxSize.value!!.width}x" + 357 "${aspectRatioBoxSize.value!!.height}, " + 358 "divider: ${dividerSize.value!!.height}", 359 (aspectRatioBoxSize.value!!.height * 0.8f).roundToInt(), 360 dividerSize.value!!.height 361 ) 362 assertEquals( 363 "broken, aspect height ${aspectRatioBoxSize.value!!.width}x" + 364 "${aspectRatioBoxSize.value!!.height}," + 365 " divider: ${dividerSize.value!!.height}", 366 aspectRatioBoxSize.value!!.width, 367 540 368 ) 369 } 370 } 371 372 @Test 373 fun dividerMatchTextHeight_inWrapConstraintLayout_shortText() = 374 with(rule.density) { 375 val constraintLayoutSize = Ref<IntSize>() 376 val aspectRatioBoxSize = Ref<IntSize>() 377 val dividerSize = Ref<IntSize>() 378 val size = 40.toDp() 379 rule.setContent { 380 ConstraintLayout( 381 // CL is wrapping width and height. 382 modifier = 383 Modifier.onGloballyPositioned { constraintLayoutSize.value = it.size } 384 ) { 385 val (aspectRatioBox, divider) = createRefs() 386 val guideline = createGuidelineFromAbsoluteLeft(0.5f) 387 388 Box( 389 Modifier.constrainAs(aspectRatioBox) { 390 centerTo(parent) 391 start.linkTo(guideline) 392 width = Dimension.preferredWrapContent 393 height = Dimension.wrapContent 394 } 395 // Small width for the CL to wrap it. 396 .width(size) 397 // This could be any (width in height out child) e.g. text 398 .aspectRatio(2f) 399 .onGloballyPositioned { coordinates -> 400 aspectRatioBoxSize.value = coordinates.size 401 } 402 ) 403 Box( 404 Modifier.constrainAs(divider) { 405 centerTo(parent) 406 width = Dimension.value(1.dp) 407 height = Dimension.fillToConstraints 408 } 409 .onGloballyPositioned { coordinates -> 410 dividerSize.value = coordinates.size 411 } 412 ) 413 } 414 } 415 416 rule.runOnIdle { 417 // The width of the ConstraintLayout should be twice the width of the aspect ratio 418 // box. 419 assertEquals(size.roundToPx() * 2, constraintLayoutSize.value!!.width) 420 // The height of the ConstraintLayout should be the height of the aspect ratio box. 421 assertEquals(size.roundToPx() / 2, constraintLayoutSize.value!!.height) 422 // The aspect ratio gets the requested size. 423 assertEquals(size.roundToPx(), aspectRatioBoxSize.value!!.width) 424 // Aspect ratio is preserved. 425 assertEquals(size.roundToPx() / 2, aspectRatioBoxSize.value!!.height) 426 // Divider has fixed width 1.dp in constraint set. 427 assertEquals(1.dp.roundToPx(), dividerSize.value!!.width) 428 // Divider should have the height of the aspect ratio box. 429 assertEquals(aspectRatioBoxSize.value!!.height, dividerSize.value!!.height) 430 } 431 } 432 433 // endregion 434 435 // region positioning tests 436 437 @Test 438 fun testConstraintLayout_withInlineDSL() = 439 with(rule.density) { 440 var rootSize: IntSize = IntSize.Zero 441 val boxSize = 100 442 val offset = 150 443 444 val position: Array<IntOffset> = Array(3) { IntOffset.Zero } 445 446 rule.setContent { 447 ConstraintLayout( 448 Modifier.fillMaxSize().onGloballyPositioned { rootSize = it.size } 449 ) { 450 val (box0, box1, box2) = createRefs() 451 Box( 452 Modifier.constrainAs(box0) { centerTo(parent) } 453 .size(boxSize.toDp(), boxSize.toDp()) 454 .onGloballyPositioned { position[0] = it.positionInRoot().round() } 455 ) 456 val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f) 457 Box( 458 Modifier.constrainAs(box1) { 459 start.linkTo(half, margin = offset.toDp()) 460 bottom.linkTo(box0.top) 461 } 462 .size(boxSize.toDp(), boxSize.toDp()) 463 .onGloballyPositioned { position[1] = it.positionInRoot().round() } 464 ) 465 Box( 466 Modifier.constrainAs(box2) { 467 start.linkTo(parent.start, margin = offset.toDp()) 468 bottom.linkTo(parent.bottom, margin = offset.toDp()) 469 } 470 .size(boxSize.toDp(), boxSize.toDp()) 471 .onGloballyPositioned { position[2] = it.positionInRoot().round() } 472 ) 473 } 474 } 475 476 val displayWidth = rootSize.width 477 val displayHeight = rootSize.height 478 479 rule.runOnIdle { 480 assertEquals( 481 Offset(((displayWidth - boxSize) / 2f), ((displayHeight - boxSize) / 2f)) 482 .round(), 483 position[0] 484 ) 485 assertEquals( 486 Offset((displayWidth / 2f + offset), ((displayHeight - boxSize) / 2f - boxSize)) 487 .round(), 488 position[1] 489 ) 490 assertEquals(IntOffset(offset, (displayHeight - boxSize - offset)), position[2]) 491 } 492 } 493 494 @Test 495 fun testConstraintLayout_withConstraintSet() = 496 with(rule.density) { 497 var rootSize: IntSize = IntSize.Zero 498 val boxSize = 100 499 val offset = 150 500 501 val position: Array<IntOffset> = Array(3) { IntOffset.Zero } 502 503 rule.setContent { 504 ConstraintLayout( 505 constraintSet = 506 ConstraintSet { 507 val box0 = createRefFor("box0") 508 val box1 = createRefFor("box1") 509 val box2 = createRefFor("box2") 510 511 constrain(box0) { centerTo(parent) } 512 513 val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f) 514 constrain(box1) { 515 start.linkTo(half, margin = offset.toDp()) 516 bottom.linkTo(box0.top) 517 } 518 519 constrain(box2) { 520 start.linkTo(parent.start, margin = offset.toDp()) 521 bottom.linkTo(parent.bottom, margin = offset.toDp()) 522 } 523 }, 524 modifier = Modifier.fillMaxSize().onGloballyPositioned { rootSize = it.size } 525 ) { 526 for (i in 0..2) { 527 Box( 528 Modifier.layoutId("box$i") 529 .size(boxSize.toDp(), boxSize.toDp()) 530 .onGloballyPositioned { position[i] = it.positionInRoot().round() } 531 ) 532 } 533 } 534 } 535 536 val displayWidth = rootSize.width 537 val displayHeight = rootSize.height 538 539 rule.runOnIdle { 540 assertEquals( 541 Offset((displayWidth - boxSize) / 2f, (displayHeight - boxSize) / 2f).round(), 542 position[0] 543 ) 544 assertEquals( 545 Offset((displayWidth / 2f + offset), ((displayHeight - boxSize) / 2f - boxSize)) 546 .round(), 547 position[1] 548 ) 549 assertEquals(IntOffset(offset, (displayHeight - boxSize - offset)), position[2]) 550 } 551 } 552 553 @Test 554 @Ignore 555 fun testConstraintLayout_rtl() = 556 with(rule.density) { 557 val boxSize = 100 558 val offset = 150 559 560 val position = Array(3) { Ref<Offset>() } 561 562 rule.setContent { 563 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 564 ConstraintLayout(Modifier.fillMaxSize()) { 565 val (box0, box1, box2) = createRefs() 566 Box( 567 Modifier.constrainAs(box0) { centerTo(parent) } 568 .size(boxSize.toDp(), boxSize.toDp()) 569 .onGloballyPositioned { position[0].value = it.positionInRoot() } 570 ) 571 val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f) 572 Box( 573 Modifier.constrainAs(box1) { 574 start.linkTo(half, margin = offset.toDp()) 575 bottom.linkTo(box0.top) 576 } 577 .size(boxSize.toDp(), boxSize.toDp()) 578 .onGloballyPositioned { position[1].value = it.positionInRoot() } 579 ) 580 Box( 581 Modifier.constrainAs(box2) { 582 start.linkTo(parent.start, margin = offset.toDp()) 583 bottom.linkTo(parent.bottom, margin = offset.toDp()) 584 } 585 .size(boxSize.toDp(), boxSize.toDp()) 586 .onGloballyPositioned { position[2].value = it.positionInRoot() } 587 ) 588 } 589 } 590 } 591 592 val displayWidth = displaySize.width 593 val displayHeight = displaySize.height 594 595 rule.runOnIdle { 596 assertEquals( 597 Offset((displayWidth - boxSize) / 2f, (displayHeight - boxSize) / 2f), 598 position[0].value 599 ) 600 assertEquals( 601 Offset( 602 (displayWidth / 2 - offset - boxSize).toFloat(), 603 ((displayHeight - boxSize) / 2 - boxSize).toFloat() 604 ), 605 position[1].value 606 ) 607 assertEquals( 608 Offset( 609 (displayWidth - offset - boxSize).toFloat(), 610 (displayHeight - boxSize - offset).toFloat() 611 ), 612 position[2].value 613 ) 614 } 615 } 616 617 @Test 618 fun testConstraintLayout_guidelines_ltr() = 619 with(rule.density) { 620 val size = 200.toDp() 621 val offset = 50.toDp() 622 623 val position = Array(8) { 0f } 624 rule.setContent { 625 ConstraintLayout(Modifier.size(size)) { 626 val guidelines = 627 arrayOf( 628 createGuidelineFromStart(offset), 629 createGuidelineFromAbsoluteLeft(offset), 630 createGuidelineFromEnd(offset), 631 createGuidelineFromAbsoluteRight(offset), 632 createGuidelineFromStart(0.25f), 633 createGuidelineFromAbsoluteLeft(0.25f), 634 createGuidelineFromEnd(0.25f), 635 createGuidelineFromAbsoluteRight(0.25f) 636 ) 637 638 guidelines.forEachIndexed { index, guideline -> 639 val ref = createRef() 640 Box( 641 Modifier.size(1.dp) 642 .constrainAs(ref) { absoluteLeft.linkTo(guideline) } 643 .onGloballyPositioned { position[index] = it.positionInParent().x } 644 ) 645 } 646 } 647 } 648 649 assertGuidelinesLtrPositions(position) 650 } 651 652 @Test 653 fun testConstraintLayout_json_guidelines_ltr() = 654 with(rule.density) { 655 val size = 200.toDp() 656 val offset = 50.toDp() 657 658 val position = Array(8) { 0f } 659 rule.setContent { 660 ConstraintLayout( 661 constraintSet = ConstraintSet(getJsonGuidelinesContent(offset.value)), 662 Modifier.size(size) 663 ) { 664 position.forEachIndexed { index, _ -> 665 Box( 666 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned { 667 position[index] = it.positionInParent().x 668 } 669 ) 670 } 671 } 672 } 673 674 assertGuidelinesLtrPositions(position) 675 } 676 677 @Test 678 fun testConstraintLayout_guidelines_rtl() = 679 with(rule.density) { 680 val size = 200.toDp() 681 val offset = 50.toDp() 682 683 val position = Array(8) { 0f } 684 rule.setContent { 685 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 686 ConstraintLayout(Modifier.size(size)) { 687 val guidelines = 688 arrayOf( 689 createGuidelineFromStart(offset), 690 createGuidelineFromAbsoluteLeft(offset), 691 createGuidelineFromEnd(offset), 692 createGuidelineFromAbsoluteRight(offset), 693 createGuidelineFromStart(0.25f), 694 createGuidelineFromAbsoluteLeft(0.25f), 695 createGuidelineFromEnd(0.25f), 696 createGuidelineFromAbsoluteRight(0.25f) 697 ) 698 699 guidelines.forEachIndexed { index, guideline -> 700 val ref = createRef() 701 Box( 702 Modifier.size(1.dp) 703 .constrainAs(ref) { absoluteLeft.linkTo(guideline) } 704 .onGloballyPositioned { 705 position[index] = it.positionInParent().x 706 } 707 ) 708 } 709 } 710 } 711 } 712 713 assertGuidelinesRtlPositions(position) 714 } 715 716 @Test 717 fun testConstraintLayout_json_guidelines_rtl() = 718 with(rule.density) { 719 val size = 200.toDp() 720 val offset = 50.toDp() 721 722 val position = Array(8) { 0f } 723 rule.setContent { 724 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 725 ConstraintLayout( 726 constraintSet = ConstraintSet(getJsonGuidelinesContent(offset.value)), 727 Modifier.size(size) 728 ) { 729 position.forEachIndexed { index, _ -> 730 Box( 731 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned { 732 position[index] = it.positionInParent().x 733 } 734 ) 735 } 736 } 737 } 738 } 739 740 assertGuidelinesRtlPositions(position) 741 } 742 743 @Test 744 fun testConstraintLayout_barriers_ltr() = 745 with(rule.density) { 746 val size = 200.toDp() 747 val offset = 50.toDp() 748 749 val position = Array(4) { 0f } 750 rule.setContent { 751 ConstraintLayout(Modifier.size(size)) { 752 val (box1, box2) = createRefs() 753 val guideline1 = createGuidelineFromAbsoluteLeft(offset) 754 val guideline2 = createGuidelineFromAbsoluteRight(offset) 755 Box( 756 Modifier.size(1.toDp()).constrainAs(box1) { 757 absoluteLeft.linkTo(guideline1) 758 } 759 ) 760 Box( 761 Modifier.size(1.toDp()).constrainAs(box2) { 762 absoluteLeft.linkTo(guideline2) 763 } 764 ) 765 766 val barriers = 767 arrayOf( 768 createStartBarrier(box1, box2), 769 createAbsoluteLeftBarrier(box1, box2), 770 createEndBarrier(box1, box2), 771 createAbsoluteRightBarrier(box1, box2) 772 ) 773 774 barriers.forEachIndexed { index, barrier -> 775 val ref = createRef() 776 Box( 777 Modifier.size(1.dp) 778 .constrainAs(ref) { absoluteLeft.linkTo(barrier) } 779 .onGloballyPositioned { position[index] = it.positionInParent().x } 780 ) 781 } 782 } 783 } 784 785 assertBarriersLtrPositions(position) 786 } 787 788 @Test 789 fun testConstraintLayout_json_barriers_ltr() = 790 with(rule.density) { 791 val size = 200.toDp() 792 val offset = 50.toDp() 793 794 val position = Array(4) { 0f } 795 rule.setContent { 796 ConstraintLayout( 797 constraintSet = ConstraintSet(getJsonBarriersContent(offset.value)), 798 Modifier.size(size) 799 ) { 800 Box(Modifier.size(1.toDp()).layoutId("boxA")) 801 Box(Modifier.size(1.toDp()).layoutId("boxB")) 802 position.forEachIndexed { index, _ -> 803 Box( 804 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned { 805 position[index] = it.positionInParent().x 806 } 807 ) 808 } 809 } 810 } 811 812 assertBarriersLtrPositions(position) 813 } 814 815 @Test 816 fun testConstraintLayout_barriers_rtl() = 817 with(rule.density) { 818 val size = 200.toDp() 819 val offset = 50.toDp() 820 821 val position = Array(4) { 0f } 822 rule.setContent { 823 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 824 ConstraintLayout(Modifier.size(size)) { 825 val (box1, box2) = createRefs() 826 val guideline1 = createGuidelineFromAbsoluteLeft(offset) 827 val guideline2 = createGuidelineFromAbsoluteRight(offset) 828 Box( 829 Modifier.size(1.toDp()).constrainAs(box1) { 830 absoluteLeft.linkTo(guideline1) 831 } 832 ) 833 Box( 834 Modifier.size(1.toDp()).constrainAs(box2) { 835 absoluteLeft.linkTo(guideline2) 836 } 837 ) 838 839 val barriers = 840 arrayOf( 841 createStartBarrier(box1, box2), 842 createAbsoluteLeftBarrier(box1, box2), 843 createEndBarrier(box1, box2), 844 createAbsoluteRightBarrier(box1, box2) 845 ) 846 847 barriers.forEachIndexed { index, barrier -> 848 val ref = createRef() 849 Box( 850 Modifier.size(1.dp) 851 .constrainAs(ref) { absoluteLeft.linkTo(barrier) } 852 .onGloballyPositioned { 853 position[index] = it.positionInParent().x 854 } 855 ) 856 } 857 } 858 } 859 } 860 861 assertBarriersRtlPositions(position) 862 } 863 864 @Test 865 fun testConstraintLayout_json_barriers_rtl() = 866 with(rule.density) { 867 val size = 200.toDp() 868 val offset = 50.toDp() 869 870 val position = Array(4) { 0f } 871 rule.setContent { 872 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 873 ConstraintLayout( 874 constraintSet = ConstraintSet(getJsonBarriersContent(offset.value)), 875 Modifier.size(size) 876 ) { 877 Box(Modifier.size(1.toDp()).layoutId("boxA")) 878 Box(Modifier.size(1.toDp()).layoutId("boxB")) 879 position.forEachIndexed { index, _ -> 880 Box( 881 Modifier.size(1.dp).layoutId("box$index").onGloballyPositioned { 882 position[index] = it.positionInParent().x 883 } 884 ) 885 } 886 } 887 } 888 } 889 890 assertBarriersRtlPositions(position) 891 } 892 893 @Test 894 fun testConstraintLayout_anchors_ltr() = 895 with(rule.density) { 896 val size = 200.toDp() 897 val offset = 50.toDp() 898 899 val position = Array(16) { 0f } 900 rule.setContent { 901 ConstraintLayout(Modifier.size(size)) { 902 val box = createRef() 903 val guideline = createGuidelineFromAbsoluteLeft(offset) 904 Box(Modifier.size(1.toDp()).constrainAs(box) { absoluteLeft.linkTo(guideline) }) 905 906 val anchors = listAnchors(box) 907 908 anchors.forEachIndexed { index, anchor -> 909 val ref = createRef() 910 Box( 911 Modifier.size(1.toDp()) 912 .constrainAs(ref) { anchor() } 913 .onGloballyPositioned { position[index] = it.positionInParent().x } 914 ) 915 } 916 } 917 } 918 919 assertAnchorsLtrPositions(position) 920 } 921 922 @Test 923 fun testConstraintLayout_json_anchors_ltr() = 924 with(rule.density) { 925 val size = 200.toDp() 926 val offset = 50.toDp() 927 928 val position = Array(16) { 0f } 929 rule.setContent { 930 ConstraintLayout( 931 constraintSet = ConstraintSet(getJsonAnchorsContent(offset.value)), 932 Modifier.size(size) 933 ) { 934 Box(Modifier.size(1.toDp()).layoutId("box")) 935 position.forEachIndexed { index, _ -> 936 Box( 937 Modifier.size(1.toDp()).layoutId("box$index").onGloballyPositioned { 938 position[index] = it.positionInParent().x 939 } 940 ) 941 } 942 } 943 } 944 945 assertAnchorsLtrPositions(position) 946 } 947 948 @Test 949 fun testConstraintLayout_anchors_rtl() = 950 with(rule.density) { 951 val size = 200.toDp() 952 val offset = 50.toDp() 953 954 val position = Array(16) { 0f } 955 rule.setContent { 956 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 957 ConstraintLayout(Modifier.size(size)) { 958 val box = createRef() 959 val guideline = createGuidelineFromAbsoluteLeft(offset) 960 Box( 961 Modifier.size(1.toDp()).constrainAs(box) { 962 absoluteLeft.linkTo(guideline) 963 } 964 ) 965 966 val anchors = listAnchors(box) 967 968 anchors.forEachIndexed { index, anchor -> 969 val ref = createRef() 970 Box( 971 Modifier.size(1.toDp()) 972 .constrainAs(ref) { anchor() } 973 .onGloballyPositioned { 974 position[index] = it.positionInParent().x 975 } 976 ) 977 } 978 } 979 } 980 } 981 982 assertAnchorsRtlPositions(position) 983 } 984 985 @Test 986 fun testConstraintLayout_json_anchors_rtl() = 987 with(rule.density) { 988 val size = 200.toDp() 989 val offset = 50.toDp() 990 991 val position = Array(16) { 0f } 992 rule.setContent { 993 CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { 994 ConstraintLayout( 995 constraintSet = ConstraintSet(getJsonAnchorsContent(offset.value)), 996 Modifier.size(size) 997 ) { 998 Box(Modifier.size(1.toDp()).layoutId("box")) 999 position.forEachIndexed { index, _ -> 1000 Box( 1001 Modifier.size(1.toDp()).layoutId("box$index").onGloballyPositioned { 1002 position[index] = it.positionInParent().x 1003 } 1004 ) 1005 } 1006 } 1007 } 1008 } 1009 1010 assertAnchorsRtlPositions(position) 1011 } 1012 1013 @Test 1014 fun testConstraintLayout_barriers_margins() = 1015 with(rule.density) { 1016 val size = 200.toDp() 1017 val offset = 50.toDp() 1018 1019 val position = Array(2) { Offset(0f, 0f) } 1020 rule.setContent { 1021 ConstraintLayout(Modifier.size(size)) { 1022 val box = createRef() 1023 val guideline1 = createGuidelineFromAbsoluteLeft(offset) 1024 val guideline2 = createGuidelineFromTop(offset) 1025 Box( 1026 Modifier.size(1.toDp()).constrainAs(box) { 1027 absoluteLeft.linkTo(guideline1) 1028 top.linkTo(guideline2) 1029 } 1030 ) 1031 1032 val leftBarrier = createAbsoluteLeftBarrier(box, margin = 10.toDp()) 1033 val topBarrier = createTopBarrier(box, margin = 10.toDp()) 1034 val rightBarrier = createAbsoluteRightBarrier(box, margin = 10.toDp()) 1035 val bottomBarrier = createBottomBarrier(box, margin = 10.toDp()) 1036 1037 Box( 1038 Modifier.size(1.dp) 1039 .constrainAs(createRef()) { 1040 absoluteLeft.linkTo(leftBarrier) 1041 top.linkTo(topBarrier) 1042 } 1043 .onGloballyPositioned { position[0] = it.positionInParent() } 1044 ) 1045 1046 Box( 1047 Modifier.size(1.dp) 1048 .constrainAs(createRef()) { 1049 absoluteLeft.linkTo(rightBarrier) 1050 top.linkTo(bottomBarrier) 1051 } 1052 .onGloballyPositioned { position[1] = it.positionInParent() } 1053 ) 1054 } 1055 } 1056 1057 rule.runOnIdle { 1058 assertEquals(Offset(60f, 60f), position[0]) 1059 assertEquals(Offset(61f, 61f), position[1]) 1060 } 1061 } 1062 1063 @Test 1064 fun links_canBeOverridden() { 1065 rule.setContent { 1066 ConstraintLayout(Modifier.width(10.dp)) { 1067 val box = createRef() 1068 Box( 1069 Modifier.constrainAs(box) { 1070 start.linkTo(parent.end) 1071 start.linkTo(parent.start) 1072 } 1073 .onGloballyPositioned { assertEquals(0f, it.positionInParent().x) } 1074 ) 1075 } 1076 } 1077 rule.waitForIdle() 1078 } 1079 1080 @Test 1081 @Ignore 1082 fun chains_defaultOutsideConstraintsCanBeOverridden() = 1083 with(rule.density) { 1084 val size = 100.toDp() 1085 val boxSize = 10.toDp() 1086 val guidelinesOffset = 20.toDp() 1087 rule.setContent { 1088 ConstraintLayout(Modifier.size(size)) { 1089 val (box1, box2) = createRefs() 1090 val startGuideline = createGuidelineFromStart(guidelinesOffset) 1091 val topGuideline = createGuidelineFromTop(guidelinesOffset) 1092 val endGuideline = createGuidelineFromEnd(guidelinesOffset) 1093 val bottomGuideline = createGuidelineFromBottom(guidelinesOffset) 1094 createHorizontalChain(box1, box2, chainStyle = ChainStyle.SpreadInside) 1095 createVerticalChain(box1, box2, chainStyle = ChainStyle.SpreadInside) 1096 Box( 1097 Modifier.size(boxSize) 1098 .constrainAs(box1) { 1099 start.linkTo(startGuideline) 1100 top.linkTo(topGuideline) 1101 } 1102 .onGloballyPositioned { 1103 assertEquals(20f, it.boundsInParent().left) 1104 assertEquals(20f, it.boundsInParent().top) 1105 } 1106 ) 1107 Box( 1108 Modifier.size(boxSize) 1109 .constrainAs(box2) { 1110 end.linkTo(endGuideline) 1111 bottom.linkTo(bottomGuideline) 1112 } 1113 .onGloballyPositioned { 1114 assertEquals(80f, it.boundsInParent().right) 1115 assertEquals(80f, it.boundsInParent().bottom) 1116 } 1117 ) 1118 } 1119 } 1120 rule.waitForIdle() 1121 } 1122 1123 @Test(expected = Test.None::class) 1124 fun testConstraintLayout_inlineDSL_recompositionDoesNotCrash() { 1125 val first = mutableStateOf(true) 1126 rule.setContent { 1127 ConstraintLayout { 1128 val box = createRef() 1129 if (first.value) { 1130 Box(Modifier.constrainAs(box) {}) 1131 } else { 1132 Box(Modifier.constrainAs(box) {}) 1133 } 1134 } 1135 } 1136 rule.runOnIdle { first.value = false } 1137 rule.waitForIdle() 1138 } 1139 1140 @Test(expected = Test.None::class) 1141 fun testConstraintLayout_ConstraintSetDSL_recompositionDoesNotCrash() { 1142 val first = mutableStateOf(true) 1143 rule.setContent { 1144 ConstraintLayout( 1145 ConstraintSet { 1146 val box = createRefFor("box") 1147 constrain(box) {} 1148 } 1149 ) { 1150 if (first.value) { 1151 Box(Modifier.layoutId("box")) 1152 } else { 1153 Box(Modifier.layoutId("box")) 1154 } 1155 } 1156 } 1157 rule.runOnIdle { first.value = false } 1158 rule.waitForIdle() 1159 } 1160 1161 @Test(expected = Test.None::class) 1162 fun testConstraintLayout_inlineDSL_remeasureDoesNotCrash() { 1163 val first = mutableStateOf(true) 1164 rule.setContent { 1165 ConstraintLayout(if (first.value) Modifier else Modifier.padding(10.dp)) { 1166 Box(if (first.value) Modifier else Modifier.size(20.dp)) 1167 } 1168 } 1169 rule.runOnIdle { first.value = false } 1170 rule.waitForIdle() 1171 } 1172 1173 @Test(expected = Test.None::class) 1174 fun testConstraintLayout_ConstraintSetDSL_remeasureDoesNotCrash() { 1175 val first = mutableStateOf(true) 1176 rule.setContent { 1177 ConstraintLayout( 1178 modifier = if (first.value) Modifier else Modifier.padding(10.dp), 1179 constraintSet = ConstraintSet {} 1180 ) { 1181 Box(if (first.value) Modifier else Modifier.size(20.dp)) 1182 } 1183 } 1184 rule.runOnIdle { first.value = false } 1185 rule.waitForIdle() 1186 } 1187 1188 @Test 1189 fun testConstraintLayout_doesNotCrashWhenOnlyContentIsRecomposed() { 1190 var smallSize by mutableStateOf(true) 1191 rule.setContent { 1192 Box { 1193 ConstraintLayout { 1194 val (box1, _) = createRefs() 1195 createBottomBarrier(box1) 1196 Box(Modifier.height(if (smallSize) 30.dp else 40.dp).constrainAs(box1) {}) 1197 Box(Modifier) 1198 } 1199 } 1200 } 1201 rule.runOnIdle { smallSize = false } 1202 rule.waitForIdle() 1203 } 1204 1205 @Test 1206 fun testInspectorValue() { 1207 rule.setContent { 1208 ConstraintLayout(Modifier.width(10.dp)) { 1209 val ref = createRef() 1210 val block: ConstrainScope.() -> Unit = {} 1211 val modifier = Modifier.constrainAs(ref, block) as InspectableValue 1212 1213 assertEquals("constrainAs", modifier.nameFallback) 1214 assertNull(modifier.valueOverride) 1215 val inspectableElements = modifier.inspectableElements.toList() 1216 assertEquals(2, inspectableElements.size) 1217 assertEquals(ValueElement("ref", ref), inspectableElements[0]) 1218 assertEquals(ValueElement("constrainBlock", block), inspectableElements[1]) 1219 } 1220 } 1221 } 1222 1223 @Test 1224 fun testConstraintLayout_doesNotRemeasureUnnecessarily() { 1225 var first by mutableStateOf(true) 1226 var dslExecutions = 0 1227 rule.setContent { 1228 val dslExecuted = remember { { ++dslExecutions } } 1229 ConstraintLayout { 1230 val (box1) = createRefs() 1231 val box2 = createRef() 1232 val guideline = createGuidelineFromStart(0.5f) 1233 val barrier = createAbsoluteLeftBarrier(box1) 1234 1235 // Make sure the content is reexecuted when first changes. 1236 @Suppress("UNUSED_EXPRESSION") first 1237 1238 // If the reference changed, we would remeasure and reexecute the DSL. 1239 Box(Modifier.constrainAs(box1) {}) 1240 // If the guideline, barrier or anchor changed or were inferred as un@Stable, we 1241 // would remeasure and reexecute the DSL. 1242 Box( 1243 Modifier.constrainAs(box2) { 1244 start.linkTo(box1.end) 1245 end.linkTo(guideline) 1246 start.linkTo(barrier) 1247 dslExecuted() 1248 } 1249 ) 1250 } 1251 } 1252 rule.runOnIdle { 1253 assertEquals(1, dslExecutions) 1254 first = false 1255 } 1256 rule.runOnIdle { assertEquals(1, dslExecutions) } 1257 } 1258 1259 @Test 1260 fun testConstraintLayout_doesRemeasure_whenHelpersChange_butConstraintsDont() { 1261 val size = 100 1262 val sizeDp = with(rule.density) { size.toDp() } 1263 var first by mutableStateOf(true) 1264 var box1Position = Offset(-1f, -1f) 1265 var box2Position = Offset(-1f, -1f) 1266 val box1PositionUpdater = 1267 Modifier.onGloballyPositioned { box1Position = it.positionInRoot() } 1268 val box2PositionUpdater = 1269 Modifier.onGloballyPositioned { box2Position = it.positionInRoot() } 1270 rule.setContent { 1271 ConstraintLayout { 1272 val (box1, box2) = createRefs() 1273 1274 if (!first) { 1275 createVerticalChain(box1, box2) 1276 } 1277 1278 Box(Modifier.size(sizeDp).then(box1PositionUpdater).constrainAs(box1) {}) 1279 Box(Modifier.size(sizeDp).then(box2PositionUpdater).constrainAs(box2) {}) 1280 } 1281 } 1282 rule.runOnIdle { 1283 assertEquals(Offset.Zero, box1Position) 1284 assertEquals(Offset.Zero, box2Position) 1285 first = false 1286 } 1287 rule.runOnIdle { 1288 assertEquals(Offset.Zero, box1Position) 1289 assertEquals(Offset(0f, size.toFloat()), box2Position) 1290 } 1291 } 1292 1293 @Test 1294 fun testConstraintLayout_doesRemeasure_whenHelpersDontChange_butConstraintsDo() { 1295 val size = 100 1296 val sizeDp = with(rule.density) { size.toDp() } 1297 var first by mutableStateOf(true) 1298 var box1Position = Offset(-1f, -1f) 1299 var box2Position = Offset(-1f, -1f) 1300 val box1PositionUpdater = 1301 Modifier.onGloballyPositioned { box1Position = it.positionInRoot() } 1302 val box2PositionUpdater = 1303 Modifier.onGloballyPositioned { box2Position = it.positionInRoot() } 1304 rule.setContent { 1305 ConstraintLayout { 1306 val (box1, box2) = createRefs() 1307 1308 val topBarrier = createTopBarrier(box1) 1309 val bottomBarrier = createBottomBarrier(box1) 1310 1311 Box(Modifier.size(sizeDp).then(box1PositionUpdater).constrainAs(box1) {}) 1312 Box( 1313 Modifier.size(sizeDp).then(box2PositionUpdater).constrainAs(box2) { 1314 if (first) { 1315 top.linkTo(topBarrier) 1316 } else { 1317 top.linkTo(bottomBarrier) 1318 } 1319 } 1320 ) 1321 } 1322 } 1323 rule.runOnIdle { 1324 assertEquals(Offset.Zero, box1Position) 1325 assertEquals(Offset.Zero, box2Position) 1326 first = false 1327 } 1328 rule.runOnIdle { 1329 assertEquals(Offset.Zero, box1Position) 1330 assertEquals(Offset(0f, size.toFloat()), box2Position) 1331 } 1332 } 1333 1334 @Test 1335 fun testConstraintLayout_updates_whenConstraintSetChanges() = 1336 with(rule.density) { 1337 val box1Size = 20 1338 var first by mutableStateOf(true) 1339 val constraintSet1 = ConstraintSet { 1340 val box1 = createRefFor("box1") 1341 val box2 = createRefFor("box2") 1342 constrain(box2) { start.linkTo(box1.end) } 1343 } 1344 val constraintSet2 = ConstraintSet { 1345 val box1 = createRefFor("box1") 1346 val box2 = createRefFor("box2") 1347 constrain(box2) { top.linkTo(box1.bottom) } 1348 } 1349 1350 var box2Position = IntOffset.Zero 1351 rule.setContent { 1352 ConstraintLayout(if (first) constraintSet1 else constraintSet2) { 1353 Box(Modifier.size(box1Size.toDp()).layoutId("box1")) 1354 Box( 1355 Modifier.layoutId("box2").onGloballyPositioned { 1356 box2Position = it.positionInRoot().round() 1357 } 1358 ) 1359 } 1360 } 1361 1362 rule.runOnIdle { 1363 assertEquals(IntOffset(box1Size, 0), box2Position) 1364 first = false 1365 } 1366 1367 rule.runOnIdle { assertEquals(IntOffset(0, box1Size), box2Position) } 1368 } 1369 1370 @Test 1371 fun testConstraintLayout_doesNotRebuildFromDsl_whenResizedOnly() { 1372 var size by mutableStateOf(100.dp) 1373 var builds = 0 1374 rule.setContent { 1375 val onBuild = remember { { ++builds } } 1376 ConstraintLayout(Modifier.size(size)) { 1377 val box = createRef() 1378 Box(Modifier.constrainAs(box) { onBuild() }) 1379 } 1380 } 1381 1382 rule.runOnIdle { 1383 assertEquals(1, builds) 1384 size = 200.dp 1385 } 1386 1387 rule.runOnIdle { assertEquals(1, builds) } 1388 } 1389 1390 @Test 1391 fun testConstraintLayout_rebuildsConstraintSet_whenHelpersChange() = 1392 with(rule.density) { 1393 var offset by mutableStateOf(10.dp) 1394 var builds = 0 1395 var obtainedX = 0f 1396 rule.setContent { 1397 ConstraintLayout { 1398 val box = createRef() 1399 val g = createGuidelineFromStart(offset) 1400 Box( 1401 Modifier.constrainAs(box) { 1402 start.linkTo(g) 1403 ++builds 1404 } 1405 .onGloballyPositioned { obtainedX = it.positionInRoot().x } 1406 ) 1407 } 1408 } 1409 1410 rule.runOnIdle { 1411 assertEquals(offset.roundToPx().toFloat(), obtainedX) 1412 offset = 20.dp 1413 assertEquals(1, builds) 1414 } 1415 1416 rule.runOnIdle { 1417 assertEquals(offset.roundToPx().toFloat(), obtainedX) 1418 assertEquals(2, builds) 1419 } 1420 } 1421 1422 @Test 1423 fun testConstraintLayout_doesNotRecomposeAgain_whenHelpersChange() { 1424 var offset by mutableStateOf(10.dp) 1425 var compositions = 0 1426 rule.setContent { 1427 ConstraintLayout { 1428 ++compositions 1429 val box = createRef() 1430 val g = createGuidelineFromStart(offset) 1431 Box(Modifier.constrainAs(box) { start.linkTo(g) }) 1432 } 1433 } 1434 1435 rule.runOnIdle { 1436 offset = 20.dp 1437 assertEquals(1, compositions) 1438 } 1439 1440 rule.runOnIdle { assertEquals(2, compositions) } 1441 } 1442 1443 @Test 1444 fun testConstraintLayout_rebuilds_whenLambdaChanges() = 1445 with(rule.density) { 1446 var first by mutableStateOf(true) 1447 var obtainedX = 0f 1448 rule.setContent { 1449 ConstraintLayout { 1450 val l1 = 1451 remember<ConstrainScope.() -> Unit> { 1452 { start.linkTo(parent.start, 10.dp) } 1453 } 1454 val l2 = 1455 remember<ConstrainScope.() -> Unit> { 1456 { start.linkTo(parent.start, 20.dp) } 1457 } 1458 val box = createRef() 1459 Box( 1460 Modifier.constrainAs(box, if (first) l1 else l2).onGloballyPositioned { 1461 obtainedX = it.positionInRoot().x 1462 } 1463 ) 1464 } 1465 } 1466 1467 rule.runOnIdle { 1468 assertEquals(10.dp.roundToPx().toFloat(), obtainedX) 1469 first = false 1470 } 1471 1472 rule.runOnIdle { assertEquals(20.dp.roundToPx().toFloat(), obtainedX) } 1473 } 1474 1475 @Test 1476 fun testConstraintLayout_updates_whenConstraintSetChangesConstraints() = 1477 with(rule.density) { 1478 val box1Size = 20 1479 var first by mutableStateOf(true) 1480 1481 var box2Position = IntOffset.Zero 1482 rule.setContent { 1483 val constraintSet = ConstraintSet { 1484 val box1 = createRefFor("box1") 1485 val box2 = createRefFor("box2") 1486 constrain(box2) { 1487 if (first) start.linkTo(box1.end) else top.linkTo(box1.bottom) 1488 } 1489 } 1490 ConstraintLayout(constraintSet) { 1491 Box(Modifier.size(box1Size.toDp()).layoutId("box1")) 1492 Box( 1493 Modifier.layoutId("box2").onGloballyPositioned { 1494 box2Position = it.positionInRoot().round() 1495 } 1496 ) 1497 } 1498 } 1499 1500 rule.runOnIdle { 1501 assertEquals(IntOffset(box1Size, 0), box2Position) 1502 first = false 1503 } 1504 1505 rule.runOnIdle { assertEquals(IntOffset(0, box1Size), box2Position) } 1506 } 1507 1508 @Test 1509 fun testConstraintLayout_doesNotUpdate_withRememberConstraintSet() = 1510 with(rule.density) { 1511 val box1Size = 20 1512 var first by mutableStateOf(true) 1513 var compCount = 0 1514 1515 var box2Position = IntOffset.Zero 1516 rule.setContent { 1517 // ConstraintSet should be immutable and shouldn't recompose if "remembered" 1518 val constraintSet = remember { 1519 ConstraintSet { 1520 val box1 = createRefFor("box1") 1521 val box2 = createRefFor("box2") 1522 constrain(box2) { 1523 if (first) start.linkTo(box1.end) else top.linkTo(box1.bottom) 1524 } 1525 } 1526 } 1527 compCount++ 1528 ConstraintLayout(constraintSet) { 1529 Box(Modifier.size(box1Size.toDp()).layoutId("box1")) 1530 Box( 1531 Modifier.layoutId("box2").onGloballyPositioned { 1532 box2Position = it.positionInRoot().round() 1533 } 1534 ) 1535 } 1536 } 1537 1538 rule.runOnIdle { 1539 assertEquals(IntOffset(box1Size, 0), box2Position) 1540 assertEquals(1, compCount) 1541 first = false 1542 } 1543 1544 rule.runOnIdle { 1545 assertEquals(IntOffset(box1Size, 0), box2Position) 1546 assertEquals(2, compCount) 1547 } 1548 } 1549 1550 @Test 1551 fun testClearDerivedConstraints_withConstraintSet() { 1552 var startOrEnd by mutableStateOf(true) 1553 val boxTag = "box1" 1554 rule.setContent { 1555 val start = remember { 1556 ConstraintSet { 1557 constrain(createRefFor(boxTag)) { 1558 width = Dimension.value(20.dp) 1559 height = Dimension.value(20.dp) 1560 start.linkTo(parent.start, 10.dp) 1561 bottom.linkTo(parent.bottom, 10.dp) 1562 } 1563 } 1564 } 1565 val end = remember { 1566 ConstraintSet(start) { 1567 constrain(createRefFor(boxTag)) { 1568 clearConstraints() 1569 top.linkTo(parent.top, 5.dp) 1570 end.linkTo(parent.end, 5.dp) 1571 } 1572 } 1573 } 1574 ConstraintLayout( 1575 modifier = Modifier.size(200.dp), 1576 constraintSet = if (startOrEnd) start else end 1577 ) { 1578 Box(modifier = Modifier.background(Color.Red).testTag(boxTag).layoutId(boxTag)) 1579 } 1580 } 1581 rule.waitForIdle() 1582 rule.onNodeWithTag(boxTag).assertPositionInRootIsEqualTo(10.dp, 170.dp) 1583 rule.runOnIdle { startOrEnd = !startOrEnd } 1584 rule.waitForIdle() 1585 rule.onNodeWithTag(boxTag).assertPositionInRootIsEqualTo(175.dp, 5.dp) 1586 } 1587 1588 @Test 1589 fun testLayoutReference_withConstraintSet() = 1590 with(rule.density) { 1591 val rootSizePx = 500 1592 val boxSizePx = 100 1593 val g1DistancePx = 50 1594 val marginToHelper = 10 1595 1596 val boxTag1 = "box1" 1597 val boxTag2 = "box2" 1598 1599 var boxPosition1 = IntOffset.Zero 1600 var boxPosition2 = IntOffset.Zero 1601 1602 val constraintSet = ConstraintSet { 1603 val box1 = createRefFor(boxTag1) 1604 val g1 = createGuidelineFromEnd(g1DistancePx.toDp()) 1605 val b1 = createEndBarrier(g1.reference, box1) 1606 1607 constrain(box1) { 1608 width = Dimension.value(boxSizePx.toDp()) 1609 height = Dimension.value(boxSizePx.toDp()) 1610 top.linkTo(parent.top) 1611 end.linkTo(g1, marginToHelper.toDp()) 1612 } 1613 constrain(createRefFor(boxTag2)) { 1614 width = Dimension.value(boxSizePx.toDp()) 1615 height = Dimension.value(boxSizePx.toDp()) 1616 top.linkTo(parent.top) 1617 start.linkTo(b1, marginToHelper.toDp()) 1618 } 1619 } 1620 rule.setContent { 1621 ConstraintLayout( 1622 modifier = Modifier.size(rootSizePx.toDp()), 1623 constraintSet = constraintSet 1624 ) { 1625 Box( 1626 modifier = 1627 Modifier.layoutTestId(boxTag1) 1628 .background(Color.Red) 1629 .onGloballyPositioned { boxPosition1 = it.positionInRoot().round() } 1630 ) 1631 Box( 1632 modifier = 1633 Modifier.layoutTestId(boxTag2) 1634 .background(Color.Blue) 1635 .onGloballyPositioned { boxPosition2 = it.positionInRoot().round() } 1636 ) 1637 } 1638 } 1639 rule.waitForIdle() 1640 1641 assertEquals(rootSizePx - g1DistancePx - marginToHelper - boxSizePx, boxPosition1.x) 1642 assertEquals(0, boxPosition1.y) 1643 1644 assertEquals(rootSizePx - g1DistancePx + marginToHelper, boxPosition2.x) 1645 assertEquals(0, boxPosition2.y) 1646 } 1647 1648 @Test 1649 fun testLayoutReference_withInlineDsl() = 1650 with(rule.density) { 1651 val rootSizePx = 500 1652 val boxSizePx = 100 1653 val g1DistancePx = 50 1654 val marginToHelper = 10 1655 1656 val boxTag1 = "box1" 1657 val boxTag2 = "box2" 1658 1659 var boxPosition1 = IntOffset.Zero 1660 var boxPosition2 = IntOffset.Zero 1661 rule.setContent { 1662 ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) { 1663 val (box1, box2) = createRefs() 1664 val g1 = createGuidelineFromEnd(g1DistancePx.toDp()) 1665 val b1 = createEndBarrier(g1.reference, box1) 1666 Box( 1667 modifier = 1668 Modifier.constrainAs(box1) { 1669 width = Dimension.value(boxSizePx.toDp()) 1670 height = Dimension.value(boxSizePx.toDp()) 1671 top.linkTo(parent.top) 1672 end.linkTo(g1, marginToHelper.toDp()) 1673 } 1674 .layoutTestId(boxTag1) 1675 .background(Color.Red) 1676 .onGloballyPositioned { boxPosition1 = it.positionInRoot().round() } 1677 ) 1678 Box( 1679 modifier = 1680 Modifier.constrainAs(box2) { 1681 width = Dimension.value(boxSizePx.toDp()) 1682 height = Dimension.value(boxSizePx.toDp()) 1683 top.linkTo(parent.top) 1684 start.linkTo(b1, marginToHelper.toDp()) 1685 } 1686 .layoutTestId(boxTag2) 1687 .background(Color.Blue) 1688 .onGloballyPositioned { boxPosition2 = it.positionInRoot().round() } 1689 ) 1690 } 1691 } 1692 rule.waitForIdle() 1693 1694 assertEquals(rootSizePx - g1DistancePx - marginToHelper - boxSizePx, boxPosition1.x) 1695 assertEquals(0, boxPosition1.y) 1696 1697 assertEquals(rootSizePx - g1DistancePx + marginToHelper, boxPosition2.x) 1698 assertEquals(0, boxPosition2.y) 1699 } 1700 1701 @Test 1702 fun testBias_withConstraintSet() { 1703 val rootSize = 100.dp 1704 val boxSize = 10.dp 1705 val horBias = 0.2f 1706 val verBias = 1f - horBias 1707 rule.setContent { 1708 ConstraintLayout( 1709 modifier = Modifier.size(rootSize), 1710 constraintSet = 1711 ConstraintSet { 1712 constrain(createRefFor("box")) { 1713 width = Dimension.value(boxSize) 1714 height = Dimension.value(boxSize) 1715 1716 centerTo(parent) 1717 horizontalBias = horBias 1718 verticalBias = verBias 1719 } 1720 } 1721 ) { 1722 Box(modifier = Modifier.background(Color.Red).layoutTestId("box")) 1723 } 1724 } 1725 rule.waitForIdle() 1726 rule 1727 .onNodeWithTag("box") 1728 .assertPositionInRootIsEqualTo((rootSize - boxSize) * 0.2f, (rootSize - boxSize) * 0.8f) 1729 } 1730 1731 @Test 1732 fun testBias_withInlineDsl() { 1733 val rootSize = 100.dp 1734 val boxSize = 10.dp 1735 val horBias = 0.2f 1736 val verBias = 1f - horBias 1737 rule.setContent { 1738 ConstraintLayout(Modifier.size(rootSize)) { 1739 val box = createRef() 1740 Box( 1741 modifier = 1742 Modifier.background(Color.Red) 1743 .constrainAs(box) { 1744 width = Dimension.value(boxSize) 1745 height = Dimension.value(boxSize) 1746 1747 centerTo(parent) 1748 horizontalBias = horBias 1749 verticalBias = verBias 1750 } 1751 .layoutTestId("box") 1752 ) 1753 } 1754 } 1755 rule.waitForIdle() 1756 rule 1757 .onNodeWithTag("box") 1758 .assertPositionInRootIsEqualTo((rootSize - boxSize) * 0.2f, (rootSize - boxSize) * 0.8f) 1759 } 1760 1761 @Test 1762 fun testConstraintSet_multipleRefs() = 1763 with(rule.density) { 1764 val rootSize = 50 1765 val boxSize = 20 1766 val margin = 10 1767 val positions = Array(3) { IntOffset.Zero } 1768 rule.setContent { 1769 ConstraintLayout( 1770 constraintSet = 1771 ConstraintSet { 1772 // Note that not enough IDs were provided, box2 will have a generated ID 1773 val (box0, box1, box2) = createRefsFor("box0", "box1") 1774 1775 constrain(box0, box1) { 1776 width = boxSize.toDp().asDimension() 1777 height = boxSize.toDp().asDimension() 1778 top.linkTo(parent.top, margin.toDp()) 1779 start.linkTo(parent.start, margin.toDp()) 1780 } 1781 constrain(box2) { 1782 width = boxSize.toDp().asDimension() 1783 height = boxSize.toDp().asDimension() 1784 1785 top.linkTo(box0.bottom) 1786 start.linkTo(box0.end) 1787 } 1788 }, 1789 Modifier.size(rootSize.toDp()) 1790 ) { 1791 Box( 1792 Modifier.layoutId("box0").onGloballyPositioned { 1793 positions[0] = it.positionInRoot().round() 1794 } 1795 ) 1796 Box( 1797 Modifier.layoutId("box1").onGloballyPositioned { 1798 positions[1] = it.positionInRoot().round() 1799 } 1800 ) 1801 Box( 1802 Modifier 1803 // Generated id, tho normally, the user wouldn't know what the ID is 1804 .layoutId("androidx.constraintlayout.id0") 1805 .onGloballyPositioned { positions[2] = it.positionInRoot().round() } 1806 ) 1807 } 1808 } 1809 rule.waitForIdle() 1810 assertEquals(IntOffset(margin, margin), positions[0]) 1811 assertEquals(IntOffset(margin, margin), positions[1]) 1812 assertEquals(IntOffset(margin + boxSize, margin + boxSize), positions[2]) 1813 } 1814 1815 @Test 1816 fun testLinkToBias_withInlineDsl_rtl() = 1817 with(rule.density) { 1818 val rootSize = 200 1819 val boxSize = 20 1820 val box1Bias = 0.2f 1821 val box2Bias = 0.2f 1822 1823 var box1Position = IntOffset.Zero 1824 var box2Position = IntOffset.Zero 1825 rule.setContent { 1826 CompositionLocalProvider(LocalLayoutDirection.provides(LayoutDirection.Rtl)) { 1827 ConstraintLayout(Modifier.size(rootSize.toDp())) { 1828 val (box1Ref, box2Ref) = createRefs() 1829 1830 Box( 1831 modifier = 1832 Modifier.background(Color.Red) 1833 .constrainAs(box1Ref) { 1834 width = Dimension.value(boxSize.toDp()) 1835 height = Dimension.value(boxSize.toDp()) 1836 1837 centerTo(parent) 1838 horizontalBias = box1Bias // unaffected by Rtl 1839 } 1840 .onGloballyPositioned { 1841 box1Position = it.positionInRoot().round() 1842 } 1843 ) 1844 Box( 1845 modifier = 1846 Modifier.background(Color.Blue) 1847 .constrainAs(box2Ref) { 1848 width = Dimension.value(boxSize.toDp()) 1849 height = Dimension.value(boxSize.toDp()) 1850 1851 top.linkTo(box1Ref.bottom) 1852 linkTo( 1853 start = parent.start, 1854 end = box1Ref.start, 1855 startMargin = 0.dp, 1856 endMargin = 0.dp, 1857 startGoneMargin = 0.dp, 1858 endGoneMargin = 0.dp, 1859 bias = box2Bias // affected by Rtl 1860 ) 1861 } 1862 .onGloballyPositioned { 1863 box2Position = it.positionInRoot().round() 1864 } 1865 ) 1866 } 1867 } 1868 } 1869 1870 rule.runOnIdle { 1871 val expectedBox1X = (rootSize - boxSize) * box1Bias 1872 val expectedBox1Y = (rootSize * 0.5f) - (boxSize * 0.5f) 1873 assertEquals(Offset(expectedBox1X, expectedBox1Y).round(), box1Position) 1874 1875 val expectedBox1End = expectedBox1X + boxSize 1876 val expectedBox2X = 1877 (rootSize - expectedBox1End - boxSize) * (1f - box2Bias) + expectedBox1End 1878 assertEquals(Offset(expectedBox2X, expectedBox1Y + boxSize).round(), box2Position) 1879 } 1880 } 1881 1882 @Test 1883 fun testContentRecomposition_withConstraintSet() = 1884 with(rule.density) { 1885 var constraintLayoutCompCount = 0 1886 1887 val baseWidth = 10 1888 val box0WidthMultiplier = mutableStateOf(2) 1889 val boxHeight = 30 1890 rule.setContent { 1891 ++constraintLayoutCompCount 1892 ConstraintLayout( 1893 constraintSet = 1894 ConstraintSet { 1895 val (box0, box1) = createRefsFor("box0", "box1") 1896 constrain(box0) { 1897 // previously, preferredWrapContent would fail if only the content 1898 // recomposed 1899 width = Dimension.preferredWrapContent 1900 1901 start.linkTo(parent.start) 1902 end.linkTo(parent.end) 1903 horizontalBias = 0f 1904 1905 top.linkTo(parent.top) 1906 } 1907 constrain(box1) { 1908 width = Dimension.fillToConstraints 1909 height = Dimension.wrapContent 1910 start.linkTo(box0.start) 1911 end.linkTo(box0.end) 1912 horizontalBias = 0f 1913 1914 top.linkTo(box0.bottom) 1915 } 1916 } 1917 ) { 1918 Box( 1919 Modifier.height(boxHeight.toDp()) 1920 .width((baseWidth * box0WidthMultiplier.value).toDp()) 1921 .layoutTestId("box0") 1922 .background(Color.Red) 1923 ) 1924 Box( 1925 Modifier.height(boxHeight.toDp()) 1926 .layoutTestId("box1") 1927 .background(Color.Blue) 1928 ) 1929 } 1930 } 1931 rule.waitForIdle() 1932 1933 rule.onNodeWithTag("box0").apply { 1934 assertPositionInRootIsEqualTo(0.dp, 0.dp) 1935 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1936 } 1937 rule.onNodeWithTag("box1").apply { 1938 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 1939 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1940 } 1941 1942 box0WidthMultiplier.value = 3 1943 rule.waitForIdle() 1944 1945 rule.onNodeWithTag("box0").apply { 1946 assertPositionInRootIsEqualTo(0.dp, 0.dp) 1947 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1948 } 1949 rule.onNodeWithTag("box1").apply { 1950 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 1951 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1952 } 1953 1954 box0WidthMultiplier.value = 1 1955 rule.waitForIdle() 1956 1957 rule.onNodeWithTag("box0").apply { 1958 assertPositionInRootIsEqualTo(0.dp, 0.dp) 1959 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1960 } 1961 rule.onNodeWithTag("box1").apply { 1962 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 1963 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 1964 } 1965 1966 assertEquals(1, constraintLayoutCompCount) 1967 } 1968 1969 @Test 1970 fun testContentRecomposition_withInlineModifier() = 1971 with(rule.density) { 1972 var constraintLayoutCompCount = 0 1973 1974 val baseWidth = 10 1975 val box0WidthMultiplier = mutableStateOf(2) 1976 val boxHeight = 30 1977 rule.setContent { 1978 ++constraintLayoutCompCount 1979 ConstraintLayout { 1980 val (box0, box1) = createRefs() 1981 Box( 1982 Modifier.height(boxHeight.toDp()) 1983 .width((baseWidth * box0WidthMultiplier.value).toDp()) 1984 .constrainAs(box0) { 1985 // previously, preferredWrapContent would fail if only the content 1986 // recomposed 1987 width = Dimension.preferredWrapContent 1988 1989 start.linkTo(parent.start) 1990 end.linkTo(parent.end) 1991 horizontalBias = 0f 1992 1993 top.linkTo(parent.top) 1994 } 1995 .testTag("box0") 1996 .background(Color.Red) 1997 ) 1998 Box( 1999 Modifier.height(boxHeight.toDp()) 2000 .constrainAs(box1) { 2001 width = Dimension.fillToConstraints 2002 height = Dimension.wrapContent 2003 start.linkTo(box0.start) 2004 end.linkTo(box0.end) 2005 horizontalBias = 0f 2006 2007 top.linkTo(box0.bottom) 2008 } 2009 .testTag("box1") 2010 .background(Color.Blue) 2011 ) 2012 } 2013 } 2014 rule.waitForIdle() 2015 2016 rule.onNodeWithTag("box0").apply { 2017 assertPositionInRootIsEqualTo(0.dp, 0.dp) 2018 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2019 } 2020 rule.onNodeWithTag("box1").apply { 2021 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 2022 assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2023 } 2024 2025 box0WidthMultiplier.value = 3 2026 rule.waitForIdle() 2027 2028 rule.onNodeWithTag("box0").apply { 2029 assertPositionInRootIsEqualTo(0.dp, 0.dp) 2030 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2031 } 2032 rule.onNodeWithTag("box1").apply { 2033 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 2034 assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2035 } 2036 2037 box0WidthMultiplier.value = 1 2038 rule.waitForIdle() 2039 2040 rule.onNodeWithTag("box0").apply { 2041 assertPositionInRootIsEqualTo(0.dp, 0.dp) 2042 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2043 } 2044 rule.onNodeWithTag("box1").apply { 2045 assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp()) 2046 assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp() 2047 } 2048 2049 assertEquals(1, constraintLayoutCompCount) 2050 } 2051 2052 @Test 2053 fun testBaselineConstraints() = 2054 with(rule.density) { 2055 fun Modifier.withBaseline() = 2056 this.layout { measurable, constraints -> 2057 val placeable = measurable.measure(constraints) 2058 val halfHeight = (placeable.height / 2f).roundToInt() 2059 layout( 2060 width = placeable.width, 2061 height = placeable.height, 2062 alignmentLines = mapOf(FirstBaseline to halfHeight) 2063 ) { 2064 placeable.place(0, 0) 2065 } 2066 } 2067 2068 val boxSize = 10 2069 val box1Margin = 13 2070 val box2Margin = -7f 2071 2072 var box1Position = IntOffset.Zero 2073 var box2Position = IntOffset.Zero 2074 rule.setContent { 2075 ConstraintLayout { 2076 val (box1, box2) = createRefs() 2077 Box( 2078 Modifier.size(boxSize.toDp()) 2079 .withBaseline() 2080 .constrainAs(box1) { 2081 baseline.linkTo(parent.top, box1Margin.toDp()) 2082 start.linkTo(parent.start) 2083 } 2084 .onGloballyPositioned { box1Position = it.positionInRoot().round() } 2085 ) 2086 Box( 2087 Modifier.size(boxSize.toDp()) 2088 .withBaseline() 2089 .constrainAs(box2) { top.linkTo(box1.baseline, box2Margin.toDp()) } 2090 .onGloballyPositioned { box2Position = it.positionInRoot().round() } 2091 ) 2092 } 2093 } 2094 val expectedBox1Y = box1Margin - (boxSize * 0.5f).roundToInt() 2095 val expectedBox2Y = expectedBox1Y + box2Margin + (boxSize * 0.5f).toInt() 2096 rule.runOnIdle { 2097 assertEquals(IntOffset(0, expectedBox1Y), box1Position) 2098 assertEquals(IntOffset(0, expectedBox2Y.roundToInt()), box2Position) 2099 } 2100 } 2101 2102 @Test 2103 fun testConstraintLayout_withParentIntrinsics() = 2104 with(rule.density) { 2105 val rootBoxWidth = 200 2106 val box1Size = 40 2107 val box2Size = 70 2108 2109 var rootSize = IntSize.Zero 2110 var clSize = IntSize.Zero 2111 var box1Position = IntOffset.Zero 2112 var box2Position = IntOffset.Zero 2113 2114 rule.setContent { 2115 Box( 2116 modifier = 2117 Modifier.width(rootBoxWidth.toDp()) 2118 .height(IntrinsicSize.Max) 2119 .background(Color.LightGray) 2120 .onGloballyPositioned { rootSize = it.size } 2121 ) { 2122 ConstraintLayout( 2123 modifier = 2124 Modifier.fillMaxWidth() 2125 .wrapContentHeight() 2126 .background(Color.Yellow) 2127 .onGloballyPositioned { clSize = it.size } 2128 ) { 2129 val (one, two) = createRefs() 2130 val horChain = 2131 createHorizontalChain(one, two, chainStyle = ChainStyle.Packed(0f)) 2132 constrain(horChain) { 2133 start.linkTo(parent.start) 2134 end.linkTo(parent.end) 2135 } 2136 Box( 2137 Modifier.size(box1Size.toDp()) 2138 .background(Color.Green) 2139 .constrainAs(one) { 2140 top.linkTo(parent.top) 2141 bottom.linkTo(parent.bottom) 2142 } 2143 .onGloballyPositioned { box1Position = it.positionInRoot().round() } 2144 ) 2145 Box( 2146 Modifier.size(box2Size.toDp()) 2147 .background(Color.Red) 2148 .constrainAs(two) { 2149 width = Dimension.preferredWrapContent 2150 top.linkTo(parent.top) 2151 bottom.linkTo(parent.bottom) 2152 } 2153 .onGloballyPositioned { box2Position = it.positionInRoot().round() } 2154 ) 2155 } 2156 } 2157 } 2158 2159 val expectedSize = IntSize(rootBoxWidth, box2Size) 2160 val expectedBox1Y = ((box2Size / 2f) - (box1Size / 2f)).roundToInt() 2161 rule.runOnIdle { 2162 assertEquals(expectedSize, rootSize) 2163 assertEquals(expectedSize, clSize) 2164 assertEquals(IntOffset(0, expectedBox1Y), box1Position) 2165 assertEquals(IntOffset(box1Size, 0), box2Position) 2166 } 2167 } 2168 2169 @Test 2170 fun testTranslationXY_withDsl() = 2171 with(rule.density) { 2172 val rootSizePx = 100 2173 val boxSizePx = 10 2174 val translationXPx = 7 2175 val translationYPx = 9 2176 2177 var position = IntOffset.Zero 2178 2179 rule.setContent { 2180 ConstraintLayout(Modifier.size(rootSizePx.toDp())) { 2181 val boxRef = createRef() 2182 Box( 2183 Modifier.constrainAs(boxRef) { 2184 width = boxSizePx.toDp().asDimension() 2185 height = boxSizePx.toDp().asDimension() 2186 centerTo(parent) 2187 2188 translationX = translationXPx.toDp() 2189 translationY = translationYPx.toDp() 2190 } 2191 .onPlaced { 2192 // TODO: Figure out a way to test `translationZ` i.e.: 2193 // `shadowElevation` 2194 position = it.boundsInParent().topLeft.round() 2195 } 2196 ) 2197 } 2198 } 2199 2200 rule.runOnIdle { 2201 assertEquals( 2202 Offset( 2203 (rootSizePx - boxSizePx) / 2f + translationXPx, 2204 (rootSizePx - boxSizePx) / 2f + translationYPx, 2205 ) 2206 .round(), 2207 position 2208 ) 2209 } 2210 } 2211 2212 @Test 2213 fun testTranslationXY_withJson() = 2214 with(rule.density) { 2215 val rootSizePx = 100 2216 val boxSizePx = 10 2217 val translationXPx = 7 2218 val translationYPx = 9 2219 2220 var position = IntOffset.Zero 2221 2222 rule.setContent { 2223 ConstraintLayout( 2224 constraintSet = 2225 ConstraintSet( 2226 """ 2227 { 2228 box: { 2229 width: ${boxSizePx.toDp().value}, 2230 height: ${boxSizePx.toDp().value}, 2231 center: 'parent', 2232 translationX: ${translationXPx.toDp().value}, 2233 translationY: ${translationYPx.toDp().value} 2234 } 2235 } 2236 """ 2237 .trimIndent() 2238 ), 2239 Modifier.size(rootSizePx.toDp()) 2240 ) { 2241 Box( 2242 Modifier.layoutId("box").onPlaced { 2243 // TODO: Figure out a way to test `translationZ` i.e.: `shadowElevation` 2244 position = it.boundsInParent().topLeft.round() 2245 } 2246 ) 2247 } 2248 } 2249 2250 rule.runOnIdle { 2251 assertEquals( 2252 Offset( 2253 (rootSizePx - boxSizePx) / 2f + translationXPx, 2254 (rootSizePx - boxSizePx) / 2f + translationYPx, 2255 ) 2256 .round(), 2257 position 2258 ) 2259 } 2260 } 2261 2262 // Required for bitmap evaluation. 2263 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) 2264 @Test 2265 fun testVisibility_withInlineDsl() = 2266 with(rule.density) { 2267 val rootSizePx = 100 2268 val boxSizePx = 10 2269 var box1Position = IntOffset.Zero 2270 val expectedInitialBox1Position = 2271 Offset((rootSizePx + boxSizePx) / 2f, (rootSizePx - boxSizePx) / 2f).round() 2272 2273 val boxVisibility = mutableStateOf(Visibility.Visible) 2274 2275 rule.setContent { 2276 ConstraintLayout(Modifier.size(rootSizePx.toDp())) { 2277 val (box0, box1) = createRefs() 2278 2279 Box( 2280 Modifier.testTag("box0") 2281 .constrainAs(box0) { 2282 centerTo(parent) 2283 visibility = boxVisibility.value 2284 } 2285 .background(Color.Red) 2286 ) { 2287 Box(Modifier.size(boxSizePx.toDp())) 2288 } 2289 Box( 2290 Modifier.testTag("box1") 2291 .constrainAs(box1) { 2292 width = boxSizePx.toDp().asDimension() 2293 height = boxSizePx.toDp().asDimension() 2294 2295 top.linkTo(box0.top) 2296 start.linkTo(box0.end) 2297 } 2298 .background(Color.Blue) 2299 .onGloballyPositioned { box1Position = it.positionInParent().round() } 2300 ) 2301 } 2302 } 2303 rule.waitForIdle() 2304 2305 var color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5) 2306 assertEquals(Color.Red.toArgb(), color.toArgb()) 2307 assertEquals(expectedInitialBox1Position, box1Position) 2308 2309 boxVisibility.value = Visibility.Invisible 2310 rule.waitForIdle() 2311 2312 color = rule.onNodeWithTag("box0").captureToImage().asAndroidBitmap().getColor(5, 5) 2313 assertNotEquals(Color.Red.toArgb(), color.toArgb()) 2314 assertEquals(expectedInitialBox1Position, box1Position) 2315 2316 boxVisibility.value = Visibility.Gone 2317 rule.waitForIdle() 2318 2319 // Dp.Unspecified since Gone Composables are not placed 2320 rule.onNodeWithTag("box0").assertWidthIsEqualTo(Dp.Unspecified) 2321 rule.onNodeWithTag("box0").assertHeightIsEqualTo(Dp.Unspecified) 2322 assertEquals(Offset(rootSizePx / 2f, rootSizePx / 2f).round(), box1Position) 2323 } 2324 2325 @Test 2326 fun testAnimateChanges_withInlineDsl() = 2327 with(rule.density) { 2328 val durationMs = 200 2329 val rootSizePx = 100 2330 val boxSizePx = 20 2331 val expectedEndPosition = IntOffset(rootSizePx - boxSizePx, rootSizePx - boxSizePx) 2332 var box0Position = IntOffset.Zero 2333 val atTopLeftCorner = mutableStateOf(true) 2334 2335 rule.setContent { 2336 ConstraintLayout( 2337 modifier = Modifier.size(rootSizePx.toDp()), 2338 animateChangesSpec = tween(durationMs) 2339 ) { 2340 val boxRef = createRef() 2341 Box( 2342 Modifier.background(Color.Red) 2343 .constrainAs(boxRef) { 2344 width = boxSizePx.toDp().asDimension() 2345 height = boxSizePx.toDp().asDimension() 2346 if (atTopLeftCorner.value) { 2347 top.linkTo(parent.top) 2348 start.linkTo(parent.start) 2349 } else { 2350 bottom.linkTo(parent.bottom) 2351 end.linkTo(parent.end) 2352 } 2353 } 2354 .onGloballyPositioned { box0Position = it.positionInParent().round() } 2355 ) 2356 } 2357 } 2358 rule.waitForIdle() 2359 assertEquals(IntOffset.Zero, box0Position) 2360 2361 rule.mainClock.autoAdvance = false 2362 atTopLeftCorner.value = false 2363 2364 rule.mainClock.advanceTimeBy(durationMs / 2L) 2365 rule.waitForIdle() 2366 assertTrue(box0Position.x > 0 && box0Position.y > 0) 2367 assertTrue( 2368 box0Position.x < expectedEndPosition.x && box0Position.y < expectedEndPosition.y 2369 ) 2370 2371 rule.mainClock.autoAdvance = true 2372 rule.waitForIdle() 2373 assertEquals(expectedEndPosition, box0Position) 2374 } 2375 2376 @Test 2377 fun testEmptyConstraintLayoutSize() = 2378 with(rule.density) { 2379 val rootSizePx = 200f 2380 2381 // Mutable state to trigger invalidations. By default simply passes original 2382 // constraints. 2383 // But we'll use this to test empty ConstraintLayout under different constraints 2384 var transformConstraints: ((Constraints) -> Constraints) by 2385 mutableStateOf({ constraints -> constraints }) 2386 2387 // To capture measured ConstraintLayout size 2388 var layoutSize = IntSize(-1, -1) 2389 2390 rule.setContent { 2391 Column(Modifier.size(rootSizePx.toDp()).verticalScroll(rememberScrollState())) { 2392 ConstraintLayout( 2393 modifier = 2394 Modifier.layout { measurable, constraints -> 2395 // Measure policy to test ConstraintLayout under different 2396 // constraints 2397 val placeable = 2398 measurable.measure(transformConstraints(constraints)) 2399 layout(placeable.width, placeable.height) { 2400 placeable.place(0, 0) 2401 } 2402 } 2403 .onGloballyPositioned { layoutSize = it.size } 2404 ) { 2405 // Empty content 2406 } 2407 } 2408 } 2409 // For this case, the default behavior should be a ConstraintLayout of size 0x0 2410 rule.waitForIdle() 2411 assertEquals(IntSize.Zero, layoutSize) 2412 2413 // Test with min constraints 2414 transformConstraints = { constraints -> 2415 // Demonstrate that vertical scroll constraints propagate 2416 assert(constraints.maxHeight == Constraints.Infinity) 2417 constraints.copy(minWidth = 123, minHeight = 321) 2418 } 2419 rule.waitForIdle() 2420 2421 // Minimum size is preferred for empty layouts. Should not crash :) 2422 assertEquals(IntSize(width = 123, height = 321), layoutSize) 2423 2424 // Transform to an equivalent of fillMaxSize(), which fills bounded constraints only 2425 transformConstraints = { constraints -> 2426 val minWidth: Int 2427 val maxWidth: Int 2428 val minHeight: Int 2429 val maxHeight: Int 2430 2431 if (constraints.hasBoundedWidth) { 2432 minWidth = constraints.maxWidth 2433 maxWidth = constraints.maxWidth 2434 } else { 2435 minWidth = constraints.minWidth 2436 maxWidth = constraints.maxWidth 2437 } 2438 if (constraints.hasBoundedHeight) { 2439 minHeight = constraints.maxHeight 2440 maxHeight = constraints.maxHeight 2441 } else { 2442 minHeight = constraints.minHeight 2443 maxHeight = constraints.maxHeight 2444 } 2445 Constraints( 2446 minWidth = minWidth, 2447 maxWidth = maxWidth, 2448 minHeight = minHeight, 2449 maxHeight = maxHeight 2450 ) 2451 } 2452 rule.waitForIdle() 2453 2454 // Vertical is infinity, fillMaxSize behavior should pin it to minimum height (Zero) 2455 assertEquals(IntSize(rootSizePx.fastRoundToInt(), 0), layoutSize) 2456 } 2457 2458 @Test 2459 fun testToggleVisibilityWithFillConstraintsWidth() = 2460 with(rule.density) { 2461 val rootSizePx = 100f 2462 2463 var toggleVisibility by mutableStateOf(false) 2464 2465 rule.setContent { 2466 // Regression test, modify only dimensions if necessary 2467 ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) { 2468 val (titleRef, detailRef) = createRefs() 2469 Box( 2470 modifier = 2471 Modifier.background(Color.Cyan).testTag("box1").constrainAs(detailRef) { 2472 centerHorizontallyTo(parent) 2473 2474 width = Dimension.fillToConstraints 2475 height = rootSizePx.toDp().asDimension() 2476 2477 visibility = 2478 if (!toggleVisibility) Visibility.Gone else Visibility.Visible 2479 } 2480 ) 2481 Box( 2482 modifier = 2483 Modifier.background(Color.Red).testTag("box2").constrainAs(titleRef) { 2484 centerHorizontallyTo(parent) 2485 2486 width = Dimension.fillToConstraints 2487 height = rootSizePx.toDp().asDimension() 2488 2489 visibility = 2490 if (toggleVisibility) Visibility.Gone else Visibility.Visible 2491 } 2492 ) 2493 } 2494 } 2495 rule.waitForIdle() 2496 2497 rule.onNodeWithTag("box1").apply { 2498 assertWidthIsEqualTo(Dp.Unspecified) 2499 assertHeightIsEqualTo(Dp.Unspecified) 2500 } 2501 rule.onNodeWithTag("box2").apply { 2502 assertWidthIsEqualTo(rootSizePx.toDp()) 2503 assertHeightIsEqualTo(rootSizePx.toDp()) 2504 } 2505 2506 toggleVisibility = !toggleVisibility 2507 rule.waitForIdle() 2508 2509 rule.onNodeWithTag("box1").apply { 2510 assertWidthIsEqualTo(rootSizePx.toDp()) 2511 assertHeightIsEqualTo(rootSizePx.toDp()) 2512 } 2513 rule.onNodeWithTag("box2").apply { 2514 assertWidthIsEqualTo(Dp.Unspecified) 2515 assertHeightIsEqualTo(Dp.Unspecified) 2516 } 2517 Unit // Test expects to return Unit 2518 } 2519 2520 @Test 2521 fun testToggleVisibilityWithFillConstraintsWidth_underLookahead() = 2522 with(rule.density) { 2523 val rootSizePx = 100f 2524 2525 var toggleVisibility by mutableStateOf(false) 2526 2527 rule.setContent { 2528 LookaheadScope { 2529 // Regression test, modify only dimensions if necessary 2530 ConstraintLayout(modifier = Modifier.size(rootSizePx.toDp())) { 2531 val (titleRef, detailRef) = createRefs() 2532 Box( 2533 modifier = 2534 Modifier.background(Color.Cyan).testTag("box1").constrainAs( 2535 detailRef 2536 ) { 2537 centerHorizontallyTo(parent) 2538 2539 width = Dimension.fillToConstraints 2540 height = rootSizePx.toDp().asDimension() 2541 2542 visibility = 2543 if (!toggleVisibility) Visibility.Gone 2544 else Visibility.Visible 2545 } 2546 ) 2547 Box( 2548 modifier = 2549 Modifier.background(Color.Red).testTag("box2").constrainAs( 2550 titleRef 2551 ) { 2552 centerHorizontallyTo(parent) 2553 2554 width = Dimension.fillToConstraints 2555 height = rootSizePx.toDp().asDimension() 2556 2557 visibility = 2558 if (toggleVisibility) Visibility.Gone 2559 else Visibility.Visible 2560 } 2561 ) 2562 } 2563 } 2564 } 2565 rule.waitForIdle() 2566 2567 rule.onNodeWithTag("box1").apply { 2568 assertWidthIsEqualTo(Dp.Unspecified) 2569 assertHeightIsEqualTo(Dp.Unspecified) 2570 } 2571 rule.onNodeWithTag("box2").apply { 2572 assertWidthIsEqualTo(rootSizePx.toDp()) 2573 assertHeightIsEqualTo(rootSizePx.toDp()) 2574 } 2575 2576 toggleVisibility = !toggleVisibility 2577 rule.waitForIdle() 2578 2579 rule.onNodeWithTag("box1").apply { 2580 assertWidthIsEqualTo(rootSizePx.toDp()) 2581 assertHeightIsEqualTo(rootSizePx.toDp()) 2582 } 2583 rule.onNodeWithTag("box2").apply { 2584 assertWidthIsEqualTo(Dp.Unspecified) 2585 assertHeightIsEqualTo(Dp.Unspecified) 2586 } 2587 Unit // Test expects to return Unit 2588 } 2589 2590 /** 2591 * Provides a list constraints combination for horizontal anchors: `start`, `end`, 2592 * `absoluteLeft`, `absoluteRight`. 2593 */ 2594 private fun listAnchors(box: ConstrainedLayoutReference): List<ConstrainScope.() -> Unit> = 2595 listOf( 2596 { start.linkTo(box.start) }, 2597 { absoluteLeft.linkTo(box.start) }, 2598 { start.linkTo(box.absoluteLeft) }, 2599 { absoluteLeft.linkTo(box.absoluteLeft) }, 2600 { end.linkTo(box.start) }, 2601 { absoluteRight.linkTo(box.start) }, 2602 { end.linkTo(box.absoluteLeft) }, 2603 { absoluteRight.linkTo(box.absoluteLeft) }, 2604 { start.linkTo(box.end) }, 2605 { absoluteLeft.linkTo(box.end) }, 2606 { start.linkTo(box.absoluteRight) }, 2607 { absoluteLeft.linkTo(box.absoluteRight) }, 2608 { end.linkTo(box.end) }, 2609 { absoluteRight.linkTo(box.end) }, 2610 { end.linkTo(box.absoluteRight) }, 2611 { absoluteRight.linkTo(box.absoluteRight) }, 2612 ) 2613 2614 private fun getJsonAnchorsContent(guidelineOffset: Float): String = 2615 // language=json5 2616 """ 2617 { 2618 g1: { type: 'vGuideline', left: $guidelineOffset }, 2619 box: { left: ['g1', 'left', 0] }, 2620 box0: { start: ['box','start',0] }, 2621 box1: { left: ['box','start',0] }, 2622 box2: { start: ['box','left',0] }, 2623 box3: { left: ['box','left',0] }, 2624 box4: { end: ['box','start',0] }, 2625 box5: { right: ['box','start',0] }, 2626 box6: { end: ['box','left',0] }, 2627 box7: { right: ['box','left',0] }, 2628 box8: { start: ['box','end',0] }, 2629 box9: { left: ['box','end',0] }, 2630 box10: { start: ['box','right',0] }, 2631 box11: { left: ['box','right',0] }, 2632 box12: { end: ['box','end',0] }, 2633 box13: { right: ['box','end',0] }, 2634 box14: { end: ['box','right',0] }, 2635 box15: { right: ['box','right',0] } 2636 } 2637 """ 2638 .trimIndent() 2639 2640 private fun assertAnchorsLtrPositions(position: Array<Float>) { 2641 rule.runOnIdle { 2642 assertEquals(16, position.size) 2643 assertEquals(50f, position[0]) 2644 assertEquals(50f, position[1]) 2645 assertEquals(50f, position[2]) 2646 assertEquals(50f, position[3]) 2647 assertEquals(49f, position[4]) 2648 assertEquals(49f, position[5]) 2649 assertEquals(49f, position[6]) 2650 assertEquals(49f, position[7]) 2651 assertEquals(51f, position[8]) 2652 assertEquals(51f, position[9]) 2653 assertEquals(51f, position[10]) 2654 assertEquals(51f, position[11]) 2655 assertEquals(50f, position[12]) 2656 assertEquals(50f, position[13]) 2657 assertEquals(50f, position[14]) 2658 assertEquals(50f, position[15]) 2659 } 2660 } 2661 2662 private fun assertAnchorsRtlPositions(position: Array<Float>) { 2663 rule.runOnIdle { 2664 assertEquals(16, position.size) 2665 assertEquals(50f, position[0]) 2666 assertEquals(51f, position[1]) 2667 assertEquals(49f, position[2]) 2668 assertEquals(50f, position[3]) 2669 assertEquals(51f, position[4]) 2670 assertEquals(50f, position[5]) 2671 assertEquals(50f, position[6]) 2672 assertEquals(49f, position[7]) 2673 assertEquals(49f, position[8]) 2674 assertEquals(50f, position[9]) 2675 assertEquals(50f, position[10]) 2676 assertEquals(51f, position[11]) 2677 assertEquals(50f, position[12]) 2678 assertEquals(49f, position[13]) 2679 assertEquals(51f, position[14]) 2680 assertEquals(50f, position[15]) 2681 } 2682 } 2683 2684 private fun getJsonGuidelinesContent(guidelineOffset: Float): String = 2685 // language=json5 2686 """ 2687 { 2688 g0: { type: 'vGuideline', start: $guidelineOffset }, 2689 g1: { type: 'vGuideline', left: $guidelineOffset }, 2690 g2: { type: 'vGuideline', end: $guidelineOffset }, 2691 g3: { type: 'vGuideline', right: $guidelineOffset }, 2692 g4: { type: 'vGuideline', percent: ["start", 0.25] }, 2693 g5: { type: 'vGuideline', percent: ["left", 0.25] }, 2694 g6: { type: 'vGuideline', percent: ["end", 0.25] }, 2695 g7: { type: 'vGuideline', percent: ["right", 0.25] }, 2696 box0: { left: ['g0', 'start', 0] }, 2697 box1: { left: ['g1', 'start', 0] }, 2698 box2: { left: ['g2', 'start', 0] }, 2699 box3: { left: ['g3', 'start', 0] }, 2700 box4: { left: ['g4', 'start', 0] }, 2701 box5: { left: ['g5', 'start', 0] }, 2702 box6: { left: ['g6', 'start', 0] }, 2703 box7: { left: ['g7', 'start', 0] } 2704 } 2705 """ 2706 .trimIndent() 2707 2708 private fun assertGuidelinesLtrPositions(position: Array<Float>) { 2709 rule.runOnIdle { 2710 assertEquals(8, position.size) 2711 assertEquals(50f, position[0]) 2712 assertEquals(50f, position[1]) 2713 assertEquals(150f, position[2]) 2714 assertEquals(150f, position[3]) 2715 assertEquals(50f, position[4]) 2716 assertEquals(50f, position[5]) 2717 assertEquals(150f, position[6]) 2718 assertEquals(150f, position[7]) 2719 } 2720 } 2721 2722 private fun assertGuidelinesRtlPositions(position: Array<Float>) { 2723 rule.runOnIdle { 2724 assertEquals(8, position.size) 2725 assertEquals(150f, position[0]) 2726 assertEquals(50f, position[1]) 2727 assertEquals(50f, position[2]) 2728 assertEquals(150f, position[3]) 2729 assertEquals(150f, position[4]) 2730 assertEquals(50f, position[5]) 2731 assertEquals(50f, position[6]) 2732 assertEquals(150f, position[7]) 2733 } 2734 } 2735 2736 private fun getJsonBarriersContent(guidelineOffset: Float): String = 2737 // language=json5 2738 """ 2739 { 2740 g0: { type: 'vGuideline', left: $guidelineOffset }, 2741 g1: { type: 'vGuideline', right: $guidelineOffset }, 2742 2743 boxA: { left: ['g0', 'start', 0] }, 2744 boxB: { left: ['g1', 'start', 0] }, 2745 2746 b0: { type: 'barrier', direction: 'start', contains: ['boxA','boxB'] }, 2747 b1: { type: 'barrier', direction: 'left', contains: ['boxA','boxB'] }, 2748 b2: { type: 'barrier', direction: 'end', contains: ['boxA','boxB'] }, 2749 b3: { type: 'barrier', direction: 'right', contains: ['boxA','boxB'] }, 2750 2751 box0: { left: ['b0', 'start', 0] }, 2752 box1: { left: ['b1', 'start', 0] }, 2753 box2: { left: ['b2', 'start', 0] }, 2754 box3: { left: ['b3', 'start', 0] }, 2755 } 2756 """ 2757 .trimIndent() 2758 2759 private fun assertBarriersLtrPositions(position: Array<Float>) { 2760 rule.runOnIdle { 2761 assertEquals(4, position.size) 2762 assertEquals(50f, position[0]) 2763 assertEquals(50f, position[1]) 2764 assertEquals(151f, position[2]) 2765 assertEquals(151f, position[3]) 2766 } 2767 } 2768 2769 private fun assertBarriersRtlPositions(position: Array<Float>) { 2770 rule.runOnIdle { 2771 assertEquals(4, position.size) 2772 assertEquals(151f, position[0]) 2773 assertEquals(50f, position[1]) 2774 assertEquals(50f, position[2]) 2775 assertEquals(151f, position[3]) 2776 } 2777 } 2778 } 2779