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