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