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