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.compose.material 18 19 import android.os.Build 20 import androidx.activity.ComponentActivity 21 import androidx.compose.animation.AnimatedVisibility 22 import androidx.compose.foundation.background 23 import androidx.compose.foundation.layout.Box 24 import androidx.compose.foundation.layout.PaddingValues 25 import androidx.compose.foundation.layout.WindowInsets 26 import androidx.compose.foundation.layout.fillMaxSize 27 import androidx.compose.foundation.layout.fillMaxWidth 28 import androidx.compose.foundation.layout.height 29 import androidx.compose.foundation.layout.padding 30 import androidx.compose.foundation.layout.requiredSize 31 import androidx.compose.foundation.layout.size 32 import androidx.compose.foundation.layout.windowInsetsPadding 33 import androidx.compose.material.icons.Icons 34 import androidx.compose.material.icons.filled.Favorite 35 import androidx.compose.runtime.Composable 36 import androidx.compose.runtime.MutableState 37 import androidx.compose.runtime.mutableStateOf 38 import androidx.compose.runtime.remember 39 import androidx.compose.testutils.LayeredComposeTestCase 40 import androidx.compose.testutils.ToggleableTestCase 41 import androidx.compose.testutils.assertNoPendingChanges 42 import androidx.compose.testutils.doFramesUntilNoChangesPending 43 import androidx.compose.testutils.forGivenTestCase 44 import androidx.compose.ui.Modifier 45 import androidx.compose.ui.draw.shadow 46 import androidx.compose.ui.geometry.Offset 47 import androidx.compose.ui.graphics.Color 48 import androidx.compose.ui.graphics.asAndroidBitmap 49 import androidx.compose.ui.layout.LayoutCoordinates 50 import androidx.compose.ui.layout.LookaheadScope 51 import androidx.compose.ui.layout.SubcomposeLayout 52 import androidx.compose.ui.layout.onGloballyPositioned 53 import androidx.compose.ui.layout.onSizeChanged 54 import androidx.compose.ui.layout.positionInParent 55 import androidx.compose.ui.layout.positionInRoot 56 import androidx.compose.ui.platform.LocalDensity 57 import androidx.compose.ui.platform.testTag 58 import androidx.compose.ui.semantics.semantics 59 import androidx.compose.ui.test.assertHeightIsEqualTo 60 import androidx.compose.ui.test.assertIsDisplayed 61 import androidx.compose.ui.test.assertWidthIsEqualTo 62 import androidx.compose.ui.test.captureToImage 63 import androidx.compose.ui.test.junit4.createAndroidComposeRule 64 import androidx.compose.ui.test.onNodeWithTag 65 import androidx.compose.ui.test.performTouchInput 66 import androidx.compose.ui.test.swipeLeft 67 import androidx.compose.ui.test.swipeRight 68 import androidx.compose.ui.unit.Density 69 import androidx.compose.ui.unit.Dp 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.toSize 74 import androidx.compose.ui.zIndex 75 import androidx.test.ext.junit.runners.AndroidJUnit4 76 import androidx.test.filters.MediumTest 77 import androidx.test.filters.SdkSuppress 78 import com.google.common.truth.Truth.assertThat 79 import com.google.common.truth.Truth.assertWithMessage 80 import kotlin.math.roundToInt 81 import kotlinx.coroutines.runBlocking 82 import org.junit.Assert.assertEquals 83 import org.junit.Ignore 84 import org.junit.Rule 85 import org.junit.Test 86 import org.junit.runner.RunWith 87 88 @MediumTest 89 @RunWith(AndroidJUnit4::class) 90 class ScaffoldTest { 91 92 @get:Rule val rule = createAndroidComposeRule<ComponentActivity>() 93 94 private val fabSpacing = 16.dp 95 private val scaffoldTag = "Scaffold" 96 97 @Test 98 fun scaffold_onlyContent_takesWholeScreen() { 99 rule 100 .setMaterialContentForSizeAssertions( 101 parentMaxWidth = 100.dp, 102 parentMaxHeight = 100.dp 103 ) { 104 Scaffold { Text("Scaffold body") } 105 } 106 .assertWidthIsEqualTo(100.dp) 107 .assertHeightIsEqualTo(100.dp) 108 } 109 110 @Test 111 fun scaffold_onlyContent_stackSlot() { 112 var child1: Offset = Offset.Zero 113 var child2: Offset = Offset.Zero 114 rule.setMaterialContent { 115 Scaffold { 116 Text("One", Modifier.onGloballyPositioned { child1 = it.positionInParent() }) 117 Text("Two", Modifier.onGloballyPositioned { child2 = it.positionInParent() }) 118 } 119 } 120 assertThat(child1.y).isEqualTo(child2.y) 121 assertThat(child1.x).isEqualTo(child2.x) 122 } 123 124 @Test 125 fun scaffold_AppbarAndContent_inColumn() { 126 var appbarPosition: Offset = Offset.Zero 127 var appbarSize: IntSize = IntSize.Zero 128 var contentPosition: Offset = Offset.Zero 129 rule.setMaterialContent { 130 Scaffold( 131 topBar = { 132 Box( 133 Modifier.fillMaxWidth() 134 .height(50.dp) 135 .background(color = Color.Red) 136 .onGloballyPositioned { positioned: LayoutCoordinates -> 137 appbarPosition = positioned.localToWindow(Offset.Zero) 138 appbarSize = positioned.size 139 } 140 ) 141 } 142 ) { 143 Box( 144 Modifier.fillMaxWidth() 145 .height(50.dp) 146 .background(Color.Blue) 147 .onGloballyPositioned { contentPosition = it.localToWindow(Offset.Zero) } 148 ) 149 } 150 } 151 assertThat(appbarPosition.y + appbarSize.height.toFloat()).isEqualTo(contentPosition.y) 152 } 153 154 @Test 155 fun scaffold_bottomBarAndContent_inStack() { 156 var appbarPosition: Offset = Offset.Zero 157 var appbarSize: IntSize = IntSize.Zero 158 var contentPosition: Offset = Offset.Zero 159 var contentSize: IntSize = IntSize.Zero 160 rule.setMaterialContent { 161 Scaffold( 162 bottomBar = { 163 Box( 164 Modifier.fillMaxWidth() 165 .height(50.dp) 166 .background(color = Color.Red) 167 .onGloballyPositioned { positioned: LayoutCoordinates -> 168 appbarPosition = positioned.positionInParent() 169 appbarSize = positioned.size 170 } 171 ) 172 } 173 ) { 174 Box( 175 Modifier.fillMaxSize() 176 .height(50.dp) 177 .background(color = Color.Blue) 178 .onGloballyPositioned { positioned: LayoutCoordinates -> 179 contentPosition = positioned.positionInParent() 180 contentSize = positioned.size 181 } 182 ) 183 } 184 } 185 val appBarBottom = appbarPosition.y + appbarSize.height 186 val contentBottom = contentPosition.y + contentSize.height 187 assertThat(appBarBottom).isEqualTo(contentBottom) 188 } 189 190 @Test 191 @Ignore("unignore once animation sync is ready (b/147291885)") 192 fun scaffold_drawer_gestures() { 193 var drawerChildPosition: Offset = Offset.Zero 194 val drawerGesturedEnabledState = mutableStateOf(false) 195 rule.setContent { 196 Box(Modifier.testTag(scaffoldTag)) { 197 Scaffold( 198 drawerContent = { 199 Box( 200 Modifier.fillMaxWidth() 201 .height(50.dp) 202 .background(color = Color.Blue) 203 .onGloballyPositioned { positioned: LayoutCoordinates -> 204 drawerChildPosition = positioned.positionInParent() 205 } 206 ) 207 }, 208 drawerGesturesEnabled = drawerGesturedEnabledState.value 209 ) { 210 Box(Modifier.fillMaxWidth().height(50.dp).background(color = Color.Blue)) 211 } 212 } 213 } 214 assertThat(drawerChildPosition.x).isLessThan(0f) 215 rule.onNodeWithTag(scaffoldTag).performTouchInput { swipeRight() } 216 assertThat(drawerChildPosition.x).isLessThan(0f) 217 rule.onNodeWithTag(scaffoldTag).performTouchInput { swipeLeft() } 218 assertThat(drawerChildPosition.x).isLessThan(0f) 219 220 rule.runOnUiThread { drawerGesturedEnabledState.value = true } 221 222 rule.onNodeWithTag(scaffoldTag).performTouchInput { swipeRight() } 223 assertThat(drawerChildPosition.x).isEqualTo(0f) 224 rule.onNodeWithTag(scaffoldTag).performTouchInput { swipeLeft() } 225 assertThat(drawerChildPosition.x).isLessThan(0f) 226 } 227 228 @Test 229 @Ignore("unignore once animation sync is ready (b/147291885)") 230 fun scaffold_drawer_manualControl(): Unit = runBlocking { 231 var drawerChildPosition: Offset = Offset.Zero 232 lateinit var scaffoldState: ScaffoldState 233 rule.setContent { 234 scaffoldState = rememberScaffoldState() 235 Box(Modifier.testTag(scaffoldTag)) { 236 Scaffold( 237 scaffoldState = scaffoldState, 238 drawerContent = { 239 Box( 240 Modifier.fillMaxWidth() 241 .height(50.dp) 242 .background(color = Color.Blue) 243 .onGloballyPositioned { positioned: LayoutCoordinates -> 244 drawerChildPosition = positioned.positionInParent() 245 } 246 ) 247 } 248 ) { 249 Box(Modifier.fillMaxWidth().height(50.dp).background(color = Color.Blue)) 250 } 251 } 252 } 253 assertThat(drawerChildPosition.x).isLessThan(0f) 254 scaffoldState.drawerState.open() 255 assertThat(drawerChildPosition.x).isLessThan(0f) 256 scaffoldState.drawerState.close() 257 assertThat(drawerChildPosition.x).isLessThan(0f) 258 } 259 260 @Test 261 fun scaffold_startDockedFab_position() { 262 var fabPosition: Offset = Offset.Zero 263 var fabSize: IntSize = IntSize.Zero 264 var bottomBarPosition: Offset = Offset.Zero 265 rule.setContent { 266 Scaffold( 267 floatingActionButton = { 268 FloatingActionButton( 269 modifier = 270 Modifier.onGloballyPositioned { positioned -> 271 fabSize = positioned.size 272 fabPosition = positioned.positionInRoot() 273 }, 274 onClick = {} 275 ) { 276 Icon(Icons.Filled.Favorite, null) 277 } 278 }, 279 floatingActionButtonPosition = FabPosition.Start, 280 isFloatingActionButtonDocked = true, 281 bottomBar = { 282 BottomAppBar( 283 Modifier.onGloballyPositioned { positioned: LayoutCoordinates -> 284 bottomBarPosition = positioned.positionInRoot() 285 } 286 ) {} 287 } 288 ) { 289 Text("body") 290 } 291 } 292 with(rule.density) { assertThat(fabPosition.x).isWithin(1f).of(fabSpacing.toPx()) } 293 val expectedFabY = bottomBarPosition.y - (fabSize.height / 2) 294 assertThat(fabPosition.y).isEqualTo(expectedFabY) 295 } 296 297 @Test 298 fun scaffold_centerDockedFab_position() { 299 var fabPosition: Offset = Offset.Zero 300 var fabSize: IntSize = IntSize.Zero 301 var bottomBarPosition: Offset = Offset.Zero 302 rule.setContent { 303 Scaffold( 304 floatingActionButton = { 305 FloatingActionButton( 306 modifier = 307 Modifier.onGloballyPositioned { positioned -> 308 fabSize = positioned.size 309 fabPosition = positioned.positionInRoot() 310 }, 311 onClick = {} 312 ) { 313 Icon(Icons.Filled.Favorite, null) 314 } 315 }, 316 floatingActionButtonPosition = FabPosition.Center, 317 isFloatingActionButtonDocked = true, 318 bottomBar = { 319 BottomAppBar( 320 Modifier.onGloballyPositioned { positioned: LayoutCoordinates -> 321 bottomBarPosition = positioned.positionInRoot() 322 } 323 ) {} 324 } 325 ) { 326 Text("body") 327 } 328 } 329 with(rule.density) { 330 assertThat(fabPosition.x) 331 .isWithin(1f) 332 .of((rule.rootWidth().toPx() - fabSize.width) / 2f) 333 } 334 val expectedFabY = bottomBarPosition.y - (fabSize.height / 2) 335 assertThat(fabPosition.y).isEqualTo(expectedFabY) 336 } 337 338 @Test 339 fun scaffold_endDockedFab_position() { 340 var fabPosition: Offset = Offset.Zero 341 var fabSize: IntSize = IntSize.Zero 342 var bottomBarPosition: Offset = Offset.Zero 343 rule.setContent { 344 Scaffold( 345 floatingActionButton = { 346 FloatingActionButton( 347 modifier = 348 Modifier.onGloballyPositioned { positioned -> 349 fabSize = positioned.size 350 fabPosition = positioned.positionInRoot() 351 }, 352 onClick = {} 353 ) { 354 Icon(Icons.Filled.Favorite, null) 355 } 356 }, 357 floatingActionButtonPosition = FabPosition.End, 358 isFloatingActionButtonDocked = true, 359 bottomBar = { 360 BottomAppBar( 361 Modifier.onGloballyPositioned { positioned: LayoutCoordinates -> 362 bottomBarPosition = positioned.positionInRoot() 363 } 364 ) {} 365 } 366 ) { 367 Text("body") 368 } 369 } 370 with(rule.density) { 371 assertThat(fabPosition.x) 372 .isWithin(1f) 373 .of(rule.rootWidth().toPx() - fabSize.width - fabSpacing.toPx()) 374 } 375 val expectedFabY = bottomBarPosition.y - (fabSize.height / 2) 376 assertThat(fabPosition.y).isEqualTo(expectedFabY) 377 } 378 379 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 380 @Test 381 fun scaffold_topAppBarIsDrawnOnTopOfContent() { 382 rule.setContent { 383 Box( 384 Modifier.requiredSize(10.dp, 20.dp) 385 .semantics(mergeDescendants = true) {} 386 .testTag("Scaffold") 387 ) { 388 Scaffold( 389 topBar = { 390 Box( 391 Modifier.requiredSize(10.dp) 392 .shadow(4.dp) 393 .zIndex(4f) 394 .background(color = Color.White) 395 ) 396 } 397 ) { 398 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 399 } 400 } 401 } 402 403 rule.onNodeWithTag("Scaffold").captureToImage().asAndroidBitmap().apply { 404 // asserts the appbar(top half part) has the shadow 405 val yPos = height / 2 + 2 406 assertThat(Color(getPixel(0, yPos))).isNotEqualTo(Color.White) 407 assertThat(Color(getPixel(width / 2, yPos))).isNotEqualTo(Color.White) 408 assertThat(Color(getPixel(width - 1, yPos))).isNotEqualTo(Color.White) 409 } 410 } 411 412 @Test 413 fun scaffold_geometry_fabSize() { 414 var fabSize: IntSize = IntSize.Zero 415 val showFab = mutableStateOf(true) 416 var fabPlacement: FabPlacement? = null 417 rule.setContent { 418 val fab = 419 @Composable { 420 if (showFab.value) { 421 FloatingActionButton( 422 modifier = 423 Modifier.onGloballyPositioned { positioned -> 424 fabSize = positioned.size 425 }, 426 onClick = {} 427 ) { 428 Icon(Icons.Filled.Favorite, null) 429 } 430 } 431 } 432 Scaffold( 433 floatingActionButton = fab, 434 floatingActionButtonPosition = FabPosition.End, 435 bottomBar = { fabPlacement = LocalFabPlacement.current } 436 ) { 437 Text("body") 438 } 439 } 440 rule.runOnIdle { 441 assertThat(fabPlacement?.width).isEqualTo(fabSize.width) 442 assertThat(fabPlacement?.height).isEqualTo(fabSize.height) 443 showFab.value = false 444 } 445 446 rule.runOnIdle { 447 assertThat(fabPlacement).isEqualTo(null) 448 assertThat(fabPlacement).isEqualTo(null) 449 } 450 } 451 452 @Test 453 fun scaffold_geometry_animated_fabSize() { 454 val fabTestTag = "FAB TAG" 455 lateinit var showFab: MutableState<Boolean> 456 var actualFabSize: IntSize = IntSize.Zero 457 var actualFabPlacement: FabPlacement? = null 458 rule.setContent { 459 showFab = remember { mutableStateOf(true) } 460 val animatedFab = 461 @Composable { 462 AnimatedVisibility(visible = showFab.value) { 463 FloatingActionButton( 464 modifier = 465 Modifier.onGloballyPositioned { positioned -> 466 actualFabSize = positioned.size 467 } 468 .testTag(fabTestTag), 469 onClick = {} 470 ) { 471 Icon(Icons.Filled.Favorite, null) 472 } 473 } 474 } 475 Scaffold( 476 floatingActionButton = animatedFab, 477 floatingActionButtonPosition = FabPosition.End, 478 bottomBar = { actualFabPlacement = LocalFabPlacement.current } 479 ) { 480 Text("body") 481 } 482 } 483 484 val fabNode = rule.onNodeWithTag(fabTestTag) 485 486 fabNode.assertIsDisplayed() 487 488 rule.runOnIdle { 489 assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width) 490 assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height) 491 actualFabSize = IntSize.Zero 492 actualFabPlacement = null 493 showFab.value = false 494 } 495 496 fabNode.assertDoesNotExist() 497 498 rule.runOnIdle { 499 assertThat(actualFabPlacement).isNull() 500 actualFabSize = IntSize.Zero 501 actualFabPlacement = null 502 showFab.value = true 503 } 504 505 fabNode.assertIsDisplayed() 506 507 rule.runOnIdle { 508 assertThat(actualFabPlacement?.width).isEqualTo(actualFabSize.width) 509 assertThat(actualFabPlacement?.height).isEqualTo(actualFabSize.height) 510 } 511 } 512 513 @Test 514 fun scaffold_innerPadding_lambdaParam() { 515 var bottomBarSize: IntSize = IntSize.Zero 516 lateinit var innerPadding: PaddingValues 517 518 lateinit var scaffoldState: ScaffoldState 519 rule.setContent { 520 scaffoldState = rememberScaffoldState() 521 Scaffold( 522 scaffoldState = scaffoldState, 523 bottomBar = { 524 Box( 525 Modifier.fillMaxWidth() 526 .height(100.dp) 527 .background(color = Color.Red) 528 .onGloballyPositioned { positioned: LayoutCoordinates -> 529 bottomBarSize = positioned.size 530 } 531 ) 532 } 533 ) { 534 innerPadding = it 535 Text("body") 536 } 537 } 538 rule.runOnIdle { 539 with(rule.density) { 540 assertThat(innerPadding.calculateBottomPadding()) 541 .isEqualTo(bottomBarSize.toSize().height.toDp()) 542 } 543 } 544 } 545 546 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 547 @Test 548 fun scaffold_respectsConsumedWindowInsets() { 549 rule.setContent { 550 Box( 551 Modifier.requiredSize(10.dp, 40.dp) 552 .windowInsetsPadding(WindowInsets(top = 10.dp, bottom = 10.dp)) 553 ) { 554 Scaffold(contentWindowInsets = WindowInsets(top = 15.dp, bottom = 15.dp)) { 555 paddingValues -> 556 // Consumed windowInsetsPadding is omitted. This replicates behavior from 557 // Modifier.windowInsetsPadding. (15.dp contentPadding - 10.dp consumedPadding) 558 assertDpIsWithinThreshold( 559 actual = paddingValues.calculateTopPadding(), 560 expected = 5.dp, 561 threshold = roundingError 562 ) 563 assertDpIsWithinThreshold( 564 actual = paddingValues.calculateBottomPadding(), 565 expected = 5.dp, 566 threshold = roundingError 567 ) 568 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 569 } 570 } 571 } 572 } 573 574 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 575 @Test 576 fun scaffold_providesInsets_respectsTopAppBar() { 577 rule.setContent { 578 Box(Modifier.requiredSize(10.dp, 40.dp)) { 579 Scaffold( 580 contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp), 581 topBar = { Box(Modifier.requiredSize(0.dp)) } 582 ) { paddingValues -> 583 // top is like the collapsed top app bar (i.e. 0dp) + rounding error 584 assertDpIsWithinThreshold( 585 actual = paddingValues.calculateTopPadding(), 586 expected = 0.dp, 587 threshold = roundingError 588 ) 589 // bottom is like the insets 590 assertDpIsWithinThreshold( 591 actual = paddingValues.calculateBottomPadding(), 592 expected = 3.dp, 593 threshold = roundingError 594 ) 595 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 596 } 597 } 598 } 599 } 600 601 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 602 @Test 603 fun scaffold_providesInsets_respectsBottomAppBar() { 604 rule.setContent { 605 Box(Modifier.requiredSize(10.dp, 40.dp)) { 606 Scaffold( 607 contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp), 608 bottomBar = { Box(Modifier.requiredSize(10.dp)) } 609 ) { paddingValues -> 610 // bottom is like bottom app bar + rounding error 611 assertDpIsWithinThreshold( 612 actual = paddingValues.calculateBottomPadding(), 613 expected = 10.dp, 614 threshold = roundingError 615 ) 616 // top is like the insets 617 assertDpIsWithinThreshold( 618 actual = paddingValues.calculateTopPadding(), 619 expected = 5.dp, 620 threshold = roundingError 621 ) 622 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 623 } 624 } 625 } 626 } 627 628 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 629 @Test 630 fun scaffold_insetsTests_snackbarRespectsInsets() { 631 val hostState = SnackbarHostState() 632 var snackbarSize: IntSize? = null 633 var snackbarPosition: Offset? = null 634 var density: Density? = null 635 rule.setContent { 636 Box(Modifier.requiredSize(10.dp, 40.dp)) { 637 density = LocalDensity.current 638 Scaffold( 639 contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp), 640 snackbarHost = { 641 SnackbarHost( 642 hostState = hostState, 643 modifier = 644 Modifier.onGloballyPositioned { 645 snackbarSize = it.size 646 snackbarPosition = it.positionInRoot() 647 } 648 ) 649 } 650 ) { 651 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 652 } 653 } 654 } 655 val snackbarBottomOffsetDp = 656 with(density!!) { (snackbarPosition!!.y.roundToInt() + snackbarSize!!.height).toDp() } 657 assertThat(rule.rootHeight() - snackbarBottomOffsetDp - 3.dp).isLessThan(1.dp) 658 } 659 660 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) 661 @Test 662 fun scaffold_insetsTests_FabRespectsInsets() { 663 var fabSize: IntSize? = null 664 var fabPosition: Offset? = null 665 var density: Density? = null 666 rule.setContent { 667 Box(Modifier.requiredSize(10.dp, 20.dp)) { 668 density = LocalDensity.current 669 Scaffold( 670 contentWindowInsets = WindowInsets(top = 5.dp, bottom = 3.dp), 671 floatingActionButton = { 672 FloatingActionButton( 673 onClick = {}, 674 modifier = 675 Modifier.onGloballyPositioned { 676 fabSize = it.size 677 fabPosition = it.positionInRoot() 678 } 679 ) { 680 Text("Fab") 681 } 682 }, 683 ) { 684 Box(Modifier.requiredSize(10.dp).background(color = Color.White)) 685 } 686 } 687 } 688 val fabBottomOffsetDp = 689 with(density!!) { (fabPosition!!.y.roundToInt() + fabSize!!.height).toDp() } 690 assertThat(rule.rootHeight() - fabBottomOffsetDp - 3.dp).isLessThan(1.dp) 691 } 692 693 // Regression test for b/295536718 694 @Test 695 fun scaffold_onSizeChanged_calledBeforeLookaheadPlace() { 696 var size: IntSize? = null 697 var onSizeChangedCount = 0 698 var onPlaceCount = 0 699 700 rule.setContent { 701 LookaheadScope { 702 Scaffold { 703 SubcomposeLayout { constraints -> 704 val measurables = 705 subcompose("second") { 706 Box( 707 Modifier.size(45.dp).onSizeChanged { 708 onSizeChangedCount++ 709 size = it 710 } 711 ) 712 } 713 val placeables = measurables.map { it.measure(constraints) } 714 715 layout(constraints.maxWidth, constraints.maxHeight) { 716 onPlaceCount++ 717 assertWithMessage("Expected onSizeChangedCount to be >= 1") 718 .that(onSizeChangedCount) 719 .isAtLeast(1) 720 assertThat(size).isNotNull() 721 placeables.forEach { it.place(0, 0) } 722 } 723 } 724 } 725 } 726 } 727 728 assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1) 729 } 730 731 // Regression test for b/373904168 732 @Test 733 fun scaffold_topBarHeightChanging_noRecompositionInBody() { 734 val testCase = TopBarHeightChangingScaffoldTestCase() 735 rule.forGivenTestCase(testCase).performTestWithEventsControl { 736 doFrame() 737 assertNoPendingChanges() 738 739 assertEquals(1, testCase.tracker.compositions) 740 741 testCase.toggleState() 742 743 doFramesUntilNoChangesPending(maxAmountOfFrames = 1) 744 745 assertEquals(1, testCase.tracker.compositions) 746 } 747 } 748 749 private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) { 750 assertThat(actual.value).isWithin(threshold.value).of(expected.value) 751 } 752 753 private val roundingError = 0.5.dp 754 } 755 756 private class TopBarHeightChangingScaffoldTestCase : LayeredComposeTestCase(), ToggleableTestCase { 757 758 private lateinit var state: MutableState<Dp> 759 760 val tracker = CompositionTracker() 761 762 @Composable MeasuredContentnull763 override fun MeasuredContent() { 764 state = remember { mutableStateOf(0.dp) } 765 val paddingValues = remember { 766 object : PaddingValues { 767 override fun calculateBottomPadding(): Dp = state.value 768 769 override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp 770 771 override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp 772 773 override fun calculateTopPadding(): Dp = 0.dp 774 } 775 } 776 777 Scaffold( 778 topBar = { 779 TopAppBar(title = { Text("Title") }, modifier = Modifier.padding(paddingValues)) 780 }, 781 ) { contentPadding -> 782 tracker.compositions++ 783 Box(Modifier.padding(contentPadding).fillMaxSize()) 784 } 785 } 786 787 @Composable 788 override fun ContentWrappers(content: @Composable () -> Unit) { <lambda>null789 MaterialTheme { content() } 790 } 791 toggleStatenull792 override fun toggleState() { 793 state.value = if (state.value == 0.dp) 10.dp else 0.dp 794 } 795 } 796