• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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