1 /* <lambda>null2 * Copyright 2023 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 com.android.compose.animation.scene 18 19 import androidx.annotation.VisibleForTesting 20 import androidx.compose.animation.core.DecayAnimationSpec 21 import androidx.compose.foundation.OverscrollFactory 22 import androidx.compose.foundation.clickable 23 import androidx.compose.foundation.gestures.Orientation 24 import androidx.compose.foundation.interaction.MutableInteractionSource 25 import androidx.compose.foundation.layout.Box 26 import androidx.compose.foundation.layout.BoxScope 27 import androidx.compose.foundation.layout.fillMaxSize 28 import androidx.compose.runtime.Composable 29 import androidx.compose.runtime.Stable 30 import androidx.compose.runtime.key 31 import androidx.compose.runtime.remember 32 import androidx.compose.runtime.snapshots.SnapshotStateMap 33 import androidx.compose.ui.Alignment 34 import androidx.compose.ui.ExperimentalComposeUiApi 35 import androidx.compose.ui.Modifier 36 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 37 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher 38 import androidx.compose.ui.input.nestedscroll.nestedScroll 39 import androidx.compose.ui.layout.ApproachLayoutModifierNode 40 import androidx.compose.ui.layout.ApproachMeasureScope 41 import androidx.compose.ui.layout.LookaheadScope 42 import androidx.compose.ui.layout.Measurable 43 import androidx.compose.ui.layout.MeasureResult 44 import androidx.compose.ui.node.LayoutAwareModifierNode 45 import androidx.compose.ui.node.ModifierNodeElement 46 import androidx.compose.ui.unit.Constraints 47 import androidx.compose.ui.unit.Density 48 import androidx.compose.ui.unit.IntSize 49 import androidx.compose.ui.unit.LayoutDirection 50 import androidx.compose.ui.util.fastAny 51 import androidx.compose.ui.util.fastFirstOrNull 52 import androidx.compose.ui.util.fastForEach 53 import androidx.compose.ui.util.fastForEachReversed 54 import androidx.compose.ui.zIndex 55 import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays 56 import com.android.compose.animation.scene.content.Content 57 import com.android.compose.animation.scene.content.Overlay 58 import com.android.compose.animation.scene.content.Scene 59 import com.android.compose.animation.scene.content.state.TransitionState 60 import com.android.compose.ui.util.lerp 61 import kotlinx.coroutines.CoroutineScope 62 63 /** The type for the content of movable elements. */ 64 internal typealias MovableElementContent = @Composable (@Composable () -> Unit) -> Unit 65 66 internal data class Ancestor( 67 val layoutImpl: SceneTransitionLayoutImpl, 68 69 /** 70 * This is the content in which the corresponding descendant of this ancestor appears in. 71 * 72 * Example: When A is the root and has two scenes SA and SB and SB contains a NestedSTL called 73 * B. Then A is the ancestor of B and inContent is SB. 74 */ 75 val inContent: ContentKey, 76 ) 77 78 @Stable 79 internal class SceneTransitionLayoutImpl( 80 internal val state: MutableSceneTransitionLayoutStateImpl, 81 internal var density: Density, 82 internal var layoutDirection: LayoutDirection, 83 internal var swipeSourceDetector: SwipeSourceDetector, 84 internal var swipeDetector: SwipeDetector, 85 internal var transitionInterceptionThreshold: Float, 86 internal var decayAnimationSpec: DecayAnimationSpec<Float>, 87 builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit, 88 89 /** 90 * The scope that should be used by *animations started by this layout only*, i.e. animations 91 * triggered by gestures set up on this layout in [swipeToScene] or interruption decay 92 * animations. 93 */ 94 internal val animationScope: CoroutineScope, 95 96 /** 97 * Number of pixels a gesture has to travel in the opposite direction to for its intrinsic 98 * direction to change. 99 * 100 * Used to determine the direction of [Transition.gestureContext]. 101 */ 102 internal val directionChangeSlop: Float, 103 104 /** 105 * The map of [Element]s. 106 * 107 * Important: [Element]s from this map should never be accessed during composition because the 108 * Elements are added when the associated Modifier.element() node is attached to the Modifier 109 * tree, i.e. after composition. 110 */ 111 internal val elements: MutableMap<ElementKey, Element> = mutableMapOf(), 112 113 /** 114 * When this STL is a [NestedSceneTransitionLayout], this is a list of [Ancestor]s which 115 * provides a reference to the ancestor STLs and indicates where this STL is composed in within 116 * its ancestors. 117 * 118 * The root STL holds an emptyList. With each nesting level the parent is supposed to add 119 * exactly one scene to the list, therefore the size of this list is equal to the nesting depth 120 * of this STL. 121 * 122 * This is used to enable transformations and shared elements across NestedSTLs. 123 */ 124 internal val ancestors: List<Ancestor> = emptyList(), 125 126 /** Whether elements and scene should be tagged using `Modifier.testTag`. */ 127 internal val implicitTestTags: Boolean = false, 128 lookaheadScope: LookaheadScope? = null, 129 defaultEffectFactory: OverscrollFactory, 130 ) { 131 132 /** 133 * The [LookaheadScope] of this layout, that can be used to compute offsets relative to the 134 * layout. For [NestedSceneTransitionLayout]s this scope is the scope of the root STL, such that 135 * offset computations can be shared among all children. 136 */ 137 private var _lookaheadScope: LookaheadScope? = lookaheadScope 138 internal val lookaheadScope: LookaheadScope 139 get() = _lookaheadScope!! 140 141 /** 142 * The map of [Scene]s. 143 * 144 * TODO(b/317014852): Make this a normal MutableMap instead. 145 */ 146 private val scenes = SnapshotStateMap<SceneKey, Scene>() 147 148 /** 149 * The map of [Overlays]. 150 * 151 * Note: We lazily create this map to avoid instantiation an expensive SnapshotStateMap in the 152 * common case where there is no overlay in this layout. 153 */ 154 private var _overlays: MutableMap<OverlayKey, Overlay>? = null 155 private val overlays 156 get() = _overlays ?: SnapshotStateMap<OverlayKey, Overlay>().also { _overlays = it } 157 158 /** 159 * The map of contents of movable elements. 160 * 161 * Note that given that this map is mutated directly during a composition, it has to be a 162 * [SnapshotStateMap] to make sure that mutations are reverted if composition is cancelled. 163 */ 164 private var _movableContents: SnapshotStateMap<ElementKey, MovableElementContent>? = null 165 val movableContents: SnapshotStateMap<ElementKey, MovableElementContent> 166 get() = 167 _movableContents 168 ?: SnapshotStateMap<ElementKey, MovableElementContent>().also { 169 _movableContents = it 170 } 171 172 /** 173 * The different values of a shared value keyed by a a [ValueKey] and the different elements and 174 * contents it is associated to. 175 */ 176 private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? = 177 null 178 internal val sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>> 179 get() = 180 _sharedValues 181 ?: mutableMapOf<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>().also { 182 _sharedValues = it 183 } 184 185 // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed. 186 internal val horizontalDraggableHandler: DraggableHandler 187 internal val verticalDraggableHandler: DraggableHandler 188 189 internal val elementStateScope = ElementStateScopeImpl(this) 190 internal val propertyTransformationScope = PropertyTransformationScopeImpl(this) 191 private var _userActionDistanceScope: UserActionDistanceScope? = null 192 internal val userActionDistanceScope: UserActionDistanceScope 193 get() = 194 _userActionDistanceScope 195 ?: UserActionDistanceScopeImpl(layoutImpl = this).also { 196 _userActionDistanceScope = it 197 } 198 199 internal var lastSize: IntSize = IntSize.Zero 200 201 /** 202 * An empty [NestedScrollDispatcher] and [NestedScrollConnection]. These are composed above our 203 * [SwipeToSceneElement] modifiers, so that the dispatcher will be used by the nested draggables 204 * to launch fling events, making sure that they are not cancelled unless this whole layout is 205 * removed from composition. 206 */ 207 private val nestedScrollDispatcher = NestedScrollDispatcher() 208 private val nestedScrollConnection = object : NestedScrollConnection {} 209 210 // TODO(b/399825091): Remove this. 211 private var scenesToAlwaysCompose: MutableList<Scene>? = null 212 213 init { 214 updateContents(builder, layoutDirection, defaultEffectFactory) 215 216 // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the 217 // current scene (required for SwipeTransition). 218 horizontalDraggableHandler = 219 DraggableHandler( 220 layoutImpl = this, 221 orientation = Orientation.Horizontal, 222 gestureEffectProvider = { content(it).horizontalEffects.gestureEffect }, 223 ) 224 225 verticalDraggableHandler = 226 DraggableHandler( 227 layoutImpl = this, 228 orientation = Orientation.Vertical, 229 gestureEffectProvider = { content(it).verticalEffects.gestureEffect }, 230 ) 231 232 // Make sure that the state is created on the same thread (most probably the main thread) 233 // than this STLImpl. 234 state.checkThread() 235 } 236 237 private fun sceneOrNull(key: SceneKey): Scene? { 238 return scenes[key] 239 ?: ancestors 240 .fastFirstOrNull { it.layoutImpl.scenes[key] != null } 241 ?.layoutImpl 242 ?.scenes 243 ?.get(key) 244 } 245 246 private fun overlayOrNull(key: OverlayKey): Overlay? { 247 return overlays[key] 248 ?: ancestors 249 .fastFirstOrNull { it.layoutImpl.overlays[key] != null } 250 ?.layoutImpl 251 ?.overlays 252 ?.get(key) 253 } 254 255 internal fun scene(key: SceneKey): Scene { 256 return sceneOrNull(key) ?: error("Scene $key is not configured") 257 } 258 259 internal fun overlay(key: OverlayKey): Overlay { 260 return overlayOrNull(key) ?: error("Overlay $key is not configured") 261 } 262 263 internal fun content(key: ContentKey): Content { 264 return when (key) { 265 is SceneKey -> scene(key) 266 is OverlayKey -> overlay(key) 267 } 268 } 269 270 internal fun isAncestorContent(content: ContentKey): Boolean { 271 return ancestors.fastAny { it.inContent == content } 272 } 273 274 internal fun contentForUserActions(): Content { 275 return findOverlayWithHighestZIndex() ?: scene(state.transitionState.currentScene) 276 } 277 278 private fun findOverlayWithHighestZIndex(): Overlay? { 279 val currentOverlays = state.transitionState.currentOverlays 280 if (currentOverlays.isEmpty()) { 281 return null 282 } 283 284 var overlay: Overlay? = null 285 currentOverlays.forEach { key -> 286 val previousZIndex = overlay?.zIndex 287 val candidate = overlay(key) 288 if (previousZIndex == null || candidate.zIndex > previousZIndex) { 289 overlay = candidate 290 } 291 } 292 293 return overlay 294 } 295 296 internal fun updateContents( 297 builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit, 298 layoutDirection: LayoutDirection, 299 defaultEffectFactory: OverscrollFactory, 300 ) { 301 // Keep a reference of the current contents. After processing [builder], the contents that 302 // were not configured will be removed. 303 val scenesToRemove = scenes.keys.toMutableSet() 304 val overlaysToRemove = 305 if (_overlays == null) mutableSetOf() else overlays.keys.toMutableSet() 306 307 val parentZIndex = 308 if (ancestors.isEmpty()) 0L else content(ancestors.last().inContent).globalZIndex 309 // The incrementing zIndex of each scene. 310 var zIndex = 0 311 var overlaysDefined = false 312 313 object : SceneTransitionLayoutScope<InternalContentScope> { 314 override fun scene( 315 key: SceneKey, 316 userActions: Map<UserAction, UserActionResult>, 317 effectFactory: OverscrollFactory?, 318 alwaysCompose: Boolean, 319 content: @Composable InternalContentScope.() -> Unit, 320 ) { 321 require(!overlaysDefined) { "all scenes must be defined before overlays" } 322 323 scenesToRemove.remove(key) 324 325 val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection) 326 val scene = scenes[key] 327 val globalZIndex = 328 Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size) 329 val factory = effectFactory ?: defaultEffectFactory 330 if (scene != null) { 331 check(alwaysCompose == scene.alwaysCompose) { 332 "scene.alwaysCompose can not change" 333 } 334 335 // Update an existing scene. 336 scene.content = content 337 scene.userActions = resolvedUserActions 338 scene.zIndex = zIndex.toFloat() 339 scene.globalZIndex = globalZIndex 340 scene.maybeUpdateEffects(factory) 341 } else { 342 // New scene. 343 val scene = 344 Scene( 345 key, 346 this@SceneTransitionLayoutImpl, 347 content, 348 resolvedUserActions, 349 zIndex.toFloat(), 350 globalZIndex, 351 factory, 352 alwaysCompose, 353 ) 354 355 scenes[key] = scene 356 357 if (alwaysCompose) { 358 (scenesToAlwaysCompose 359 ?: mutableListOf<Scene>().also { scenesToAlwaysCompose = it }) 360 .add(scene) 361 } 362 } 363 } 364 365 override fun overlay( 366 key: OverlayKey, 367 userActions: Map<UserAction, UserActionResult>, 368 alignment: Alignment, 369 isModal: Boolean, 370 effectFactory: OverscrollFactory?, 371 content: @Composable (InternalContentScope.() -> Unit), 372 ) { 373 overlaysDefined = true 374 overlaysToRemove.remove(key) 375 376 val overlay = overlays[key] 377 val resolvedUserActions = resolveUserActions(key, userActions, layoutDirection) 378 val globalZIndex = 379 Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size) 380 val factory = effectFactory ?: defaultEffectFactory 381 if (overlay != null) { 382 // Update an existing overlay. 383 overlay.content = content 384 overlay.zIndex = zIndex.toFloat() 385 overlay.globalZIndex = globalZIndex 386 overlay.userActions = resolvedUserActions 387 overlay.alignment = alignment 388 overlay.isModal = isModal 389 overlay.maybeUpdateEffects(factory) 390 } else { 391 // New overlay. 392 overlays[key] = 393 Overlay( 394 key, 395 this@SceneTransitionLayoutImpl, 396 content, 397 resolvedUserActions, 398 zIndex.toFloat(), 399 globalZIndex, 400 alignment, 401 isModal, 402 factory, 403 ) 404 } 405 } 406 } 407 .builder() 408 409 scenesToRemove.forEach { scenes.remove(it) } 410 overlaysToRemove.forEach { overlays.remove(it) } 411 } 412 413 private fun resolveUserActions( 414 key: ContentKey, 415 userActions: Map<UserAction, UserActionResult>, 416 layoutDirection: LayoutDirection, 417 ): Map<UserAction.Resolved, UserActionResult> { 418 return userActions 419 .mapKeys { it.key.resolve(layoutDirection) } 420 .also { checkUserActions(key, it) } 421 } 422 423 private fun checkUserActions( 424 key: ContentKey, 425 userActions: Map<UserAction.Resolved, UserActionResult>, 426 ) { 427 userActions.forEach { (action, result) -> 428 fun details() = "Content $key, action $action, result $result." 429 430 when (result) { 431 is UserActionResult.ChangeScene -> { 432 check(key != result.toScene) { 433 error("Transition to the same scene is not supported. ${details()}") 434 } 435 } 436 437 is UserActionResult.ReplaceByOverlay -> { 438 check(key is OverlayKey) { 439 "ReplaceByOverlay() can only be used for overlays, not scenes. ${details()}" 440 } 441 442 check(key != result.overlay) { 443 "Transition to the same overlay is not supported. ${details()}" 444 } 445 } 446 447 is UserActionResult.ShowOverlay, 448 is UserActionResult.HideOverlay -> { 449 /* Always valid. */ 450 } 451 } 452 } 453 } 454 455 @Composable 456 internal fun Content(modifier: Modifier) { 457 Box( 458 modifier 459 .nestedScroll(nestedScrollConnection, nestedScrollDispatcher) 460 // Handle horizontal and vertical swipes on this layout. 461 // Note: order here is important and will give a slight priority to the vertical 462 // swipes. 463 .swipeToScene(horizontalDraggableHandler) 464 .swipeToScene(verticalDraggableHandler) 465 .then( 466 LayoutElement(layoutImpl = this, transitionState = this.state.transitionState) 467 ) 468 ) { 469 LookaheadScope { 470 if (_lookaheadScope == null) { 471 // We can't init this in a SideEffect as other NestedSTLs are already calling 472 // this during composition. However, when composition is canceled 473 // SceneTransitionLayoutImpl is discarded as well. So it's fine to do this here. 474 _lookaheadScope = this 475 } 476 477 BackHandler() 478 Scenes() 479 Overlays() 480 } 481 } 482 } 483 484 @Composable 485 private fun BackHandler() { 486 val result = contentForUserActions().userActions[Back.Resolved] 487 PredictiveBackHandler(layoutImpl = this, result = result) 488 } 489 490 @Composable 491 private fun Scenes() { 492 scenesToCompose().fastForEach { (scene, isInvisible) -> 493 key(scene.key) { scene.Content(isInvisible = isInvisible) } 494 } 495 } 496 497 private fun scenesToCompose(): List<SceneToCompose> { 498 val transitions = state.currentTransitions 499 return buildList { 500 val visited = mutableSetOf<SceneKey>() 501 fun maybeAdd(sceneKey: SceneKey, isInvisible: Boolean = false) { 502 if (visited.add(sceneKey)) { 503 add(SceneToCompose(scene(sceneKey), isInvisible)) 504 } 505 } 506 507 if (transitions.isEmpty()) { 508 maybeAdd(state.transitionState.currentScene) 509 } else { 510 // Compose the new scene we are going to first. 511 transitions.fastForEachReversed { transition -> 512 when (transition) { 513 is TransitionState.Transition.ChangeScene -> { 514 maybeAdd(transition.toScene) 515 maybeAdd(transition.fromScene) 516 } 517 518 is TransitionState.Transition.ShowOrHideOverlay -> 519 maybeAdd(transition.fromOrToScene) 520 521 is TransitionState.Transition.ReplaceOverlay -> {} 522 } 523 } 524 525 // Make sure that the current scene is always composed. 526 maybeAdd(transitions.last().currentScene) 527 } 528 529 scenesToAlwaysCompose?.fastForEach { maybeAdd(it.key, isInvisible = true) } 530 } 531 } 532 533 private data class SceneToCompose(val scene: Scene, val isInvisible: Boolean) 534 535 @Composable 536 private fun BoxScope.Overlays() { 537 val overlaysOrderedByZIndex = overlaysToComposeOrderedByZIndex() 538 if (overlaysOrderedByZIndex.isEmpty()) { 539 return 540 } 541 542 overlaysOrderedByZIndex.fastForEach { overlay -> 543 val key = overlay.key 544 key(key) { 545 // We put the overlays inside a Box that is matching the layout size so that they 546 // are measured after all scenes and that their max size is the size of the layout 547 // without the overlays. 548 Box(Modifier.matchParentSize().zIndex(overlay.zIndex)) { 549 if (overlay.isModal) { 550 // Add a fullscreen clickable to prevent swipes from reaching the scenes and 551 // other overlays behind this overlay. Clicking will close the overlay. 552 Box( 553 Modifier.fillMaxSize().clickable( 554 interactionSource = remember { MutableInteractionSource() }, 555 indication = null, 556 ) { 557 if (state.canHideOverlay(key)) { 558 state.hideOverlay(key, animationScope = animationScope) 559 } 560 } 561 ) 562 } 563 564 overlay.Content(Modifier.align(overlay.alignment)) 565 } 566 } 567 } 568 } 569 570 private fun overlaysToComposeOrderedByZIndex(): List<Overlay> { 571 if (_overlays == null) return emptyList() 572 573 val transitions = state.currentTransitions 574 return if (transitions.isEmpty()) { 575 state.transitionState.currentOverlays.map { overlay(it) } 576 } else { 577 buildList { 578 val visited = mutableSetOf<OverlayKey>() 579 fun maybeAdd(key: OverlayKey) { 580 if (visited.add(key)) { 581 add(overlay(key)) 582 } 583 } 584 585 transitions.fastForEach { transition -> 586 when (transition) { 587 is TransitionState.Transition.ChangeScene -> {} 588 is TransitionState.Transition.ShowOrHideOverlay -> 589 maybeAdd(transition.overlay) 590 591 is TransitionState.Transition.ReplaceOverlay -> { 592 maybeAdd(transition.fromOverlay) 593 maybeAdd(transition.toOverlay) 594 } 595 } 596 } 597 598 // Make sure that all current overlays are composed. 599 transitions.last().currentOverlays.forEach { maybeAdd(it) } 600 } 601 } 602 .sortedBy { it.zIndex } 603 } 604 605 internal fun hideOverlays(hide: HideCurrentOverlays) { 606 fun maybeHide(overlay: OverlayKey) { 607 if (state.canHideOverlay(overlay)) { 608 state.hideOverlay(overlay, animationScope = this.animationScope) 609 } 610 } 611 612 when (hide) { 613 HideCurrentOverlays.None -> {} 614 HideCurrentOverlays.All -> HashSet(state.currentOverlays).forEach { maybeHide(it) } 615 is HideCurrentOverlays.Some -> hide.overlays.forEach { maybeHide(it) } 616 } 617 } 618 619 @VisibleForTesting 620 internal fun setContentsAndLayoutTargetSizeForTest(size: IntSize) { 621 lastSize = size 622 (scenes.values + overlays.values).forEach { it.targetSize = size } 623 } 624 625 @VisibleForTesting internal fun overlaysOrNullForTest(): Map<OverlayKey, Overlay>? = _overlays 626 } 627 628 private data class LayoutElement( 629 private val layoutImpl: SceneTransitionLayoutImpl, 630 private val transitionState: TransitionState, 631 ) : ModifierNodeElement<LayoutNode>() { createnull632 override fun create(): LayoutNode = LayoutNode(layoutImpl, transitionState) 633 634 override fun update(node: LayoutNode) { 635 node.layoutImpl = layoutImpl 636 node.transitionState = transitionState 637 } 638 } 639 640 private class LayoutNode( 641 var layoutImpl: SceneTransitionLayoutImpl, 642 var transitionState: TransitionState, 643 ) : Modifier.Node(), ApproachLayoutModifierNode, LayoutAwareModifierNode { onRemeasurednull644 override fun onRemeasured(size: IntSize) { 645 layoutImpl.lastSize = size 646 } 647 isMeasurementApproachInProgressnull648 override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { 649 return transitionState is TransitionState.Transition.ChangeScene 650 } 651 652 @ExperimentalComposeUiApi approachMeasurenull653 override fun ApproachMeasureScope.approachMeasure( 654 measurable: Measurable, 655 constraints: Constraints, 656 ): MeasureResult { 657 // Measure content normally. 658 val placeable = measurable.measure(constraints) 659 660 val width: Int 661 val height: Int 662 val transition = transitionState as? TransitionState.Transition.ChangeScene 663 if (transition == null) { 664 width = placeable.width 665 height = placeable.height 666 } else { 667 // Interpolate the size. 668 val fromSize = layoutImpl.scene(transition.fromScene).targetSize 669 val toSize = layoutImpl.scene(transition.toScene).targetSize 670 671 check(fromSize != Element.SizeUnspecified) { "fromSize is unspecified " } 672 check(toSize != Element.SizeUnspecified) { "toSize is unspecified" } 673 674 // Optimization: make sure we don't read state.progress if fromSize == 675 // toSize to avoid running this code every frame when the layout size does 676 // not change. 677 if (fromSize == toSize) { 678 width = fromSize.width 679 height = fromSize.height 680 } else { 681 val size = lerp(fromSize, toSize, transition.progress) 682 width = size.width.coerceAtLeast(0) 683 height = size.height.coerceAtLeast(0) 684 } 685 } 686 687 return layout(width, height) { placeable.place(0, 0) } 688 } 689 } 690