1 /*
<lambda>null2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.compose.animation.scene
18
19 import androidx.compose.runtime.Stable
20 import androidx.compose.runtime.getValue
21 import androidx.compose.runtime.mutableStateOf
22 import androidx.compose.runtime.setValue
23 import androidx.compose.runtime.snapshots.SnapshotStateMap
24 import androidx.compose.ui.ExperimentalComposeUiApi
25 import androidx.compose.ui.Modifier
26 import androidx.compose.ui.geometry.Offset
27 import androidx.compose.ui.geometry.isSpecified
28 import androidx.compose.ui.geometry.isUnspecified
29 import androidx.compose.ui.geometry.lerp
30 import androidx.compose.ui.graphics.CompositingStrategy
31 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
32 import androidx.compose.ui.graphics.drawscope.scale
33 import androidx.compose.ui.layout.ApproachLayoutModifierNode
34 import androidx.compose.ui.layout.ApproachMeasureScope
35 import androidx.compose.ui.layout.LayoutCoordinates
36 import androidx.compose.ui.layout.Measurable
37 import androidx.compose.ui.layout.MeasureResult
38 import androidx.compose.ui.layout.MeasureScope
39 import androidx.compose.ui.layout.Placeable
40 import androidx.compose.ui.node.DrawModifierNode
41 import androidx.compose.ui.node.ModifierNodeElement
42 import androidx.compose.ui.node.TraversableNode
43 import androidx.compose.ui.node.traverseDescendants
44 import androidx.compose.ui.platform.testTag
45 import androidx.compose.ui.unit.Constraints
46 import androidx.compose.ui.unit.IntSize
47 import androidx.compose.ui.unit.round
48 import androidx.compose.ui.util.fastCoerceIn
49 import androidx.compose.ui.util.fastForEachReversed
50 import androidx.compose.ui.util.lerp
51 import com.android.compose.animation.scene.content.Content
52 import com.android.compose.animation.scene.content.state.TransitionState
53 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
54 import com.android.compose.animation.scene.transformation.InterpolatedPropertyTransformation
55 import com.android.compose.animation.scene.transformation.PropertyTransformation
56 import com.android.compose.animation.scene.transformation.TransformationWithRange
57 import com.android.compose.modifiers.thenIf
58 import com.android.compose.ui.graphics.drawInContainer
59 import com.android.compose.ui.util.IntIndexedMap
60 import com.android.compose.ui.util.lerp
61 import kotlin.math.roundToInt
62 import kotlinx.coroutines.launch
63
64 /** An element on screen, that can be composed in one or more contents. */
65 @Stable
66 internal class Element(val key: ElementKey) {
67 /** The mapping between a content and the state this element has in that content, if any. */
68 // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
69 // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
70 val stateByContent = SnapshotStateMap<ContentKey, State>()
71
72 /**
73 * A sorted map of nesting depth (key) to content key (value). For shared elements it is used to
74 * determine which content this element should be rendered by. The nesting depth refers to the
75 * number of STLs nested within each other, starting at 0 for the parent STL and increasing by
76 * one for each nested [NestedSceneTransitionLayout].
77 */
78 val renderAuthority = IntIndexedMap<ContentKey>()
79
80 /**
81 * The last transition that was used when computing the state (size, position and alpha) of this
82 * element in any content, or `null` if it was last laid out when idle.
83 */
84 var lastTransition: TransitionState.Transition? = null
85
86 /** Whether this element was ever drawn in a content. */
87 var wasDrawnInAnyContent = false
88
89 override fun toString(): String {
90 return "Element(key=$key)"
91 }
92
93 /** The last and target state of this element in a given content. */
94 @Stable
95 class State(val content: ContentKey) {
96 /**
97 * The *target* state of this element in this content, i.e. the state of this element when
98 * we are idle on this content.
99 */
100 var targetSize by mutableStateOf(SizeUnspecified)
101 var targetOffset by mutableStateOf(Offset.Unspecified)
102
103 /** The last state this element had in this content. */
104 var lastOffset = Offset.Unspecified
105 var lastSize = SizeUnspecified
106 var lastScale = Scale.Unspecified
107 var lastAlpha = AlphaUnspecified
108
109 /**
110 * The state of this element in this content right before the last interruption (if any).
111 */
112 var offsetBeforeInterruption = Offset.Unspecified
113 var sizeBeforeInterruption = SizeUnspecified
114 var scaleBeforeInterruption = Scale.Unspecified
115 var alphaBeforeInterruption = AlphaUnspecified
116
117 /**
118 * The delta values to add to this element state to have smoother interruptions. These
119 * should be multiplied by the
120 * [current interruption progress][ContentState.Transition.interruptionProgress] so that
121 * they nicely animate from their values down to 0.
122 */
123 var offsetInterruptionDelta = Offset.Zero
124 var sizeInterruptionDelta = IntSize.Zero
125 var scaleInterruptionDelta = Scale.Zero
126 var alphaInterruptionDelta = 0f
127
128 /**
129 * The attached [ElementNode] a Modifier.element() for a given element and content. During
130 * composition, this set could have 0 to 2 elements. After composition and after all
131 * modifier nodes have been attached/detached, this set should contain exactly 1 element.
132 */
133 val nodes = mutableSetOf<ElementNode>()
134 }
135
136 companion object {
137 val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
138 val AlphaUnspecified = Float.MAX_VALUE
139 }
140 }
141
142 data class Scale(val scaleX: Float, val scaleY: Float, val pivot: Offset = Offset.Unspecified) {
143 companion object {
144 val Default = Scale(1f, 1f, Offset.Unspecified)
145 val Zero = Scale(0f, 0f, Offset.Zero)
146 val Unspecified = Scale(Float.MAX_VALUE, Float.MAX_VALUE, Offset.Unspecified)
147 }
148 }
149
150 /** The implementation of [ContentScope.element]. */
151 @Stable
elementnull152 internal fun Modifier.element(
153 layoutImpl: SceneTransitionLayoutImpl,
154 content: Content,
155 key: ElementKey,
156 ): Modifier {
157 // Make sure that we read the current transitions during composition and not during
158 // layout/drawing.
159 // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
160 // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
161 val currentTransitionStates = layoutImpl.state.transitionStates
162 return thenIf(layoutImpl.state.isElevationPossible(content.key, key)) {
163 Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
164 }
165 .then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
166 .testTag(key.testTag)
167 }
168
maybeElevateInContentnull169 private fun Modifier.maybeElevateInContent(
170 layoutImpl: SceneTransitionLayoutImpl,
171 content: Content,
172 key: ElementKey,
173 transitionStates: List<TransitionState>,
174 ): Modifier {
175 fun isSharedElement(
176 stateByContent: Map<ContentKey, Element.State>,
177 transition: TransitionState.Transition,
178 ): Boolean {
179 fun inFromContent() = transition.fromContent in stateByContent
180 fun inToContent() = transition.toContent in stateByContent
181 fun inCurrentScene() = transition.currentScene in stateByContent
182
183 return if (transition is TransitionState.Transition.ReplaceOverlay) {
184 (inFromContent() && (inToContent() || inCurrentScene())) ||
185 (inToContent() && inCurrentScene())
186 } else {
187 inFromContent() && inToContent()
188 }
189 }
190
191 return drawInContainer(
192 content.containerState,
193 enabled = {
194 val stateByContent = layoutImpl.elements.getValue(key).stateByContent
195 val state = elementState(transitionStates, isInContent = { it in stateByContent })
196
197 state is TransitionState.Transition &&
198 state.transformationSpec
199 .transformations(key, content.key)
200 .shared
201 ?.transformation
202 ?.elevateInContent == content.key &&
203 isSharedElement(stateByContent, state) &&
204 isSharedElementEnabled(key, state) &&
205 shouldPlaceElement(
206 layoutImpl,
207 content.key,
208 layoutImpl.elements.getValue(key),
209 state,
210 )
211 },
212 )
213 }
214
215 /**
216 * An element associated to [ElementNode]. Note that this element does not support updates as its
217 * arguments should always be the same.
218 */
219 internal data class ElementModifier(
220 internal val layoutImpl: SceneTransitionLayoutImpl,
221 private val currentTransitionStates: List<TransitionState>,
222 internal val content: Content,
223 internal val key: ElementKey,
224 ) : ModifierNodeElement<ElementNode>() {
createnull225 override fun create(): ElementNode =
226 ElementNode(layoutImpl, currentTransitionStates, content, key)
227
228 override fun update(node: ElementNode) {
229 node.update(layoutImpl, currentTransitionStates, content, key)
230 }
231 }
232
233 internal class ElementNode(
234 private var layoutImpl: SceneTransitionLayoutImpl,
235 private var currentTransitionStates: List<TransitionState>,
236 private var content: Content,
237 private var key: ElementKey,
238 ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
239 private var _element: Element? = null
240 private val element: Element
241 get() = _element!!
242
243 private val stateInContent: Element.State
244 get() = element.stateByContent.getValue(content.key)
245
246 override val traverseKey: Any = ElementTraverseKey
247
onAttachnull248 override fun onAttach() {
249 super.onAttach()
250 updateElementAndContentValues()
251 addNodeToContentState()
252 }
253
updateElementAndContentValuesnull254 private fun updateElementAndContentValues() {
255 val element =
256 layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
257 _element = element
258 addToRenderAuthority(element)
259 if (!element.stateByContent.contains(content.key)) {
260 val elementState = Element.State(content.key)
261 element.stateByContent[content.key] = elementState
262
263 layoutImpl.ancestorContentKeys.forEach { element.stateByContent[it] = elementState }
264 }
265 }
266
addNodeToContentStatenull267 private fun addNodeToContentState() {
268 stateInContent.nodes.add(this)
269
270 coroutineScope.launch {
271 // At this point all [CodeLocationNode] have been attached or detached, which means that
272 // [elementState.codeLocations] should have exactly 1 element, otherwise this means that
273 // this element was composed multiple times in the same content.
274 val nCodeLocations = stateInContent.nodes.size
275 if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
276 error("$key was composed $nCodeLocations times in ${stateInContent.content}")
277 }
278 }
279 }
280
onDetachnull281 override fun onDetach() {
282 super.onDetach()
283 removeNodeFromContentState()
284 maybePruneMaps(layoutImpl, element, stateInContent)
285
286 removeFromRenderAuthority()
287 _element = null
288 }
289
addToRenderAuthoritynull290 private fun addToRenderAuthority(element: Element) {
291 val nestingDepth = layoutImpl.ancestorContentKeys.size
292 element.renderAuthority[nestingDepth] = content.key
293 }
294
removeFromRenderAuthoritynull295 private fun removeFromRenderAuthority() {
296 val nestingDepth = layoutImpl.ancestorContentKeys.size
297 if (element.renderAuthority[nestingDepth] == content.key) {
298 element.renderAuthority.remove(nestingDepth)
299 }
300 }
301
removeNodeFromContentStatenull302 private fun removeNodeFromContentState() {
303 stateInContent.nodes.remove(this)
304 }
305
updatenull306 fun update(
307 layoutImpl: SceneTransitionLayoutImpl,
308 currentTransitionStates: List<TransitionState>,
309 content: Content,
310 key: ElementKey,
311 ) {
312 check(layoutImpl == this.layoutImpl && content == this.content)
313 this.currentTransitionStates = currentTransitionStates
314
315 removeNodeFromContentState()
316
317 val prevElement = this.element
318 val prevElementState = this.stateInContent
319 this.key = key
320 updateElementAndContentValues()
321
322 addNodeToContentState()
323 maybePruneMaps(layoutImpl, prevElement, prevElementState)
324 }
325
isMeasurementApproachInProgressnull326 override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
327 // TODO(b/324191441): Investigate whether making this check more complex (checking if this
328 // element is shared or transformed) would lead to better performance.
329 return layoutImpl.state.isTransitioning()
330 }
331
isPlacementApproachInProgressnull332 override fun Placeable.PlacementScope.isPlacementApproachInProgress(
333 lookaheadCoordinates: LayoutCoordinates
334 ): Boolean {
335 // TODO(b/324191441): Investigate whether making this check more complex (checking if this
336 // element is shared or transformed) would lead to better performance.
337 return layoutImpl.state.isTransitioning()
338 }
339
340 @ExperimentalComposeUiApi
measurenull341 override fun MeasureScope.measure(
342 measurable: Measurable,
343 constraints: Constraints,
344 ): MeasureResult {
345 check(isLookingAhead)
346
347 return measurable.measure(constraints).run {
348 // Update the size this element has in this content when idle.
349 stateInContent.targetSize = size()
350
351 layout(width, height) {
352 // Update the offset (relative to the SceneTransitionLayout) this element has in
353 // this content when idle.
354 coordinates?.let { coords ->
355 with(layoutImpl.lookaheadScope) {
356 stateInContent.targetOffset =
357 lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
358 }
359 }
360 place(0, 0)
361 }
362 }
363 }
364
approachMeasurenull365 override fun ApproachMeasureScope.approachMeasure(
366 measurable: Measurable,
367 constraints: Constraints,
368 ): MeasureResult {
369 val elementState = elementState(layoutImpl, element, currentTransitionStates)
370 if (elementState == null) {
371 // If the element is not part of any transition, place it normally in its idle scene.
372 // This is the case if for example a transition between two overlays is ongoing where
373 // sharedElement isn't part of either but the element is still rendered as part of
374 // the underlying scene that is currently not being transitioned.
375 val currentState = currentTransitionStates.last()
376 val shouldPlaceInThisContent =
377 elementContentWhenIdle(
378 layoutImpl,
379 currentState,
380 isInContent = { it in element.stateByContent },
381 ) == content.key
382 return if (shouldPlaceInThisContent) {
383 placeNormally(measurable, constraints)
384 } else {
385 doNotPlace(measurable, constraints)
386 }
387 }
388
389 val transition = elementState as? TransitionState.Transition
390
391 // If this element is not supposed to be laid out now, either because it is not part of any
392 // ongoing transition or the other content of its transition is overscrolling, then lay out
393 // the element normally and don't place it.
394 val overscrollContent = transition?.currentOverscrollSpec?.content
395 if (overscrollContent != null && overscrollContent != content.key) {
396 when (transition) {
397 is TransitionState.Transition.ChangeScene ->
398 return doNotPlace(measurable, constraints)
399
400 // If we are overscrolling an overlay that does not contain an element that is in
401 // the current scene, place it in that scene otherwise the element won't be placed
402 // at all.
403 is TransitionState.Transition.ShowOrHideOverlay,
404 is TransitionState.Transition.ReplaceOverlay -> {
405 if (
406 content.key == transition.currentScene &&
407 overscrollContent !in element.stateByContent
408 ) {
409 return placeNormally(measurable, constraints)
410 } else {
411 return doNotPlace(measurable, constraints)
412 }
413 }
414 }
415 }
416
417 val placeable =
418 measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
419 stateInContent.lastSize = placeable.size()
420 return layout(placeable.width, placeable.height) { place(elementState, placeable) }
421 }
422
doNotPlacenull423 private fun ApproachMeasureScope.doNotPlace(
424 measurable: Measurable,
425 constraints: Constraints,
426 ): MeasureResult {
427 recursivelyClearPlacementValues()
428 stateInContent.lastSize = Element.SizeUnspecified
429
430 val placeable = measurable.measure(constraints)
431 return layout(placeable.width, placeable.height) { /* Do not place */ }
432 }
433
placeNormallynull434 private fun ApproachMeasureScope.placeNormally(
435 measurable: Measurable,
436 constraints: Constraints,
437 ): MeasureResult {
438 val placeable = measurable.measure(constraints)
439 stateInContent.lastSize = placeable.size()
440 return layout(placeable.width, placeable.height) {
441 coordinates?.let {
442 with(layoutImpl.lookaheadScope) {
443 stateInContent.lastOffset =
444 lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
445 }
446 }
447
448 placeable.place(0, 0)
449 }
450 }
451
Placeablenull452 private fun Placeable.PlacementScope.place(
453 elementState: TransitionState,
454 placeable: Placeable,
455 ) {
456 with(layoutImpl.lookaheadScope) {
457 // Update the offset (relative to the SceneTransitionLayout) this element has in this
458 // content when idle.
459 val coords =
460 coordinates ?: error("Element ${element.key} does not have any coordinates")
461
462 // No need to place the element in this content if we don't want to draw it anyways.
463 if (!shouldPlaceElement(layoutImpl, content.key, element, elementState)) {
464 recursivelyClearPlacementValues()
465 return
466 }
467
468 val transition = elementState as? TransitionState.Transition
469 val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
470 val targetOffset =
471 computeValue(
472 layoutImpl,
473 stateInContent,
474 element,
475 transition,
476 contentValue = { it.targetOffset },
477 transformation = { it.offset },
478 currentValue = { currentOffset },
479 isSpecified = { it != Offset.Unspecified },
480 ::lerp,
481 )
482
483 val interruptedOffset =
484 computeInterruptedValue(
485 layoutImpl,
486 transition,
487 value = targetOffset,
488 unspecifiedValue = Offset.Unspecified,
489 zeroValue = Offset.Zero,
490 getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption },
491 setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it },
492 getInterruptionDelta = { stateInContent.offsetInterruptionDelta },
493 setInterruptionDelta = { delta ->
494 setPlacementInterruptionDelta(
495 element = element,
496 stateInContent = stateInContent,
497 transition = transition,
498 delta = delta,
499 setter = { stateInContent, delta ->
500 stateInContent.offsetInterruptionDelta = delta
501 },
502 )
503 },
504 diff = { a, b -> a - b },
505 add = { a, b, bProgress -> a + b * bProgress },
506 )
507
508 stateInContent.lastOffset = interruptedOffset
509
510 val offset = (interruptedOffset - currentOffset).round()
511 if (
512 isElementOpaque(content, element, transition) &&
513 interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) ==
514 1f
515 ) {
516 stateInContent.lastAlpha = 1f
517
518 // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is
519 // not animated once b/305195729 is fixed. Test that drawing is not invalidated in
520 // that case.
521 placeable.place(offset)
522 } else {
523 placeable.placeWithLayer(offset) {
524 // This layer might still run on its own (outside of the placement phase) even
525 // if this element is not placed or composed anymore, so we need to double check
526 // again here before calling [elementAlpha] (which will update
527 // [SceneState.lastAlpha]). We also need to recompute the current transition to
528 // make sure that we are using the current transition and not a reference to an
529 // old one. See b/343138966 for details.
530 if (_element == null) {
531 return@placeWithLayer
532 }
533
534 val elementState = elementState(layoutImpl, element, currentTransitionStates)
535 if (
536 elementState == null ||
537 !shouldPlaceElement(layoutImpl, content.key, element, elementState)
538 ) {
539 return@placeWithLayer
540 }
541
542 val transition = elementState as? TransitionState.Transition
543 alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
544 compositingStrategy = CompositingStrategy.ModulateAlpha
545 }
546 }
547 }
548 }
549
550 /**
551 * Recursively clear the last placement values on this node and all descendants ElementNodes.
552 * This should be called when this node is not placed anymore, so that we correctly clear values
553 * for the descendants for which approachMeasure() won't be called.
554 */
recursivelyClearPlacementValuesnull555 private fun recursivelyClearPlacementValues() {
556 fun Element.State.clearLastPlacementValues() {
557 lastOffset = Offset.Unspecified
558 lastScale = Scale.Unspecified
559 lastAlpha = Element.AlphaUnspecified
560 }
561
562 stateInContent.clearLastPlacementValues()
563 traverseDescendants(ElementTraverseKey) { node ->
564 if ((node as ElementNode)._element != null) {
565 node.stateInContent.clearLastPlacementValues()
566 }
567 TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
568 }
569 }
570
drawnull571 override fun ContentDrawScope.draw() {
572 element.wasDrawnInAnyContent = true
573
574 val transition =
575 elementState(layoutImpl, element, currentTransitionStates)
576 as? TransitionState.Transition
577 val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
578 if (drawScale == Scale.Default) {
579 drawContent()
580 } else {
581 scale(
582 drawScale.scaleX,
583 drawScale.scaleY,
584 if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
585 ) {
586 this@draw.drawContent()
587 }
588 }
589 }
590
591 companion object {
592 private val ElementTraverseKey = Any()
593
maybePruneMapsnull594 private fun maybePruneMaps(
595 layoutImpl: SceneTransitionLayoutImpl,
596 element: Element,
597 stateInContent: Element.State,
598 ) {
599 fun pruneForContent(contentKey: ContentKey) {
600 // If element is not composed in this content anymore, remove the content values.
601 // This works because [onAttach] is called before [onDetach], so if an element is
602 // moved from the UI tree we will first add the new code location then remove the
603 // old one.
604 if (
605 stateInContent.nodes.isEmpty() &&
606 element.stateByContent[contentKey] == stateInContent
607 ) {
608 element.stateByContent.remove(contentKey)
609
610 // If the element is not composed in any content, remove it from the elements
611 // map.
612 if (
613 element.stateByContent.isEmpty() &&
614 layoutImpl.elements[element.key] == element
615 ) {
616 layoutImpl.elements.remove(element.key)
617 }
618 }
619 }
620
621 pruneForContent(stateInContent.content)
622 layoutImpl.ancestorContentKeys.forEach { content -> pruneForContent(content) }
623 }
624 }
625 }
626
627 /** The [TransitionState] that we should consider for [element]. */
elementStatenull628 private fun elementState(
629 layoutImpl: SceneTransitionLayoutImpl,
630 element: Element,
631 transitionStates: List<TransitionState>,
632 ): TransitionState? {
633 val state = elementState(transitionStates, isInContent = { it in element.stateByContent })
634
635 val transition = state as? TransitionState.Transition
636 val previousTransition = element.lastTransition
637 element.lastTransition = transition
638
639 if (transition != previousTransition && transition != null && previousTransition != null) {
640 // The previous transition was interrupted by another transition.
641 prepareInterruption(layoutImpl, element, transition, previousTransition)
642 } else if (transition == null && previousTransition != null) {
643 // The transition was just finished.
644 element.stateByContent.values.forEach {
645 it.clearValuesBeforeInterruption()
646 it.clearInterruptionDeltas()
647 }
648 }
649
650 return state
651 }
652
elementStatenull653 internal inline fun elementState(
654 transitionStates: List<TransitionState>,
655 isInContent: (ContentKey) -> Boolean,
656 ): TransitionState? {
657 val lastState = transitionStates.last()
658 if (lastState is TransitionState.Idle) {
659 check(transitionStates.size == 1)
660 return lastState
661 }
662
663 // Find the last transition with a content that contains the element.
664 transitionStates.fastForEachReversed { state ->
665 val transition = state as TransitionState.Transition
666 if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
667 return transition
668 }
669 }
670
671 return null
672 }
673
elementContentWhenIdlenull674 internal inline fun elementContentWhenIdle(
675 layoutImpl: SceneTransitionLayoutImpl,
676 currentState: TransitionState,
677 isInContent: (ContentKey) -> Boolean,
678 ): ContentKey {
679 val currentScene = currentState.currentScene
680 val overlays = currentState.currentOverlays
681 if (overlays.isEmpty()) {
682 return currentScene
683 }
684
685 // Find the overlay with highest zIndex that contains the element.
686 // TODO(b/353679003): Should we cache enabledOverlays into a List<> to avoid a lot of
687 // allocations here?
688 var currentOverlay: OverlayKey? = null
689 for (overlay in overlays) {
690 if (
691 isInContent(overlay) &&
692 (currentOverlay == null ||
693 (layoutImpl.overlay(overlay).zIndex >
694 layoutImpl.overlay(currentOverlay).zIndex))
695 ) {
696 currentOverlay = overlay
697 }
698 }
699
700 return currentOverlay ?: currentScene
701 }
702
prepareInterruptionnull703 private fun prepareInterruption(
704 layoutImpl: SceneTransitionLayoutImpl,
705 element: Element,
706 transition: TransitionState.Transition,
707 previousTransition: TransitionState.Transition,
708 ) {
709 if (transition.replacedTransition == previousTransition) {
710 return
711 }
712
713 val stateByContent = element.stateByContent
714 fun updateStateInContent(key: ContentKey): Element.State? {
715 return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() }
716 }
717
718 val previousFromState = updateStateInContent(previousTransition.fromContent)
719 val previousToState = updateStateInContent(previousTransition.toContent)
720 val fromState = updateStateInContent(transition.fromContent)
721 val toState = updateStateInContent(transition.toContent)
722
723 val previousUniqueState = reconcileStates(element, previousTransition, previousState = null)
724 reconcileStates(element, transition, previousState = previousUniqueState)
725
726 // Remove the interruption values to all contents but the content(s) where the element will be
727 // placed, to make sure that interruption deltas are computed only right after this interruption
728 // is prepared.
729 fun cleanInterruptionValues(stateInContent: Element.State) {
730 stateInContent.sizeInterruptionDelta = IntSize.Zero
731 stateInContent.offsetInterruptionDelta = Offset.Zero
732 stateInContent.alphaInterruptionDelta = 0f
733 stateInContent.scaleInterruptionDelta = Scale.Zero
734
735 if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) {
736 stateInContent.offsetBeforeInterruption = Offset.Unspecified
737 stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
738 stateInContent.scaleBeforeInterruption = Scale.Unspecified
739 }
740 }
741
742 previousFromState?.let { cleanInterruptionValues(it) }
743 previousToState?.let { cleanInterruptionValues(it) }
744 fromState?.let { cleanInterruptionValues(it) }
745 toState?.let { cleanInterruptionValues(it) }
746 }
747
748 /**
749 * Reconcile the state of [element] in the formContent and toContent of [transition] so that the
750 * values before interruption have their expected values, taking shared transitions into account.
751 *
752 * @return the unique state this element had during [transition], `null` if it had multiple
753 * different states (i.e. the shared animation was disabled).
754 */
reconcileStatesnull755 private fun reconcileStates(
756 element: Element,
757 transition: TransitionState.Transition,
758 previousState: Element.State?,
759 ): Element.State? {
760 fun reconcileWithPreviousState(state: Element.State) {
761 if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) {
762 state.updateValuesBeforeInterruption(previousState)
763 }
764 }
765
766 val fromContentState = element.stateByContent[transition.fromContent]
767 val toContentState = element.stateByContent[transition.toContent]
768
769 if (fromContentState == null || toContentState == null) {
770 return (fromContentState ?: toContentState)
771 ?.also { reconcileWithPreviousState(it) }
772 ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified }
773 }
774
775 if (!isSharedElementEnabled(element.key, transition)) {
776 return null
777 }
778
779 if (
780 fromContentState.offsetBeforeInterruption != Offset.Unspecified &&
781 toContentState.offsetBeforeInterruption == Offset.Unspecified
782 ) {
783 // Element is shared and placed in fromContent only.
784 toContentState.updateValuesBeforeInterruption(fromContentState)
785 return fromContentState
786 }
787
788 if (
789 toContentState.offsetBeforeInterruption != Offset.Unspecified &&
790 fromContentState.offsetBeforeInterruption == Offset.Unspecified
791 ) {
792 // Element is shared and placed in toContent only.
793 fromContentState.updateValuesBeforeInterruption(toContentState)
794 return toContentState
795 }
796
797 return null
798 }
799
Elementnull800 private fun Element.State.selfUpdateValuesBeforeInterruption() {
801 sizeBeforeInterruption = lastSize
802
803 if (lastAlpha > 0f) {
804 offsetBeforeInterruption = lastOffset
805 scaleBeforeInterruption = lastScale
806 alphaBeforeInterruption = lastAlpha
807 } else {
808 // Consider the element as not placed in this content if it was fully transparent.
809 // TODO(b/290930950): Look into using derived state inside place() instead to not even place
810 // the element at all when alpha == 0f.
811 offsetBeforeInterruption = Offset.Unspecified
812 scaleBeforeInterruption = Scale.Unspecified
813 alphaBeforeInterruption = Element.AlphaUnspecified
814 }
815 }
816
updateValuesBeforeInterruptionnull817 private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) {
818 offsetBeforeInterruption = lastState.offsetBeforeInterruption
819 sizeBeforeInterruption = lastState.sizeBeforeInterruption
820 scaleBeforeInterruption = lastState.scaleBeforeInterruption
821 alphaBeforeInterruption = lastState.alphaBeforeInterruption
822
823 clearInterruptionDeltas()
824 }
825
Elementnull826 private fun Element.State.clearInterruptionDeltas() {
827 offsetInterruptionDelta = Offset.Zero
828 sizeInterruptionDelta = IntSize.Zero
829 scaleInterruptionDelta = Scale.Zero
830 alphaInterruptionDelta = 0f
831 }
832
clearValuesBeforeInterruptionnull833 private fun Element.State.clearValuesBeforeInterruption() {
834 offsetBeforeInterruption = Offset.Unspecified
835 scaleBeforeInterruption = Scale.Unspecified
836 alphaBeforeInterruption = Element.AlphaUnspecified
837 }
838
839 /**
840 * Compute what [value] should be if we take the
841 * [interruption progress][ContentState.Transition.interruptionProgress] of [transition] into
842 * account.
843 */
computeInterruptedValuenull844 private inline fun <T> computeInterruptedValue(
845 layoutImpl: SceneTransitionLayoutImpl,
846 transition: TransitionState.Transition?,
847 value: T,
848 unspecifiedValue: T,
849 zeroValue: T,
850 getValueBeforeInterruption: () -> T,
851 setValueBeforeInterruption: (T) -> Unit,
852 getInterruptionDelta: () -> T,
853 setInterruptionDelta: (T) -> Unit,
854 diff: (a: T, b: T) -> T, // a - b
855 add: (a: T, b: T, bProgress: Float) -> T, // a + (b * bProgress)
856 ): T {
857 val valueBeforeInterruption = getValueBeforeInterruption()
858
859 // If the value before the interruption is specified, it means that this is the first time we
860 // compute [value] right after an interruption.
861 if (valueBeforeInterruption != unspecifiedValue) {
862 // Compute and store the delta between the value before the interruption and the current
863 // value.
864 setInterruptionDelta(diff(valueBeforeInterruption, value))
865
866 // Reset the value before interruption now that we processed it.
867 setValueBeforeInterruption(unspecifiedValue)
868 }
869
870 val delta = getInterruptionDelta()
871 return if (delta == zeroValue || transition == null) {
872 // There was no interruption or there is no transition: just return the value.
873 value
874 } else {
875 // Add `delta * interruptionProgress` to the value so that we animate to value.
876 val interruptionProgress = transition.interruptionProgress(layoutImpl)
877 if (interruptionProgress == 0f) {
878 value
879 } else {
880 add(value, delta, interruptionProgress)
881 }
882 }
883 }
884
885 /**
886 * Set the interruption delta of a *placement/drawing*-related value (offset, alpha, scale). This
887 * ensures that the delta is also set on the other content in the transition for shared elements, so
888 * that there is no jump cut if the content where the element is placed has changed.
889 */
setPlacementInterruptionDeltanull890 private inline fun <T> setPlacementInterruptionDelta(
891 element: Element,
892 stateInContent: Element.State,
893 transition: TransitionState.Transition?,
894 delta: T,
895 setter: (Element.State, T) -> Unit,
896 ) {
897 // Set the interruption delta on the current content.
898 setter(stateInContent, delta)
899
900 if (transition == null) {
901 return
902 }
903
904 // If the element is shared, also set the delta on the other content so that it is used by that
905 // content if we start overscrolling it and change the content where the element is placed.
906 val otherContent =
907 if (stateInContent.content == transition.fromContent) transition.toContent
908 else transition.fromContent
909 val otherContentState = element.stateByContent[otherContent] ?: return
910 if (isSharedElementEnabled(element.key, transition)) {
911 setter(otherContentState, delta)
912 }
913 }
914
shouldPlaceElementnull915 private fun shouldPlaceElement(
916 layoutImpl: SceneTransitionLayoutImpl,
917 content: ContentKey,
918 element: Element,
919 elementState: TransitionState,
920 ): Boolean {
921 if (element.key.placeAllCopies) {
922 return true
923 }
924
925 val transition =
926 when (elementState) {
927 is TransitionState.Idle -> {
928 return element.shouldBeRenderedBy(content) &&
929 content ==
930 elementContentWhenIdle(
931 layoutImpl,
932 elementState,
933 isInContent = { it in element.stateByContent },
934 )
935 }
936 is TransitionState.Transition -> elementState
937 }
938
939 // Don't place the element in this content if this content is not part of the current element
940 // transition.
941 val isReplacingOverlay = transition is TransitionState.Transition.ReplaceOverlay
942 if (
943 content != transition.fromContent &&
944 content != transition.toContent &&
945 (!isReplacingOverlay || content != transition.currentScene)
946 ) {
947 return false
948 }
949
950 // Place the element if it is not shared.
951 var copies = 0
952 if (transition.fromContent in element.stateByContent) copies++
953 if (transition.toContent in element.stateByContent) copies++
954 if (isReplacingOverlay && transition.currentScene in element.stateByContent) copies++
955 if (copies <= 1) {
956 return true
957 }
958
959 val sharedTransformation = sharedElementTransformation(element.key, transition)
960 if (sharedTransformation?.transformation?.enabled == false) {
961 return true
962 }
963
964 return shouldPlaceSharedElement(layoutImpl, content, element.key, transition)
965 }
966
967 /**
968 * Whether the element is opaque or not.
969 *
970 * Important: The logic here should closely match the logic in [elementAlpha]. Note that we don't
971 * reuse [elementAlpha] and simply check if alpha == 1f because [isElementOpaque] is checked during
972 * placement and we don't want to read the transition progress in that phase.
973 */
isElementOpaquenull974 private fun isElementOpaque(
975 content: Content,
976 element: Element,
977 transition: TransitionState.Transition?,
978 ): Boolean {
979 if (transition == null) {
980 return true
981 }
982
983 val fromState = element.stateByContent[transition.fromContent]
984 val toState = element.stateByContent[transition.toContent]
985
986 if (fromState == null && toState == null) {
987 // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
988 // run anymore.
989 return true
990 }
991
992 val isSharedElement = fromState != null && toState != null
993 if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
994 return true
995 }
996
997 return transition.transformationSpec.transformations(element.key, content.key).alpha == null
998 }
999
1000 /**
1001 * Whether the element is opaque or not.
1002 *
1003 * Important: The logic here should closely match the logic in [isElementOpaque]. Note that we don't
1004 * reuse [elementAlpha] in [isElementOpaque] and simply check if alpha == 1f because
1005 * [isElementOpaque] is checked during placement and we don't want to read the transition progress
1006 * in that phase.
1007 */
elementAlphanull1008 private fun elementAlpha(
1009 layoutImpl: SceneTransitionLayoutImpl,
1010 element: Element,
1011 transition: TransitionState.Transition?,
1012 stateInContent: Element.State,
1013 ): Float {
1014 val alpha =
1015 computeValue(
1016 layoutImpl,
1017 stateInContent,
1018 element,
1019 transition,
1020 contentValue = { 1f },
1021 transformation = { it.alpha },
1022 currentValue = { 1f },
1023 isSpecified = { true },
1024 ::lerp,
1025 )
1026 .fastCoerceIn(0f, 1f)
1027
1028 // If the element is fading during this transition and that it is drawn for the first time, make
1029 // sure that it doesn't instantly appear on screen.
1030 if (!element.wasDrawnInAnyContent && alpha > 0f) {
1031 element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f }
1032 }
1033
1034 val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha)
1035 stateInContent.lastAlpha = interruptedAlpha
1036 return interruptedAlpha
1037 }
1038
interruptedAlphanull1039 private fun interruptedAlpha(
1040 layoutImpl: SceneTransitionLayoutImpl,
1041 element: Element,
1042 transition: TransitionState.Transition?,
1043 stateInContent: Element.State,
1044 alpha: Float,
1045 ): Float {
1046 return computeInterruptedValue(
1047 layoutImpl,
1048 transition,
1049 value = alpha,
1050 unspecifiedValue = Element.AlphaUnspecified,
1051 zeroValue = 0f,
1052 getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption },
1053 setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it },
1054 getInterruptionDelta = { stateInContent.alphaInterruptionDelta },
1055 setInterruptionDelta = { delta ->
1056 setPlacementInterruptionDelta(
1057 element = element,
1058 stateInContent = stateInContent,
1059 transition = transition,
1060 delta = delta,
1061 setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta },
1062 )
1063 },
1064 diff = { a, b -> a - b },
1065 add = { a, b, bProgress -> a + b * bProgress },
1066 )
1067 }
1068
measurenull1069 private fun measure(
1070 layoutImpl: SceneTransitionLayoutImpl,
1071 element: Element,
1072 transition: TransitionState.Transition?,
1073 stateInContent: Element.State,
1074 measurable: Measurable,
1075 constraints: Constraints,
1076 ): Placeable {
1077 // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
1078 // case we store the resulting placeable here to make sure the element is not measured more than
1079 // once.
1080 var maybePlaceable: Placeable? = null
1081
1082 val targetSize =
1083 computeValue(
1084 layoutImpl,
1085 stateInContent,
1086 element,
1087 transition,
1088 contentValue = { it.targetSize },
1089 transformation = { it.size },
1090 currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
1091 isSpecified = { it != Element.SizeUnspecified },
1092 ::lerp,
1093 )
1094
1095 // The measurable was already measured, so we can't take interruptions into account here given
1096 // that we are not allowed to measure the same measurable twice.
1097 maybePlaceable?.let { placeable ->
1098 stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
1099 stateInContent.sizeInterruptionDelta = IntSize.Zero
1100 return placeable
1101 }
1102
1103 val interruptedSize =
1104 computeInterruptedValue(
1105 layoutImpl,
1106 transition,
1107 value = targetSize,
1108 unspecifiedValue = Element.SizeUnspecified,
1109 zeroValue = IntSize.Zero,
1110 getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption },
1111 setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it },
1112 getInterruptionDelta = { stateInContent.sizeInterruptionDelta },
1113 setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it },
1114 diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) },
1115 add = { a, b, bProgress ->
1116 IntSize(
1117 (a.width + b.width * bProgress).roundToInt(),
1118 (a.height + b.height * bProgress).roundToInt(),
1119 )
1120 },
1121 )
1122
1123 return measurable.measure(
1124 Constraints.fixed(
1125 interruptedSize.width.coerceAtLeast(0),
1126 interruptedSize.height.coerceAtLeast(0),
1127 )
1128 )
1129 }
1130
sizenull1131 private fun Placeable.size(): IntSize = IntSize(width, height)
1132
1133 private fun ContentDrawScope.getDrawScale(
1134 layoutImpl: SceneTransitionLayoutImpl,
1135 element: Element,
1136 transition: TransitionState.Transition?,
1137 stateInContent: Element.State,
1138 ): Scale {
1139 val scale =
1140 computeValue(
1141 layoutImpl,
1142 stateInContent,
1143 element,
1144 transition,
1145 contentValue = { Scale.Default },
1146 transformation = { it.drawScale },
1147 currentValue = { Scale.Default },
1148 isSpecified = { true },
1149 ::lerp,
1150 )
1151
1152 fun Offset.specifiedOrCenter(): Offset {
1153 return this.takeIf { isSpecified } ?: center
1154 }
1155
1156 val interruptedScale =
1157 computeInterruptedValue(
1158 layoutImpl,
1159 transition,
1160 value = scale,
1161 unspecifiedValue = Scale.Unspecified,
1162 zeroValue = Scale.Zero,
1163 getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption },
1164 setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it },
1165 getInterruptionDelta = { stateInContent.scaleInterruptionDelta },
1166 setInterruptionDelta = { delta ->
1167 setPlacementInterruptionDelta(
1168 element = element,
1169 stateInContent = stateInContent,
1170 transition = transition,
1171 delta = delta,
1172 setter = { stateInContent, delta ->
1173 stateInContent.scaleInterruptionDelta = delta
1174 },
1175 )
1176 },
1177 diff = { a, b ->
1178 Scale(
1179 scaleX = a.scaleX - b.scaleX,
1180 scaleY = a.scaleY - b.scaleY,
1181 pivot =
1182 if (a.pivot.isUnspecified && b.pivot.isUnspecified) {
1183 Offset.Unspecified
1184 } else {
1185 a.pivot.specifiedOrCenter() - b.pivot.specifiedOrCenter()
1186 },
1187 )
1188 },
1189 add = { a, b, bProgress ->
1190 Scale(
1191 scaleX = a.scaleX + b.scaleX * bProgress,
1192 scaleY = a.scaleY + b.scaleY * bProgress,
1193 pivot =
1194 if (a.pivot.isUnspecified && b.pivot.isUnspecified) {
1195 Offset.Unspecified
1196 } else {
1197 a.pivot.specifiedOrCenter() + b.pivot.specifiedOrCenter() * bProgress
1198 },
1199 )
1200 },
1201 )
1202
1203 stateInContent.lastScale = interruptedScale
1204 return interruptedScale
1205 }
1206
1207 /**
1208 * Return the value that should be used depending on the current layout state and transition.
1209 *
1210 * Important: This function must remain inline because of all the lambda parameters. These lambdas
1211 * are necessary because getting some of them might require some computation, like measuring a
1212 * Measurable.
1213 *
1214 * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
1215 * @param currentContentState the content state of the content for which we are computing the value.
1216 * Note that during interruptions, this could be the state of a content that is neither
1217 * [transition.toContent] nor [transition.fromContent].
1218 * @param element the element being animated.
1219 * @param contentValue the value being animated.
1220 * @param transformation the transformation associated to the value being animated.
1221 * @param currentValue the value that would be used if it is not transformed. Note that this is
1222 * different than [idleValue] even if the value is not transformed directly because it could be
1223 * impacted by the transformations on other elements, like a parent that is being translated or
1224 * resized.
1225 * @param lerp the linear interpolation function used to interpolate between two values of this
1226 * value type.
1227 */
computeValuenull1228 private inline fun <T> computeValue(
1229 layoutImpl: SceneTransitionLayoutImpl,
1230 currentContentState: Element.State,
1231 element: Element,
1232 transition: TransitionState.Transition?,
1233 contentValue: (Element.State) -> T,
1234 transformation: (ElementTransformations) -> TransformationWithRange<PropertyTransformation<T>>?,
1235 currentValue: () -> T,
1236 isSpecified: (T) -> Boolean,
1237 lerp: (T, T, Float) -> T,
1238 ): T {
1239 if (transition == null) {
1240 // There is no ongoing transition. Even if this element SceneTransitionLayout is not
1241 // animated, the layout itself might be animated (e.g. by another parent
1242 // SceneTransitionLayout), in which case this element still need to participate in the
1243 // layout phase.
1244 return currentValue()
1245 }
1246
1247 val fromContent = transition.fromContent
1248 val toContent = transition.toContent
1249
1250 val fromState = element.stateByContent[fromContent]
1251 val toState = element.stateByContent[toContent]
1252
1253 if (fromState == null && toState == null) {
1254 // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
1255 // run anymore.
1256 return contentValue(currentContentState)
1257 }
1258
1259 val currentContent = currentContentState.content
1260 if (transition is TransitionState.HasOverscrollProperties) {
1261 val overscroll = transition.currentOverscrollSpec
1262 if (overscroll?.content == currentContent) {
1263 val elementSpec =
1264 overscroll.transformationSpec.transformations(element.key, currentContent)
1265 val propertySpec = transformation(elementSpec) ?: return currentValue()
1266 val overscrollState =
1267 checkNotNull(if (currentContent == toContent) toState else fromState)
1268 val idleValue = contentValue(overscrollState)
1269 val targetValue =
1270 with(
1271 propertySpec.transformation.requireInterpolatedTransformation(
1272 element,
1273 transition,
1274 ) {
1275 "Custom transformations in overscroll specs should not be possible"
1276 }
1277 ) {
1278 layoutImpl.propertyTransformationScope.transform(
1279 currentContent,
1280 element.key,
1281 transition,
1282 idleValue,
1283 )
1284 }
1285
1286 // Make sure we don't read progress if values are the same and we don't need to
1287 // interpolate, so we don't invalidate the phase where this is read.
1288 if (targetValue == idleValue) {
1289 return targetValue
1290 }
1291
1292 // TODO(b/290184746): Make sure that we don't overflow transformations associated to a
1293 // range.
1294 val directionSign = if (transition.isUpOrLeft) -1 else 1
1295 val isToContent = overscroll.content == transition.toContent
1296 val linearProgress = transition.progress.let { if (isToContent) it - 1f else it }
1297 val progressConverter =
1298 overscroll.progressConverter
1299 ?: layoutImpl.state.transitions.defaultProgressConverter
1300 val progress = directionSign * progressConverter.convert(linearProgress)
1301 val rangeProgress = propertySpec.range?.progress(progress) ?: progress
1302
1303 // Interpolate between the value at rest and the over scrolled value.
1304 return lerp(idleValue, targetValue, rangeProgress)
1305 }
1306 }
1307
1308 // The element is shared: interpolate between the value in fromContent and the value in
1309 // toContent.
1310 // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
1311 // elements follow the finger direction.
1312 val isSharedElement = fromState != null && toState != null
1313 if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
1314 return interpolateSharedElement(
1315 transition = transition,
1316 contentValue = contentValue,
1317 fromState = fromState!!,
1318 toState = toState!!,
1319 isSpecified = isSpecified,
1320 lerp = lerp,
1321 )
1322 }
1323
1324 // If we are replacing an overlay and the element is both in a single overlay and in the current
1325 // scene, interpolate the state of the element using the current scene as the other scene.
1326 var currentSceneState: Element.State? = null
1327 if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) {
1328 currentSceneState = element.stateByContent[transition.currentScene]
1329 if (currentSceneState != null && isSharedElementEnabled(element.key, transition)) {
1330 return interpolateSharedElement(
1331 transition = transition,
1332 contentValue = contentValue,
1333 fromState = fromState ?: currentSceneState,
1334 toState = toState ?: currentSceneState,
1335 isSpecified = isSpecified,
1336 lerp = lerp,
1337 )
1338 }
1339 }
1340
1341 // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
1342 // end (for leaving elements) of the transition.
1343 val contentState =
1344 checkNotNull(
1345 when {
1346 isSharedElement && currentContent == fromContent -> fromState
1347 isSharedElement -> toState
1348 currentSceneState != null && currentContent == transition.currentScene ->
1349 currentSceneState
1350 else -> fromState ?: toState
1351 }
1352 )
1353
1354 // The content for which we compute the transformation. Note that this is not necessarily
1355 // [currentContent] because [currentContent] could be a different content than the transition
1356 // fromContent or toContent during interruptions.
1357 val content = contentState.content
1358
1359 val transformationWithRange =
1360 transformation(transition.transformationSpec.transformations(element.key, content))
1361
1362 val previewTransformation =
1363 transition.previewTransformationSpec?.let {
1364 transformation(it.transformations(element.key, content))
1365 }
1366 if (previewTransformation != null) {
1367 val isInPreviewStage = transition.isInPreviewStage
1368
1369 val idleValue = contentValue(contentState)
1370 val isEntering = content == toContent
1371 val previewTargetValue =
1372 with(
1373 previewTransformation.transformation.requireInterpolatedTransformation(
1374 element,
1375 transition,
1376 ) {
1377 "Custom transformations in preview specs should not be possible"
1378 }
1379 ) {
1380 layoutImpl.propertyTransformationScope.transform(
1381 content,
1382 element.key,
1383 transition,
1384 idleValue,
1385 )
1386 }
1387
1388 val targetValueOrNull =
1389 transformationWithRange?.let { transformation ->
1390 with(
1391 transformation.transformation.requireInterpolatedTransformation(
1392 element,
1393 transition,
1394 ) {
1395 "Custom transformations are not allowed for properties with a preview"
1396 }
1397 ) {
1398 layoutImpl.propertyTransformationScope.transform(
1399 content,
1400 element.key,
1401 transition,
1402 idleValue,
1403 )
1404 }
1405 }
1406
1407 // Make sure we don't read progress if values are the same and we don't need to interpolate,
1408 // so we don't invalidate the phase where this is read.
1409 when {
1410 isInPreviewStage && isEntering && previewTargetValue == targetValueOrNull ->
1411 return previewTargetValue
1412 isInPreviewStage && !isEntering && idleValue == previewTargetValue -> return idleValue
1413 previewTargetValue == targetValueOrNull && idleValue == previewTargetValue ->
1414 return idleValue
1415 else -> {}
1416 }
1417
1418 val previewProgress = transition.previewProgress
1419 // progress is not needed for all cases of the below when block, therefore read it lazily
1420 // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range
1421 val previewRangeProgress =
1422 previewTransformation.range?.progress(previewProgress) ?: previewProgress
1423
1424 if (isInPreviewStage) {
1425 // if we're in the preview stage of the transition, interpolate between start state and
1426 // preview target state:
1427 return if (isEntering) {
1428 // i.e. in the entering case between previewTargetValue and targetValue (or
1429 // idleValue if no transformation is defined in the second stage transition)...
1430 lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress)
1431 } else {
1432 // ...and in the exiting case between the idleValue and the previewTargetValue.
1433 lerp(idleValue, previewTargetValue, previewRangeProgress)
1434 }
1435 }
1436
1437 // if we're in the second stage of the transition, interpolate between the state the
1438 // element was left at the end of the preview-phase and the target state:
1439 return if (isEntering) {
1440 // i.e. in the entering case between preview-end-state and the idleValue...
1441 lerp(
1442 lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress),
1443 idleValue,
1444 transformationWithRange?.range?.progress(transition.progress) ?: transition.progress,
1445 )
1446 } else {
1447 if (targetValueOrNull == null) {
1448 // ... and in the exiting case, the element should remain in the preview-end-state
1449 // if no further transformation is defined in the second-stage transition...
1450 lerp(idleValue, previewTargetValue, previewRangeProgress)
1451 } else {
1452 // ...and otherwise it should be interpolated between preview-end-state and
1453 // targetValue
1454 lerp(
1455 lerp(idleValue, previewTargetValue, previewRangeProgress),
1456 targetValueOrNull,
1457 transformationWithRange.range?.progress(transition.progress)
1458 ?: transition.progress,
1459 )
1460 }
1461 }
1462 }
1463
1464 if (transformationWithRange == null) {
1465 // If there is no transformation explicitly associated to this element value, let's use
1466 // the value given by the system (like the current position and size given by the layout
1467 // pass).
1468 return currentValue()
1469 }
1470
1471 val transformation = transformationWithRange.transformation
1472 when (transformation) {
1473 is CustomPropertyTransformation ->
1474 return with(transformation) {
1475 layoutImpl.propertyTransformationScope.transform(
1476 content,
1477 element.key,
1478 transition,
1479 transition.coroutineScope,
1480 )
1481 }
1482 is InterpolatedPropertyTransformation -> {
1483 /* continue */
1484 }
1485 }
1486
1487 val idleValue = contentValue(contentState)
1488 val targetValue =
1489 with(transformation) {
1490 layoutImpl.propertyTransformationScope.transform(
1491 content,
1492 element.key,
1493 transition,
1494 idleValue,
1495 )
1496 }
1497
1498 // Make sure we don't read progress if values are the same and we don't need to interpolate, so
1499 // we don't invalidate the phase where this is read.
1500 if (targetValue == idleValue) {
1501 return targetValue
1502 }
1503
1504 val progress = transition.progress
1505 // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
1506 val rangeProgress = transformationWithRange.range?.progress(progress) ?: progress
1507
1508 // Interpolate between the value at rest and the value before entering/after leaving.
1509 val isEntering =
1510 when {
1511 content == toContent -> true
1512 content == fromContent -> false
1513 content == transition.currentScene -> toState == null
1514 else -> content == toContent
1515 }
1516 return if (isEntering) {
1517 lerp(targetValue, idleValue, rangeProgress)
1518 } else {
1519 lerp(idleValue, targetValue, rangeProgress)
1520 }
1521 }
1522
requireInterpolatedTransformationnull1523 private inline fun <T> PropertyTransformation<T>.requireInterpolatedTransformation(
1524 element: Element,
1525 transition: TransitionState.Transition,
1526 errorMessage: () -> String,
1527 ): InterpolatedPropertyTransformation<T> {
1528 return when (this) {
1529 is InterpolatedPropertyTransformation -> this
1530 is CustomPropertyTransformation -> {
1531 val elem = element.key.debugName
1532 val fromContent = transition.fromContent
1533 val toContent = transition.toContent
1534 error("${errorMessage()} (element=$elem fromContent=$fromContent toContent=$toContent)")
1535 }
1536 }
1537 }
1538
interpolateSharedElementnull1539 private inline fun <T> interpolateSharedElement(
1540 transition: TransitionState.Transition,
1541 contentValue: (Element.State) -> T,
1542 fromState: Element.State,
1543 toState: Element.State,
1544 isSpecified: (T) -> Boolean,
1545 lerp: (T, T, Float) -> T,
1546 ): T {
1547 val start = contentValue(fromState)
1548 val end = contentValue(toState)
1549
1550 // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
1551 // nodes before the intermediate layout pass.
1552 if (!isSpecified(start)) return end
1553 if (!isSpecified(end)) return start
1554
1555 // Make sure we don't read progress if values are the same and we don't need to interpolate,
1556 // so we don't invalidate the phase where this is read.
1557 return if (start == end) start else lerp(start, end, transition.progress)
1558 }
1559