• 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 @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
18 
19 package com.android.compose.animation.scene
20 
21 import android.util.Log
22 import androidx.annotation.VisibleForTesting
23 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
24 import androidx.compose.material3.MaterialTheme
25 import androidx.compose.material3.MotionScheme
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.SideEffect
28 import androidx.compose.runtime.Stable
29 import androidx.compose.runtime.derivedStateOf
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.setValue
34 import androidx.compose.ui.util.fastAll
35 import androidx.compose.ui.util.fastAny
36 import androidx.compose.ui.util.fastForEach
37 import com.android.compose.animation.scene.content.state.TransitionState
38 import com.android.compose.animation.scene.transformation.SharedElementTransformation
39 import kotlinx.coroutines.CoroutineScope
40 import kotlinx.coroutines.CoroutineStart
41 import kotlinx.coroutines.Job
42 import kotlinx.coroutines.cancel
43 import kotlinx.coroutines.launch
44 
45 /**
46  * The state of a [SceneTransitionLayout].
47  *
48  * @see MutableSceneTransitionLayoutState
49  */
50 @Stable
51 sealed interface SceneTransitionLayoutState {
52     /**
53      * The current effective scene. If a new transition is triggered, it will start from this scene.
54      */
55     val currentScene: SceneKey
56 
57     /**
58      * The current set of overlays. This represents the set of overlays that will be visible on
59      * screen once all [currentTransitions] are finished.
60      *
61      * @see MutableSceneTransitionLayoutState.showOverlay
62      * @see MutableSceneTransitionLayoutState.hideOverlay
63      * @see MutableSceneTransitionLayoutState.replaceOverlay
64      */
65     val currentOverlays: Set<OverlayKey>
66 
67     /**
68      * The current [TransitionState]. All values read here are backed by the Snapshot system.
69      *
70      * To observe those values outside of Compose/the Snapshot system, use
71      * [SceneTransitionLayoutState.observableTransitionState] instead.
72      */
73     val transitionState: TransitionState
74 
75     /**
76      * The current transition, or `null` if we are idle.
77      *
78      * Note: If you need to handle interruptions and multiple transitions running in parallel, use
79      * [currentTransitions] instead.
80      */
81     val currentTransition: TransitionState.Transition?
82         get() = transitionState as? TransitionState.Transition
83 
84     /**
85      * The list of [TransitionState.Transition] currently running. This will be the empty list if we
86      * are idle.
87      */
88     val currentTransitions: List<TransitionState.Transition>
89 
90     /** The [SceneTransitions] used when animating this state. */
91     val transitions: SceneTransitions
92 
93     /**
94      * Whether we are transitioning. If [from] or [to] is empty, we will also check that they match
95      * the contents we are animating from and/or to.
96      */
97     fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean
98 
99     /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */
100     fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean
101 
102     /** Whether we are transitioning from or to [content]. */
103     fun isTransitioningFromOrTo(content: ContentKey): Boolean
104 }
105 
106 /** A [SceneTransitionLayoutState] whose target scene can be imperatively set. */
107 sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState {
108     /** The [SceneTransitions] used when animating this state. */
109     override var transitions: SceneTransitions
110 
111     /**
112      * Set the target scene of this state to [targetScene].
113      *
114      * If [targetScene] is the same as the [currentScene][TransitionState.currentScene] of
115      * [transitionState], then nothing will happen and this will return `null`. Note that this means
116      * that this will also do nothing if the user is currently swiping from [targetScene] to another
117      * scene, or if we were already animating to [targetScene].
118      *
119      * If [targetScene] is different than the [currentScene][TransitionState.currentScene] of
120      * [transitionState], then this will animate to [targetScene]. The associated
121      * [TransitionState.Transition] will be returned and will be set as the current
122      * [transitionState] of this [MutableSceneTransitionLayoutState]. The [Job] in which the
123      * transition runs will be returned, allowing you to easily [join][Job.join] or
124      * [cancel][Job.cancel] the animation.
125      *
126      * Note that because a non-null [TransitionState.Transition] is returned does not mean that the
127      * transition will finish and that we will settle to [targetScene]. The returned transition
128      * might still be interrupted, for instance by another call to [setTargetScene] or by a user
129      * gesture.
130      *
131      * If [animationScope] is cancelled during the transition and that the transition was still
132      * active, then the [transitionState] of this [MutableSceneTransitionLayoutState] will be set to
133      * `TransitionState.Idle(targetScene)`.
134      */
setTargetScenenull135     fun setTargetScene(
136         targetScene: SceneKey,
137         animationScope: CoroutineScope,
138         transitionKey: TransitionKey? = null,
139     ): Pair<TransitionState.Transition, Job>?
140 
141     /**
142      * Immediately snap to the given [scene] and/or [overlays], instantly interrupting all ongoing
143      * transitions and settling to a [TransitionState.Idle] state.
144      */
145     fun snapTo(
146         scene: SceneKey = transitionState.currentScene,
147         overlays: Set<OverlayKey> = transitionState.currentOverlays,
148     )
149 
150     /**
151      * Request to show [overlay] so that it animates in from [currentScene] and ends up being
152      * visible on screen.
153      *
154      * After this returns, this overlay will be included in [currentOverlays]. This does nothing if
155      * [overlay] is already in [currentOverlays].
156      */
157     fun showOverlay(
158         overlay: OverlayKey,
159         animationScope: CoroutineScope,
160         transitionKey: TransitionKey? = null,
161     )
162 
163     /**
164      * Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being
165      * visible on screen.
166      *
167      * After this returns, this overlay will not be included in [currentOverlays]. This does nothing
168      * if [overlay] is not in [currentOverlays].
169      */
170     fun hideOverlay(
171         overlay: OverlayKey,
172         animationScope: CoroutineScope,
173         transitionKey: TransitionKey? = null,
174     )
175 
176     /**
177      * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up
178      * being visible.
179      *
180      * This throws if [from] is not currently in [currentOverlays] or if [to] is already in
181      * [currentOverlays].
182      */
183     fun replaceOverlay(
184         from: OverlayKey,
185         to: OverlayKey,
186         animationScope: CoroutineScope,
187         transitionKey: TransitionKey? = null,
188     )
189 
190     /**
191      * Instantly start a [transition], running it in [animationScope].
192      *
193      * This call returns immediately and [transition] will be the [currentTransition] of this
194      * [MutableSceneTransitionLayoutState].
195      *
196      * @see startTransition
197      */
198     fun startTransitionImmediately(
199         animationScope: CoroutineScope,
200         transition: TransitionState.Transition,
201         chain: Boolean = true,
202     ): Job
203 
204     /**
205      * Start a new [transition].
206      *
207      * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and
208      * will run in parallel to the current transitions. If [chain] is `false`, then the list of
209      * [currentTransitions] will be cleared and [transition] will be the only running transition.
210      *
211      * If any transition is currently ongoing, it will be interrupted and forced to animate to its
212      * current state by calling [TransitionState.Transition.freezeAndAnimateToCurrentState].
213      *
214      * This method returns when [transition] is done running, i.e. when the call to
215      * [run][TransitionState.Transition.run] returns.
216      */
217     suspend fun startTransition(transition: TransitionState.Transition, chain: Boolean = true)
218 }
219 
220 /**
221  * Return a [MutableSceneTransitionLayoutState] initially idle at [initialScene].
222  *
223  * @param initialScene the initial scene to which this state is initialized.
224  * @param transitions the [SceneTransitions] used when this state is transitioning between scenes.
225  * @param canChangeScene whether we can transition to the given scene. This is called when the user
226  *   commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
227  *   `true`, then the gesture will be committed and we will animate to the other scene. Otherwise,
228  *   the gesture will be cancelled and we will animate back to the current scene.
229  * @param canShowOverlay whether we should commit a user action that will result in showing the
230  *   given overlay.
231  * @param canHideOverlay whether we should commit a user action that will result in hiding the given
232  *   overlay.
233  * @param canReplaceOverlay whether we should commit a user action that will result in replacing
234  *   `from` overlay by `to` overlay.
235  * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
236  *   [SceneTransitionLayoutState]s.
237  * @param deferTransitionProgress whether we should wait for the first composition to be done before
238  *   changing the progress of a transition. This can help reduce perceivable jank at the start of a
239  *   transition in case the first composition of a content takes a lot of time and we are going to
240  *   miss that first frame.
241  */
242 fun MutableSceneTransitionLayoutState(
243     initialScene: SceneKey,
244     motionScheme: MotionScheme,
245     transitions: SceneTransitions = SceneTransitions.Empty,
246     initialOverlays: Set<OverlayKey> = emptySet(),
247     canChangeScene: (SceneKey) -> Boolean = { true },
<lambda>null248     canShowOverlay: (OverlayKey) -> Boolean = { true },
<lambda>null249     canHideOverlay: (OverlayKey) -> Boolean = { true },
_null250     canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
<lambda>null251     onTransitionStart: (TransitionState.Transition) -> Unit = {},
<lambda>null252     onTransitionEnd: (TransitionState.Transition) -> Unit = {},
253 
254     // TODO(b/400688335): Turn on by default and remove this flag before flexiglass is released.
255     deferTransitionProgress: Boolean = false,
256 ): MutableSceneTransitionLayoutState {
257     return MutableSceneTransitionLayoutStateImpl(
258         initialScene,
259         motionScheme,
260         transitions,
261         initialOverlays,
262         canChangeScene,
263         canShowOverlay,
264         canHideOverlay,
265         canReplaceOverlay,
266         onTransitionStart,
267         onTransitionEnd,
268         deferTransitionProgress,
269     )
270 }
271 
272 @Composable
rememberMutableSceneTransitionLayoutStatenull273 fun rememberMutableSceneTransitionLayoutState(
274     initialScene: SceneKey,
275     transitions: SceneTransitions = SceneTransitions.Empty,
276     initialOverlays: Set<OverlayKey> = emptySet(),
277     canChangeScene: (SceneKey) -> Boolean = { true },
<lambda>null278     canShowOverlay: (OverlayKey) -> Boolean = { true },
<lambda>null279     canHideOverlay: (OverlayKey) -> Boolean = { true },
_null280     canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
<lambda>null281     onTransitionStart: (TransitionState.Transition) -> Unit = {},
<lambda>null282     onTransitionEnd: (TransitionState.Transition) -> Unit = {},
283 
284     // TODO(b/400688335): Turn on by default and remove this flag before flexiglass is released.
285     deferTransitionProgress: Boolean = false,
286 ): MutableSceneTransitionLayoutState {
287     val motionScheme = MaterialTheme.motionScheme
<lambda>null288     val layoutState = remember {
289         MutableSceneTransitionLayoutStateImpl(
290             initialScene = initialScene,
291             motionScheme = motionScheme,
292             transitions = transitions,
293             initialOverlays = initialOverlays,
294             canChangeScene = canChangeScene,
295             canShowOverlay = canShowOverlay,
296             canHideOverlay = canHideOverlay,
297             canReplaceOverlay = canReplaceOverlay,
298             onTransitionStart = onTransitionStart,
299             onTransitionEnd = onTransitionEnd,
300             deferTransitionProgress = deferTransitionProgress,
301         )
302     }
303 
<lambda>null304     SideEffect {
305         layoutState.transitions = transitions
306         layoutState.motionScheme = motionScheme
307         layoutState.canChangeScene = canChangeScene
308         layoutState.canShowOverlay = canShowOverlay
309         layoutState.canHideOverlay = canHideOverlay
310         layoutState.canReplaceOverlay = canReplaceOverlay
311         layoutState.onTransitionStart = onTransitionStart
312         layoutState.onTransitionEnd = onTransitionEnd
313         layoutState.deferTransitionProgress = deferTransitionProgress
314     }
315     return layoutState
316 }
317 
318 /** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
319 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
320 internal class MutableSceneTransitionLayoutStateImpl(
321     initialScene: SceneKey,
322     internal var motionScheme: MotionScheme,
<lambda>null323     override var transitions: SceneTransitions = transitions {},
324     initialOverlays: Set<OverlayKey> = emptySet(),
<lambda>null325     internal var canChangeScene: (SceneKey) -> Boolean = { true },
<lambda>null326     internal var canShowOverlay: (OverlayKey) -> Boolean = { true },
<lambda>null327     internal var canHideOverlay: (OverlayKey) -> Boolean = { true },
_null328     internal var canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
329         true
330     },
<lambda>null331     internal var onTransitionStart: (TransitionState.Transition) -> Unit = {},
<lambda>null332     internal var onTransitionEnd: (TransitionState.Transition) -> Unit = {},
333     // TODO(b/400688335): Turn on by default and remove this flag before flexiglass is released.
334     internal var deferTransitionProgress: Boolean = false,
335 ) : MutableSceneTransitionLayoutState {
336     private val creationThread: Thread = Thread.currentThread()
337 
338     /**
339      * The current [TransitionState]. This list will either be:
340      * 1. A list with a single [TransitionState.Idle] element, when we are idle.
341      * 2. A list with one or more [TransitionState.Transition], when we are transitioning.
342      */
343     internal var transitionStates: List<TransitionState> by
344         mutableStateOf(listOf(TransitionState.Idle(initialScene, initialOverlays)))
345         private set
346 
347     /**
348      * The flattened list of [SharedElementTransformation.Factory] within all the transitions in
349      * [transitionStates].
350      */
351     private val transformationFactoriesWithElevation:
<lambda>null352         List<SharedElementTransformation.Factory> by derivedStateOf {
353         transformationFactoriesWithElevation(transitionStates)
354     }
355 
356     override val currentScene: SceneKey
357         get() = transitionState.currentScene
358 
359     override val currentOverlays: Set<OverlayKey>
360         get() = transitionState.currentOverlays
361 
362     override val transitionState: TransitionState
363         get() = transitionStates[transitionStates.lastIndex]
364 
365     override val currentTransitions: List<TransitionState.Transition>
366         get() {
367             if (transitionStates.last() is TransitionState.Idle) {
368                 check(transitionStates.size == 1)
369                 return emptyList()
370             } else {
371                 @Suppress("UNCHECKED_CAST")
372                 return transitionStates as List<TransitionState.Transition>
373             }
374         }
375 
376     /** The transitions that are finished, i.e. for which [finishTransition] was called. */
377     @VisibleForTesting internal val finishedTransitions = mutableSetOf<TransitionState.Transition>()
378 
checkThreadnull379     internal fun checkThread() {
380         val current = Thread.currentThread()
381         if (current !== creationThread) {
382             error(
383                 """
384                     Only the original thread that created a SceneTransitionLayoutState can mutate it
385                       Expected: ${creationThread.name}
386                       Current: ${current.name}
387                 """
388                     .trimIndent()
389             )
390         }
391     }
392 
isTransitioningnull393     override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean {
394         val transition = currentTransition ?: return false
395         return transition.isTransitioning(from, to)
396     }
397 
isTransitioningBetweennull398     override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean {
399         val transition = currentTransition ?: return false
400         return transition.isTransitioningBetween(content, other)
401     }
402 
isTransitioningFromOrTonull403     override fun isTransitioningFromOrTo(content: ContentKey): Boolean {
404         val transition = currentTransition ?: return false
405         return transition.isTransitioningFromOrTo(content)
406     }
407 
setTargetScenenull408     override fun setTargetScene(
409         targetScene: SceneKey,
410         animationScope: CoroutineScope,
411         transitionKey: TransitionKey?,
412     ): Pair<TransitionState.Transition.ChangeScene, Job>? {
413         checkThread()
414 
415         return animationScope.animateToScene(
416             layoutState = this@MutableSceneTransitionLayoutStateImpl,
417             target = targetScene,
418             transitionKey = transitionKey,
419         )
420     }
421 
startTransitionImmediatelynull422     override fun startTransitionImmediately(
423         animationScope: CoroutineScope,
424         transition: TransitionState.Transition,
425         chain: Boolean,
426     ): Job {
427         // Note that we start with UNDISPATCHED so that startTransition() is called directly and
428         // transition becomes the current [transitionState] right after this call.
429         return animationScope.launch(start = CoroutineStart.UNDISPATCHED) {
430             startTransition(transition, chain)
431         }
432     }
433 
startTransitionnull434     override suspend fun startTransition(transition: TransitionState.Transition, chain: Boolean) {
435         Log.i(TAG, "startTransition(transition=$transition, chain=$chain)")
436         checkThread()
437 
438         // Prepare the transition before starting it. This is outside of the try/finally block on
439         // purpose because preparing a transition might throw an exception (e.g. if we find multiple
440         // specs matching this transition), in which case we want to throw that exception here
441         // before even starting the transition.
442         prepareTransitionBeforeStarting(transition)
443 
444         try {
445             // Start the transition.
446             startTransitionInternal(transition, chain)
447 
448             // Run the transition until it is finished.
449             onTransitionStart(transition)
450             transition.runInternal()
451         } finally {
452             finishTransition(transition)
453             onTransitionEnd(transition)
454         }
455     }
456 
prepareTransitionBeforeStartingnull457     private fun prepareTransitionBeforeStarting(transition: TransitionState.Transition) {
458         // Set the current scene and overlays on the transition.
459         val currentState = transitionState
460         transition.currentSceneWhenTransitionStarted = currentState.currentScene
461         transition.currentOverlaysWhenTransitionStarted = currentState.currentOverlays
462 
463         // Compute the [TransformationSpec] when the transition starts.
464         val fromContent = transition.fromContent
465         val toContent = transition.toContent
466 
467         // Update the transition specs.
468         val spec = transitions.transitionSpec(fromContent, toContent, key = transition.key)
469         transition._cuj = spec.cuj
470         transition.transformationSpec = spec.transformationSpec(transition)
471         transition.previewTransformationSpec = spec.previewTransformationSpec(transition)
472     }
473 
startTransitionInternalnull474     private fun startTransitionInternal(transition: TransitionState.Transition, chain: Boolean) {
475         when (val currentState = transitionStates.last()) {
476             is TransitionState.Idle -> {
477                 // Replace [Idle] by [transition].
478                 check(transitionStates.size == 1)
479                 transitionStates = listOf(transition)
480             }
481             is TransitionState.Transition -> {
482                 // Force the current transition to finish to currentScene.
483                 currentState.freezeAndAnimateToCurrentState()
484 
485                 val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS
486                 val clearCurrentTransitions = !chain || tooManyTransitions
487                 if (clearCurrentTransitions) {
488                     if (tooManyTransitions) logTooManyTransitions()
489 
490                     // Force finish all transitions.
491                     currentTransitions.fastForEach { finishTransition(it) }
492 
493                     // We finished all transitions, so we are now idle. We remove this state so that
494                     // we end up only with the new transition after appending it.
495                     check(transitionStates.size == 1)
496                     check(transitionStates[0] is TransitionState.Idle)
497                     transitionStates = listOf(transition)
498                 } else {
499                     // Append the new transition.
500                     transitionStates = transitionStates + transition
501                 }
502             }
503         }
504     }
505 
logTooManyTransitionsnull506     private fun logTooManyTransitions() {
507         Log.wtf(
508             TAG,
509             buildString {
510                 appendLine("Potential leak detected in SceneTransitionLayoutState!")
511                 appendLine("  Some transition(s) never called STLState.finishTransition().")
512                 appendLine("  Transitions (size=${transitionStates.size}):")
513                 transitionStates.fastForEach { state ->
514                     val transition = state as TransitionState.Transition
515                     val from = transition.fromContent
516                     val to = transition.toContent
517                     val indicator = if (finishedTransitions.contains(transition)) "x" else " "
518                     appendLine("  [$indicator] $from => $to ($transition)")
519                 }
520             },
521         )
522     }
523 
524     /**
525      * Notify that [transition] was finished and that it settled to its
526      * [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was
527      * interrupted since it was started.
528      */
finishTransitionnull529     private fun finishTransition(transition: TransitionState.Transition) {
530         checkThread()
531 
532         if (finishedTransitions.contains(transition)) {
533             // This transition was already finished.
534             return
535         }
536 
537         // Make sure that this transition is cancelled in case it was force finished, for instance
538         // if snapToScene() is called.
539         transition.coroutineScope.cancel()
540 
541         val transitionStates = this.transitionStates
542         if (!transitionStates.contains(transition)) {
543             // This transition was already removed from transitionStates.
544             return
545         }
546 
547         Log.i(TAG, "finishTransition(transition=$transition)")
548         check(transitionStates.fastAll { it is TransitionState.Transition })
549 
550         // Mark this transition as finished.
551         finishedTransitions.add(transition)
552 
553         if (finishedTransitions.size != transitionStates.size) {
554             // Some transitions were not finished, so we won't settle to idle.
555             return
556         }
557 
558         // Keep a reference to the last transition, in case all transitions are finished and we
559         // should settle to Idle.
560         val lastTransition = transitionStates.last()
561 
562         transitionStates.fastForEach { state ->
563             if (!finishedTransitions.contains(state)) {
564                 // Some transitions were not finished, so we won't settle to idle.
565                 return
566             }
567         }
568 
569         val idle = TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
570         Log.i(TAG, "all transitions finished. idle=$idle")
571         finishedTransitions.clear()
572         this.transitionStates = listOf(idle)
573     }
574 
snapTonull575     override fun snapTo(scene: SceneKey, overlays: Set<OverlayKey>) {
576         checkThread()
577 
578         // Force finish all transitions.
579         currentTransitions.fastForEach { finishTransition(it) }
580 
581         check(transitionStates.size == 1)
582         check(currentTransitions.isEmpty())
583         transitionStates = listOf(TransitionState.Idle(scene, overlays))
584     }
585 
showOverlaynull586     override fun showOverlay(
587         overlay: OverlayKey,
588         animationScope: CoroutineScope,
589         transitionKey: TransitionKey?,
590     ) {
591         checkThread()
592 
593         // Overlay is already shown, do nothing.
594         val currentState = transitionState
595         if (overlay in currentState.currentOverlays) {
596             return
597         }
598 
599         val fromScene = currentState.currentScene
600         fun animate(
601             replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null,
602             reversed: Boolean = false,
603         ) {
604             animationScope.showOrHideOverlay(
605                 layoutState = this@MutableSceneTransitionLayoutStateImpl,
606                 overlay = overlay,
607                 fromOrToScene = fromScene,
608                 isShowing = true,
609                 transitionKey = transitionKey,
610                 replacedTransition = replacedTransition,
611                 reversed = reversed,
612             )
613         }
614 
615         if (
616             currentState is TransitionState.Transition.ShowOrHideOverlay &&
617                 currentState.overlay == overlay &&
618                 currentState.fromOrToScene == fromScene
619         ) {
620             animate(
621                 replacedTransition = currentState,
622                 reversed = overlay == currentState.fromContent,
623             )
624         } else {
625             animate()
626         }
627     }
628 
hideOverlaynull629     override fun hideOverlay(
630         overlay: OverlayKey,
631         animationScope: CoroutineScope,
632         transitionKey: TransitionKey?,
633     ) {
634         checkThread()
635 
636         // Overlay is not shown, do nothing.
637         val currentState = transitionState
638         if (!currentState.currentOverlays.contains(overlay)) {
639             return
640         }
641 
642         val toScene = currentState.currentScene
643         fun animate(
644             replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null,
645             reversed: Boolean = false,
646         ) {
647             animationScope.showOrHideOverlay(
648                 layoutState = this@MutableSceneTransitionLayoutStateImpl,
649                 overlay = overlay,
650                 fromOrToScene = toScene,
651                 isShowing = false,
652                 transitionKey = transitionKey,
653                 replacedTransition = replacedTransition,
654                 reversed = reversed,
655             )
656         }
657 
658         if (
659             currentState is TransitionState.Transition.ShowOrHideOverlay &&
660                 currentState.overlay == overlay &&
661                 currentState.fromOrToScene == toScene
662         ) {
663             animate(replacedTransition = currentState, reversed = overlay == currentState.toContent)
664         } else {
665             animate()
666         }
667     }
668 
replaceOverlaynull669     override fun replaceOverlay(
670         from: OverlayKey,
671         to: OverlayKey,
672         animationScope: CoroutineScope,
673         transitionKey: TransitionKey?,
674     ) {
675         checkThread()
676 
677         val currentState = transitionState
678         require(from != to) {
679             "replaceOverlay must be called with different overlays (from = to = ${from.debugName})"
680         }
681         require(from in currentState.currentOverlays) {
682             "Overlay ${from.debugName} is not shown so it can't be replaced by ${to.debugName}"
683         }
684         require(to !in currentState.currentOverlays) {
685             "Overlay ${to.debugName} is already shown so it can't replace ${from.debugName}"
686         }
687 
688         fun animate(
689             replacedTransition: TransitionState.Transition.ReplaceOverlay? = null,
690             reversed: Boolean = false,
691         ) {
692             animationScope.replaceOverlay(
693                 layoutState = this@MutableSceneTransitionLayoutStateImpl,
694                 fromOverlay = if (reversed) to else from,
695                 toOverlay = if (reversed) from else to,
696                 transitionKey = transitionKey,
697                 replacedTransition = replacedTransition,
698                 reversed = reversed,
699             )
700         }
701 
702         if (currentState is TransitionState.Transition.ReplaceOverlay) {
703             if (currentState.fromOverlay == from && currentState.toOverlay == to) {
704                 animate(replacedTransition = currentState, reversed = false)
705                 return
706             }
707 
708             if (currentState.fromOverlay == to && currentState.toOverlay == from) {
709                 animate(replacedTransition = currentState, reversed = true)
710                 return
711             }
712         }
713 
714         animate()
715     }
716 
transformationFactoriesWithElevationnull717     private fun transformationFactoriesWithElevation(
718         transitionStates: List<TransitionState>
719     ): List<SharedElementTransformation.Factory> {
720         return buildList {
721             transitionStates.fastForEach { state ->
722                 if (state !is TransitionState.Transition) {
723                     return@fastForEach
724                 }
725 
726                 state.transformationSpec.transformationMatchers.fastForEach { transformationMatcher
727                     ->
728                     val factory = transformationMatcher.factory
729                     if (
730                         factory is SharedElementTransformation.Factory &&
731                             factory.elevateInContent != null
732                     ) {
733                         add(factory)
734                     }
735                 }
736             }
737         }
738     }
739 
740     /**
741      * Return whether we might need to elevate [element] (or any element if [element] is `null`) in
742      * [content].
743      *
744      * This is used to compose `Modifier.container()` and `Modifier.drawInContainer()` only when
745      * necessary, for performance.
746      */
isElevationPossiblenull747     internal fun isElevationPossible(content: ContentKey, element: ElementKey?): Boolean {
748         if (transformationFactoriesWithElevation.isEmpty()) return false
749         return transformationFactoriesWithElevation.fastAny { factory ->
750             factory.elevateInContent == content &&
751                 (element == null || factory.matcher.matches(element, content))
752         }
753     }
754 }
755 
756 private const val TAG = "SceneTransitionLayoutState"
757 
758 /**
759  * The max number of concurrent transitions. If the number of transitions goes past this number,
760  * this probably means that there is a leak and we will Log.wtf before clearing the list of
761  * transitions.
762  */
763 private const val MAX_CONCURRENT_TRANSITIONS = 100
764