• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 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.fastAll
49 import androidx.compose.ui.util.fastAny
50 import androidx.compose.ui.util.fastCoerceIn
51 import androidx.compose.ui.util.fastForEach
52 import androidx.compose.ui.util.fastForEachIndexed
53 import androidx.compose.ui.util.fastForEachReversed
54 import androidx.compose.ui.util.lerp
55 import com.android.compose.animation.scene.Element.Companion.SizeUnspecified
56 import com.android.compose.animation.scene.content.Content
57 import com.android.compose.animation.scene.content.state.TransitionState
58 import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
59 import com.android.compose.animation.scene.transformation.InterpolatedPropertyTransformation
60 import com.android.compose.animation.scene.transformation.PropertyTransformation
61 import com.android.compose.animation.scene.transformation.TransformationWithRange
62 import com.android.compose.modifiers.thenIf
63 import com.android.compose.ui.graphics.drawInContainer
64 import com.android.compose.ui.util.lerp
65 import kotlin.math.roundToInt
66 import kotlinx.coroutines.launch
67 
68 /** An element on screen, that can be composed in one or more contents. */
69 @Stable
70 internal class Element(val key: ElementKey) {
71     /** The mapping between a content and the state this element has in that content, if any. */
72     // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
73     // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
74     val stateByContent = SnapshotStateMap<ContentKey, State>()
75 
76     /**
77      * The last transition that was used when computing the state (size, position and alpha) of this
78      * element in any content, or `null` if it was last laid out when idle.
79      */
80     var lastTransition: TransitionState.Transition? = null
81 
82     /** Whether this element was ever drawn in a content. */
83     var wasDrawnInAnyContent = false
84 
85     override fun toString(): String {
86         return "Element(key=$key)"
87     }
88 
89     /** The last and target state of this element in a given content. */
90     @Stable
91     class State(
92         /**
93          * A list of contents where this element state finds itself in. The last content is the
94          * content of the STL which is actually responsible to compose and place this element. The
95          * other contents (if any) are the ancestors. The ancestors do not actually place this
96          * element but the element is part of the ancestors scene as part of a NestedSTL. The state
97          * can be accessed by ancestor transitions to read the properties of this element to compute
98          * transformations.
99          */
100         val contents: List<ContentKey>
101     ) {
102         /**
103          * The *target* state of this element in this content, i.e. the state of this element when
104          * we are idle on this content.
105          */
106         var targetSize by mutableStateOf(SizeUnspecified)
107         var targetOffset by mutableStateOf(Offset.Unspecified)
108 
109         /**
110          * The *approach* state of this element in this content, i.e. the intermediate layout state
111          * during transitions, used for smooth animation. Note: These values are computed before
112          * measuring the children.
113          */
114         var approachSize by mutableStateOf(SizeUnspecified)
115 
116         /** The last state this element had in this content. */
117         var lastOffset = Offset.Unspecified
118         var lastSize = SizeUnspecified
119         var lastScale = Scale.Unspecified
120         var lastAlpha = AlphaUnspecified
121 
122         /**
123          * The state of this element in this content right before the last interruption (if any).
124          */
125         var offsetBeforeInterruption = Offset.Unspecified
126         var sizeBeforeInterruption = SizeUnspecified
127         var scaleBeforeInterruption = Scale.Unspecified
128         var alphaBeforeInterruption = AlphaUnspecified
129 
130         /**
131          * The delta values to add to this element state to have smoother interruptions. These
132          * should be multiplied by the
133          * [current interruption progress][ContentState.Transition.interruptionProgress] so that
134          * they nicely animate from their values down to 0.
135          */
136         var offsetInterruptionDelta = Offset.Zero
137         var sizeInterruptionDelta = IntSize.Zero
138         var scaleInterruptionDelta = Scale.Zero
139         var alphaInterruptionDelta = 0f
140 
141         /**
142          * The attached [ElementNode] a Modifier.element() for a given element and content. During
143          * composition, this set could have 0 to 2 elements. After composition and after all
144          * modifier nodes have been attached/detached, this set should contain exactly 1 element.
145          */
146         val nodes = mutableSetOf<ElementNode>()
147     }
148 
149     companion object {
150         val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
151         val AlphaUnspecified = Float.MAX_VALUE
152     }
153 }
154 
155 data class Scale(val scaleX: Float, val scaleY: Float, val pivot: Offset = Offset.Unspecified) {
156     companion object {
157         val Default = Scale(1f, 1f, Offset.Unspecified)
158         val Zero = Scale(0f, 0f, Offset.Zero)
159         val Unspecified = Scale(Float.MAX_VALUE, Float.MAX_VALUE, Offset.Unspecified)
160     }
161 }
162 
163 /** The implementation of [ContentScope.element]. */
164 @Stable
elementnull165 internal fun Modifier.element(
166     layoutImpl: SceneTransitionLayoutImpl,
167     content: Content,
168     key: ElementKey,
169 ): Modifier {
170     // Make sure that we read the current transitions during composition and not during
171     // layout/drawing.
172     // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
173     // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
174     val currentTransitionStates = getAllNestedTransitionStates(layoutImpl)
175 
176     return thenIf(layoutImpl.state.isElevationPossible(content.key, key)) {
177             Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
178         }
179         .then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
180         .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
181 }
182 
183 /**
184  * Returns the transition states of all ancestors + the transition state of the current STL. The
185  * last element is the transition state of the local STL (the one with the highest nestingDepth).
186  *
187  * @return Each transition state of a STL is a List and this is a list of all the states.
188  */
getAllNestedTransitionStatesnull189 internal fun getAllNestedTransitionStates(
190     layoutImpl: SceneTransitionLayoutImpl
191 ): List<List<TransitionState>> {
192     return buildList {
193         layoutImpl.ancestors.fastForEach { add(it.layoutImpl.state.transitionStates) }
194         add(layoutImpl.state.transitionStates)
195     }
196 }
197 
maybeElevateInContentnull198 private fun Modifier.maybeElevateInContent(
199     layoutImpl: SceneTransitionLayoutImpl,
200     content: Content,
201     key: ElementKey,
202     transitionStates: List<List<TransitionState>>,
203 ): Modifier {
204     fun isSharedElement(
205         stateByContent: Map<ContentKey, Element.State>,
206         transition: TransitionState.Transition,
207     ): Boolean {
208         fun inFromContent() = transition.fromContent in stateByContent
209         fun inToContent() = transition.toContent in stateByContent
210         fun inCurrentScene() = transition.currentScene in stateByContent
211 
212         return if (transition is TransitionState.Transition.ReplaceOverlay) {
213             (inFromContent() && (inToContent() || inCurrentScene())) ||
214                 (inToContent() && inCurrentScene())
215         } else {
216             inFromContent() && inToContent()
217         }
218     }
219 
220     return drawInContainer(
221         content.containerState,
222         enabled = {
223             val stateByContent = layoutImpl.elements.getValue(key).stateByContent
224             val state = elementState(transitionStates, key, isInContent = { it in stateByContent })
225 
226             state is TransitionState.Transition &&
227                 state.transformationSpec
228                     .transformations(key, content.key)
229                     ?.shared
230                     ?.transformation
231                     ?.elevateInContent == content.key &&
232                 isSharedElement(stateByContent, state) &&
233                 isSharedElementEnabled(key, state) &&
234                 shouldPlaceElement(
235                     layoutImpl,
236                     content.key,
237                     layoutImpl.elements.getValue(key),
238                     state,
239                 )
240         },
241     )
242 }
243 
244 /**
245  * An element associated to [ElementNode]. Note that this element does not support updates as its
246  * arguments should always be the same.
247  */
248 internal data class ElementModifier(
249     internal val layoutImpl: SceneTransitionLayoutImpl,
250     private val currentTransitionStates: List<List<TransitionState>>,
251     internal val content: Content,
252     internal val key: ElementKey,
253 ) : ModifierNodeElement<ElementNode>() {
createnull254     override fun create(): ElementNode =
255         ElementNode(layoutImpl, currentTransitionStates, content, key)
256 
257     override fun update(node: ElementNode) {
258         node.update(layoutImpl, currentTransitionStates, content, key)
259     }
260 }
261 
262 internal class ElementNode(
263     private var layoutImpl: SceneTransitionLayoutImpl,
264     private var currentTransitionStates: List<List<TransitionState>>,
265     private var content: Content,
266     private var key: ElementKey,
267 ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
268     private var _element: Element? = null
269     private val element: Element
270         get() = _element!!
271 
272     private val stateInContent: Element.State
273         get() = element.stateByContent.getValue(content.key)
274 
275     override val traverseKey: Any = ElementTraverseKey
276 
onAttachnull277     override fun onAttach() {
278         super.onAttach()
279         updateElementAndContentValues()
280         addNodeToContentState()
281     }
282 
updateElementAndContentValuesnull283     private fun updateElementAndContentValues() {
284         val element =
285             layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
286         _element = element
287         if (!element.stateByContent.contains(content.key)) {
288             val contents = buildList {
289                 layoutImpl.ancestors.fastForEach { add(it.inContent) }
290                 add(content.key)
291             }
292 
293             val elementState = Element.State(contents)
294             element.stateByContent[content.key] = elementState
295 
296             layoutImpl.ancestors.fastForEach {
297                 element.stateByContent.putIfAbsent(it.inContent, elementState)
298             }
299         }
300     }
301 
addNodeToContentStatenull302     private fun addNodeToContentState() {
303         stateInContent.nodes.add(this)
304 
305         coroutineScope.launch {
306             // At this point all [CodeLocationNode] have been attached or detached, which means that
307             // [elementState.codeLocations] should have exactly 1 element, otherwise this means that
308             // this element was composed multiple times in the same content.
309             val nCodeLocations = stateInContent.nodes.size
310             if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
311                 error("$key was composed $nCodeLocations times in ${stateInContent.contents}")
312             }
313         }
314     }
315 
onDetachnull316     override fun onDetach() {
317         super.onDetach()
318         removeNodeFromContentState()
319         maybePruneMaps(layoutImpl, element, stateInContent)
320 
321         _element = null
322     }
323 
removeNodeFromContentStatenull324     private fun removeNodeFromContentState() {
325         stateInContent.nodes.remove(this)
326     }
327 
updatenull328     fun update(
329         layoutImpl: SceneTransitionLayoutImpl,
330         currentTransitionStates: List<List<TransitionState>>,
331         content: Content,
332         key: ElementKey,
333     ) {
334         check(layoutImpl == this.layoutImpl && content == this.content)
335         this.currentTransitionStates = currentTransitionStates
336 
337         removeNodeFromContentState()
338 
339         val prevElement = this.element
340         val prevElementState = this.stateInContent
341         this.key = key
342         updateElementAndContentValues()
343 
344         addNodeToContentState()
345         maybePruneMaps(layoutImpl, prevElement, prevElementState)
346     }
347 
isMeasurementApproachInProgressnull348     override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
349         // TODO(b/324191441): Investigate whether making this check more complex (checking if this
350         // element is shared or transformed) would lead to better performance.
351         val isTransitioning = isAnyStateTransitioning()
352         if (!isTransitioning) {
353             stateInContent.approachSize = SizeUnspecified
354         }
355         return isTransitioning
356     }
357 
isPlacementApproachInProgressnull358     override fun Placeable.PlacementScope.isPlacementApproachInProgress(
359         lookaheadCoordinates: LayoutCoordinates
360     ): Boolean {
361         // TODO(b/324191441): Investigate whether making this check more complex (checking if this
362         // element is shared or transformed) would lead to better performance.
363         return isAnyStateTransitioning()
364     }
365 
isAnyStateTransitioningnull366     private fun isAnyStateTransitioning(): Boolean {
367         return layoutImpl.state.isTransitioning() ||
368             layoutImpl.ancestors.fastAny { it.layoutImpl.state.isTransitioning() }
369     }
370 
371     @ExperimentalComposeUiApi
measurenull372     override fun MeasureScope.measure(
373         measurable: Measurable,
374         constraints: Constraints,
375     ): MeasureResult {
376         check(isLookingAhead)
377 
378         return measurable.measure(constraints).run {
379             // Update the size this element has in this content when idle.
380             stateInContent.targetSize = size()
381 
382             layout(width, height) {
383                 // Update the offset (relative to the SceneTransitionLayout) this element has in
384                 // this content when idle.
385                 coordinates?.let { coords ->
386                     with(layoutImpl.lookaheadScope) {
387                         stateInContent.targetOffset =
388                             lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
389                     }
390                 }
391                 place(0, 0)
392             }
393         }
394     }
395 
approachMeasurenull396     override fun ApproachMeasureScope.approachMeasure(
397         measurable: Measurable,
398         constraints: Constraints,
399     ): MeasureResult {
400         val elementState = elementState(layoutImpl, element, currentTransitionStates)
401         if (elementState == null) {
402             // If the element is not part of any transition, place it normally in its idle scene.
403             // This is the case if for example a transition between two overlays is ongoing where
404             // sharedElement isn't part of either but the element is still rendered as part of
405             // the underlying scene that is currently not being transitioned.
406             val currentState = currentTransitionStates.last().last()
407             stateInContent.approachSize = Element.SizeUnspecified
408             val shouldPlaceInThisContent =
409                 elementContentWhenIdle(
410                     layoutImpl,
411                     currentState,
412                     isInContent = { it in element.stateByContent },
413                 ) == content.key
414             return if (shouldPlaceInThisContent) {
415                 placeNormally(measurable, constraints)
416             } else {
417                 doNotPlace(measurable, constraints)
418             }
419         }
420         syncAncestorElementState()
421 
422         val transition = elementState as? TransitionState.Transition
423 
424         val placeable =
425             approachMeasure(
426                 layoutImpl = layoutImpl,
427                 element = element,
428                 transition = transition,
429                 stateInContent = stateInContent,
430                 measurable = measurable,
431                 constraints = constraints,
432             )
433         stateInContent.lastSize = placeable.size()
434         return layout(placeable.width, placeable.height) { place(elementState, placeable) }
435     }
436 
doNotPlacenull437     private fun ApproachMeasureScope.doNotPlace(
438         measurable: Measurable,
439         constraints: Constraints,
440     ): MeasureResult {
441         recursivelyClearPlacementValues()
442         stateInContent.lastSize = Element.SizeUnspecified
443 
444         val placeable = measurable.measure(constraints)
445         return layout(placeable.width, placeable.height) { /* Do not place */ }
446     }
447 
placeNormallynull448     private fun ApproachMeasureScope.placeNormally(
449         measurable: Measurable,
450         constraints: Constraints,
451     ): MeasureResult {
452         val placeable = measurable.measure(constraints)
453         stateInContent.lastSize = placeable.size()
454         return layout(placeable.width, placeable.height) {
455             coordinates?.let {
456                 with(layoutImpl.lookaheadScope) {
457                     stateInContent.lastOffset =
458                         lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
459                 }
460             }
461 
462             placeable.place(0, 0)
463         }
464     }
465 
Placeablenull466     private fun Placeable.PlacementScope.place(
467         elementState: TransitionState,
468         placeable: Placeable,
469     ) {
470         with(layoutImpl.lookaheadScope) {
471             // Update the offset (relative to the SceneTransitionLayout) this element has in this
472             // content when idle.
473             val coords =
474                 coordinates ?: error("Element ${element.key} does not have any coordinates")
475 
476             // No need to place the element in this content if we don't want to draw it anyways.
477             if (!shouldPlaceElement(layoutImpl, content.key, element, elementState)) {
478                 recursivelyClearPlacementValues()
479                 return
480             }
481 
482             val transition = elementState as? TransitionState.Transition
483             val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
484             val targetOffset =
485                 computeValue(
486                     layoutImpl,
487                     stateInContent,
488                     element,
489                     transition,
490                     contentValue = { it.targetOffset },
491                     transformation = { it?.offset },
492                     currentValue = { currentOffset },
493                     isSpecified = { it != Offset.Unspecified },
494                     ::lerp,
495                 )
496 
497             val interruptedOffset =
498                 computeInterruptedValue(
499                     layoutImpl,
500                     transition,
501                     value = targetOffset,
502                     unspecifiedValue = Offset.Unspecified,
503                     zeroValue = Offset.Zero,
504                     getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption },
505                     setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it },
506                     getInterruptionDelta = { stateInContent.offsetInterruptionDelta },
507                     setInterruptionDelta = { delta ->
508                         setPlacementInterruptionDelta(
509                             element = element,
510                             stateInContent = stateInContent,
511                             transition = transition,
512                             delta = delta,
513                             setter = { stateInContent, delta ->
514                                 stateInContent.offsetInterruptionDelta = delta
515                             },
516                         )
517                     },
518                     diff = { a, b -> a - b },
519                     add = { a, b, bProgress -> a + b * bProgress },
520                 )
521 
522             stateInContent.lastOffset = interruptedOffset
523 
524             val offset = (interruptedOffset - currentOffset).round()
525             if (
526                 isElementOpaque(content, element, transition) &&
527                     interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) ==
528                         1f
529             ) {
530                 stateInContent.lastAlpha = 1f
531 
532                 // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is
533                 // not animated once b/305195729 is fixed. Test that drawing is not invalidated in
534                 // that case.
535                 placeable.place(offset)
536             } else {
537                 placeable.placeWithLayer(offset) {
538                     // This layer might still run on its own (outside of the placement phase) even
539                     // if this element is not placed or composed anymore, so we need to double check
540                     // again here before calling [elementAlpha] (which will update
541                     // [SceneState.lastAlpha]). We also need to recompute the current transition to
542                     // make sure that we are using the current transition and not a reference to an
543                     // old one. See b/343138966 for details.
544                     if (_element == null) {
545                         return@placeWithLayer
546                     }
547 
548                     val elementState = elementState(layoutImpl, element, currentTransitionStates)
549                     if (
550                         elementState == null ||
551                             !shouldPlaceElement(layoutImpl, content.key, element, elementState)
552                     ) {
553                         return@placeWithLayer
554                     }
555 
556                     val transition = elementState as? TransitionState.Transition
557                     alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
558                     compositingStrategy = CompositingStrategy.ModulateAlpha
559                 }
560             }
561         }
562     }
563 
564     /**
565      * This method makes sure that the ancestor element state is *roughly* in sync. Assume we have
566      * the following nested scenes:
567      * ```
568      *       /   \
569      *     P1     P2
570      *   /   \
571      *  C1   C2
572      * ```
573      *
574      * We write the state of the shared element into its parent P1 even though P1 does not contain
575      * the element directly but it's part of its NestedSTL instead. This value is used to
576      * interpolate transitions on higher levels, e.g. between P1 and P2. Technically the best
577      * solution would be to always write the fully interpolated state into P1 but because this
578      * interferes with `computeValue` computations of other transitions this solution requires more
579      * sophistication and additional invocations of `computeValue`. We might want to aim for such a
580      * solution in the future when we allocate resources to that feature. For now, we only roughly
581      * set the state of P1 to either C1 or C2 based on heuristics.
582      *
583      * If we assign the P1 state just on attach/detach of a scene like we do for C1 and C2, this
584      * leads to problems where P1 can either become stale or is erased. This leads to situations
585      * where a shared element is not animated anymore.
586      *
587      * With this solution we track the transition state of the local transition at all times and set
588      * P1 based on the currentScene or overlay. In certain sequences of transition this may create
589      * jump cuts of a shared element mainly because of two reasons:
590      *
591      * a) P1 state is modified during a transition of P1 and X. Due to the new value it may jump cut
592      * when the interruption system is not triggered correctly. b) A dominant parent transition ends
593      * (P1 - X) but a local transition is still running, resulting in a different state of the
594      * element.
595      *
596      * Both issues can be addressed by interpolating P1 in the future.
597      */
syncAncestorElementStatenull598     private fun syncAncestorElementState() {
599         // every nested STL syncs only the level above it
600         layoutImpl.ancestors.lastOrNull()?.also { ancestor ->
601             val localTransition =
602                 localElementState(
603                     currentTransitionStates.last(),
604                     isInContent = { it in element.stateByContent },
605                 )
606             when (localTransition) {
607                 is TransitionState.Idle ->
608                     assignState(ancestor.inContent, localTransition.currentScene)
609                 is TransitionState.Transition.ChangeScene ->
610                     assignState(ancestor.inContent, localTransition.currentScene)
611                 is TransitionState.Transition.ReplaceOverlay ->
612                     assignState(ancestor.inContent, localTransition.effectivelyShownOverlay)
613                 is TransitionState.Transition.ShowOrHideOverlay ->
614                     if (localTransition.isEffectivelyShown) {
615                         assignState(ancestor.inContent, localTransition.overlay)
616                     } else {
617                         assignState(ancestor.inContent, localTransition.fromOrToScene)
618                     }
619                 null -> {}
620             }
621         }
622     }
623 
assignStatenull624     private fun assignState(toContent: ContentKey, fromContent: ContentKey) {
625         val fromState = element.stateByContent[fromContent]
626         if (fromState != null) {
627             element.stateByContent[toContent] = fromState
628         } else {
629             element.stateByContent.remove(toContent)
630         }
631     }
632 
633     /**
634      * Recursively clear the last placement values on this node and all descendants ElementNodes.
635      * This should be called when this node is not placed anymore, so that we correctly clear values
636      * for the descendants for which approachMeasure() won't be called.
637      */
recursivelyClearPlacementValuesnull638     private fun recursivelyClearPlacementValues() {
639         fun Element.State.clearLastPlacementValues() {
640             lastOffset = Offset.Unspecified
641             lastScale = Scale.Unspecified
642             lastAlpha = Element.AlphaUnspecified
643         }
644 
645         stateInContent.clearLastPlacementValues()
646         traverseDescendants(ElementTraverseKey) { node ->
647             if ((node as ElementNode)._element != null) {
648                 node.stateInContent.clearLastPlacementValues()
649             }
650             TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
651         }
652     }
653 
drawnull654     override fun ContentDrawScope.draw() {
655         element.wasDrawnInAnyContent = true
656 
657         val transition =
658             elementState(layoutImpl, element, currentTransitionStates)
659                 as? TransitionState.Transition
660         val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
661         if (drawScale == Scale.Default) {
662             drawContent()
663         } else {
664             scale(
665                 drawScale.scaleX,
666                 drawScale.scaleY,
667                 if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
668             ) {
669                 this@draw.drawContent()
670             }
671         }
672     }
673 
674     companion object {
675         private val ElementTraverseKey = Any()
676 
maybePruneMapsnull677         private fun maybePruneMaps(
678             layoutImpl: SceneTransitionLayoutImpl,
679             element: Element,
680             stateInContent: Element.State,
681         ) {
682             fun pruneForContent(contentKey: ContentKey) {
683                 // If element is not composed in this content anymore, remove the content values.
684                 // This works because [onAttach] is called before [onDetach], so if an element is
685                 // moved from the UI tree we will first add the new code location then remove the
686                 // old one.
687                 if (
688                     stateInContent.nodes.isEmpty() &&
689                         element.stateByContent[contentKey] == stateInContent
690                 ) {
691                     element.stateByContent.remove(contentKey)
692 
693                     // If the element is not composed in any content, remove it from the elements
694                     // map.
695                     if (
696                         element.stateByContent.isEmpty() &&
697                             layoutImpl.elements[element.key] == element
698                     ) {
699                         layoutImpl.elements.remove(element.key)
700                     }
701                 }
702             }
703 
704             stateInContent.contents.fastForEach { pruneForContent(it) }
705         }
706     }
707 }
708 
709 /** The [TransitionState] that we should consider for [element]. */
elementStatenull710 private fun elementState(
711     layoutImpl: SceneTransitionLayoutImpl,
712     element: Element,
713     transitionStates: List<List<TransitionState>>,
714 ): TransitionState? {
715     val state =
716         elementState(transitionStates, element.key, isInContent = { it in element.stateByContent })
717 
718     val transition = state as? TransitionState.Transition
719     val previousTransition = element.lastTransition
720     element.lastTransition = transition
721 
722     if (transition != previousTransition && transition != null && previousTransition != null) {
723         // The previous transition was interrupted by another transition.
724         prepareInterruption(layoutImpl, element, transition, previousTransition)
725     } else if (transition == null && previousTransition != null) {
726         // The transition was just finished.
727         element.stateByContent.values.forEach {
728             it.clearValuesBeforeInterruption()
729             it.clearInterruptionDeltas()
730         }
731     }
732 
733     return state
734 }
735 
elementStatenull736 internal inline fun elementState(
737     transitionStates: List<List<TransitionState>>,
738     elementKey: ElementKey,
739     isInContent: (ContentKey) -> Boolean,
740 ): TransitionState? {
741     // transitionStates is a list of all ancestor transition states + transitionState of the local
742     // STL. By traversing the list in normal order we by default prioritize the transitionState of
743     // the highest ancestor if it is running and has a transformation for this element.
744     transitionStates.fastForEachIndexed { index, states ->
745         if (index < transitionStates.size - 1) {
746             // Check if any ancestor runs a transition that has a transformation for the element
747             states.fastForEachReversed { state ->
748                 if (
749                     isSharedElement(state, isInContent) ||
750                         hasTransformationForElement(state, elementKey)
751                 ) {
752                     return state
753                 }
754             }
755         } else {
756             return localElementState(states, isInContent)
757         }
758     }
759     return null
760 }
761 
localElementStatenull762 private inline fun localElementState(
763     states: List<TransitionState>,
764     isInContent: (ContentKey) -> Boolean,
765 ): TransitionState? {
766     // the last state of the list is the state of the local STL
767     val lastState = states.last()
768     if (lastState is TransitionState.Idle) {
769         check(states.size == 1)
770         return lastState
771     }
772 
773     // Find the last transition with a content that contains the element.
774     states.fastForEachReversed { state ->
775         val transition = state as TransitionState.Transition
776         if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
777             return transition
778         }
779     }
780 
781     // We are running a transition where both from and to don't contain the element. The element
782     // may still be rendered as e.g. it can be part of a idle scene where two overlays are currently
783     // transitioning above it.
784     return null
785 }
786 
isSharedElementnull787 private inline fun isSharedElement(
788     state: TransitionState,
789     isInContent: (ContentKey) -> Boolean,
790 ): Boolean {
791     return state is TransitionState.Transition &&
792         isInContent(state.fromContent) &&
793         isInContent(state.toContent)
794 }
795 
hasTransformationForElementnull796 private fun hasTransformationForElement(state: TransitionState, elementKey: ElementKey): Boolean {
797     return state is TransitionState.Transition &&
798         (state.transformationSpec.hasTransformation(elementKey, state.fromContent) ||
799             state.transformationSpec.hasTransformation(elementKey, state.toContent))
800 }
801 
elementContentWhenIdlenull802 internal inline fun elementContentWhenIdle(
803     layoutImpl: SceneTransitionLayoutImpl,
804     currentState: TransitionState,
805     isInContent: (ContentKey) -> Boolean,
806 ): ContentKey {
807     val currentScene = currentState.currentScene
808     val overlays = currentState.currentOverlays
809     if (overlays.isEmpty()) {
810         return currentScene
811     }
812 
813     // Find the overlay with highest zIndex that contains the element.
814     // TODO(b/353679003): Should we cache enabledOverlays into a List<> to avoid a lot of
815     // allocations here?
816     var currentOverlay: OverlayKey? = null
817     for (overlay in overlays) {
818         if (
819             isInContent(overlay) &&
820                 (currentOverlay == null ||
821                     (layoutImpl.overlay(overlay).zIndex >
822                         layoutImpl.overlay(currentOverlay).zIndex))
823         ) {
824             currentOverlay = overlay
825         }
826     }
827 
828     return currentOverlay ?: currentScene
829 }
830 
prepareInterruptionnull831 private fun prepareInterruption(
832     layoutImpl: SceneTransitionLayoutImpl,
833     element: Element,
834     transition: TransitionState.Transition,
835     previousTransition: TransitionState.Transition,
836 ) {
837     if (transition.replacedTransition == previousTransition) {
838         return
839     }
840 
841     val stateByContent = element.stateByContent
842     fun updateStateInContent(key: ContentKey): Element.State? {
843         return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() }
844     }
845 
846     val previousFromState = updateStateInContent(previousTransition.fromContent)
847     val previousToState = updateStateInContent(previousTransition.toContent)
848     val fromState = updateStateInContent(transition.fromContent)
849     val toState = updateStateInContent(transition.toContent)
850 
851     val previousUniqueState = reconcileStates(element, previousTransition, previousState = null)
852     reconcileStates(element, transition, previousState = previousUniqueState)
853 
854     // Remove the interruption values to all contents but the content(s) where the element will be
855     // placed, to make sure that interruption deltas are computed only right after this interruption
856     // is prepared.
857     fun cleanInterruptionValues(stateInContent: Element.State) {
858         stateInContent.sizeInterruptionDelta = IntSize.Zero
859         stateInContent.offsetInterruptionDelta = Offset.Zero
860         stateInContent.alphaInterruptionDelta = 0f
861         stateInContent.scaleInterruptionDelta = Scale.Zero
862 
863         if (!shouldPlaceElement(layoutImpl, stateInContent.contents.last(), element, transition)) {
864             stateInContent.offsetBeforeInterruption = Offset.Unspecified
865             stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
866             stateInContent.scaleBeforeInterruption = Scale.Unspecified
867         }
868     }
869 
870     previousFromState?.let { cleanInterruptionValues(it) }
871     previousToState?.let { cleanInterruptionValues(it) }
872     fromState?.let { cleanInterruptionValues(it) }
873     toState?.let { cleanInterruptionValues(it) }
874 }
875 
876 /**
877  * Reconcile the state of [element] in the fromContent and toContent of [transition] so that the
878  * values before interruption have their expected values, taking shared transitions into account.
879  *
880  * @return the unique state this element had during [transition], `null` if it had multiple
881  *   different states (i.e. the shared animation was disabled).
882  */
reconcileStatesnull883 private fun reconcileStates(
884     element: Element,
885     transition: TransitionState.Transition,
886     previousState: Element.State?,
887 ): Element.State? {
888     fun reconcileWithPreviousState(state: Element.State) {
889         if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) {
890             state.updateValuesBeforeInterruption(previousState)
891         }
892     }
893 
894     val fromContentState = element.stateByContent[transition.fromContent]
895     val toContentState = element.stateByContent[transition.toContent]
896 
897     if (fromContentState == null || toContentState == null) {
898         return (fromContentState ?: toContentState)
899             ?.also { reconcileWithPreviousState(it) }
900             ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified }
901     }
902 
903     if (!isSharedElementEnabled(element.key, transition)) {
904         return null
905     }
906 
907     if (
908         fromContentState.offsetBeforeInterruption != Offset.Unspecified &&
909             toContentState.offsetBeforeInterruption == Offset.Unspecified
910     ) {
911         // Element is shared and placed in fromContent only.
912         toContentState.updateValuesBeforeInterruption(fromContentState)
913         return fromContentState
914     }
915 
916     if (
917         toContentState.offsetBeforeInterruption != Offset.Unspecified &&
918             fromContentState.offsetBeforeInterruption == Offset.Unspecified
919     ) {
920         // Element is shared and placed in toContent only.
921         fromContentState.updateValuesBeforeInterruption(toContentState)
922         return toContentState
923     }
924 
925     return null
926 }
927 
Elementnull928 private fun Element.State.selfUpdateValuesBeforeInterruption() {
929     sizeBeforeInterruption = lastSize
930 
931     if (lastAlpha > 0f) {
932         offsetBeforeInterruption = lastOffset
933         scaleBeforeInterruption = lastScale
934         alphaBeforeInterruption = lastAlpha
935     } else {
936         // Consider the element as not placed in this content if it was fully transparent.
937         // TODO(b/290930950): Look into using derived state inside place() instead to not even place
938         // the element at all when alpha == 0f.
939         offsetBeforeInterruption = Offset.Unspecified
940         scaleBeforeInterruption = Scale.Unspecified
941         alphaBeforeInterruption = Element.AlphaUnspecified
942     }
943 }
944 
updateValuesBeforeInterruptionnull945 private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) {
946     offsetBeforeInterruption = lastState.offsetBeforeInterruption
947     sizeBeforeInterruption = lastState.sizeBeforeInterruption
948     scaleBeforeInterruption = lastState.scaleBeforeInterruption
949     alphaBeforeInterruption = lastState.alphaBeforeInterruption
950 
951     clearInterruptionDeltas()
952 }
953 
Elementnull954 private fun Element.State.clearInterruptionDeltas() {
955     offsetInterruptionDelta = Offset.Zero
956     sizeInterruptionDelta = IntSize.Zero
957     scaleInterruptionDelta = Scale.Zero
958     alphaInterruptionDelta = 0f
959 }
960 
clearValuesBeforeInterruptionnull961 private fun Element.State.clearValuesBeforeInterruption() {
962     offsetBeforeInterruption = Offset.Unspecified
963     scaleBeforeInterruption = Scale.Unspecified
964     alphaBeforeInterruption = Element.AlphaUnspecified
965 }
966 
967 /**
968  * Compute what [value] should be if we take the
969  * [interruption progress][ContentState.Transition.interruptionProgress] of [transition] into
970  * account.
971  */
computeInterruptedValuenull972 private inline fun <T> computeInterruptedValue(
973     layoutImpl: SceneTransitionLayoutImpl,
974     transition: TransitionState.Transition?,
975     value: T,
976     unspecifiedValue: T,
977     zeroValue: T,
978     getValueBeforeInterruption: () -> T,
979     setValueBeforeInterruption: (T) -> Unit,
980     getInterruptionDelta: () -> T,
981     setInterruptionDelta: (T) -> Unit,
982     diff: (a: T, b: T) -> T, // a - b
983     add: (a: T, b: T, bProgress: Float) -> T, // a + (b * bProgress)
984 ): T {
985     val valueBeforeInterruption = getValueBeforeInterruption()
986 
987     // If the value before the interruption is specified, it means that this is the first time we
988     // compute [value] right after an interruption.
989     if (valueBeforeInterruption != unspecifiedValue) {
990         // Compute and store the delta between the value before the interruption and the current
991         // value.
992         setInterruptionDelta(diff(valueBeforeInterruption, value))
993 
994         // Reset the value before interruption now that we processed it.
995         setValueBeforeInterruption(unspecifiedValue)
996     }
997 
998     val delta = getInterruptionDelta()
999     return if (delta == zeroValue || transition == null) {
1000         // There was no interruption or there is no transition: just return the value.
1001         value
1002     } else {
1003         // Add `delta * interruptionProgress` to the value so that we animate to value.
1004         val interruptionProgress = transition.interruptionProgress(layoutImpl)
1005         if (interruptionProgress == 0f) {
1006             value
1007         } else {
1008             add(value, delta, interruptionProgress)
1009         }
1010     }
1011 }
1012 
1013 /**
1014  * Set the interruption delta of a *placement/drawing*-related value (offset, alpha, scale). This
1015  * ensures that the delta is also set on the other content in the transition for shared elements, so
1016  * that there is no jump cut if the content where the element is placed has changed.
1017  */
setPlacementInterruptionDeltanull1018 private inline fun <T> setPlacementInterruptionDelta(
1019     element: Element,
1020     stateInContent: Element.State,
1021     transition: TransitionState.Transition?,
1022     delta: T,
1023     setter: (Element.State, T) -> Unit,
1024 ) {
1025     // Set the interruption delta on the current content.
1026     setter(stateInContent, delta)
1027 
1028     if (transition == null) {
1029         return
1030     }
1031 
1032     // If the element is shared, also set the delta on the other content so that it is used by that
1033     // content if we start overscrolling it and change the content where the element is placed.
1034     val otherContent =
1035         if (stateInContent.contents.last() == transition.fromContent) transition.toContent
1036         else transition.fromContent
1037     val otherContentState = element.stateByContent[otherContent] ?: return
1038     if (isSharedElementEnabled(element.key, transition)) {
1039         setter(otherContentState, delta)
1040     }
1041 }
1042 
shouldPlaceElementnull1043 private fun shouldPlaceElement(
1044     layoutImpl: SceneTransitionLayoutImpl,
1045     content: ContentKey,
1046     element: Element,
1047     elementState: TransitionState,
1048 ): Boolean {
1049     if (element.key.placeAllCopies) {
1050         return true
1051     }
1052 
1053     val transition =
1054         when (elementState) {
1055             is TransitionState.Idle -> {
1056                 return content ==
1057                     elementContentWhenIdle(
1058                         layoutImpl,
1059                         elementState,
1060                         isInContent = { it in element.stateByContent },
1061                     )
1062             }
1063             is TransitionState.Transition -> elementState
1064         }
1065 
1066     // Don't place the element in this content if this content is not part of the current element
1067     // transition.
1068     val isReplacingOverlay = transition is TransitionState.Transition.ReplaceOverlay
1069     if (
1070         content != transition.fromContent &&
1071             content != transition.toContent &&
1072             (!isReplacingOverlay || content != transition.currentScene) &&
1073             transitionDoesNotInvolveAncestorContent(layoutImpl, transition)
1074     ) {
1075         return false
1076     }
1077 
1078     // Place the element if it is not shared.
1079     var copies = 0
1080     if (transition.fromContent in element.stateByContent) copies++
1081     if (transition.toContent in element.stateByContent) copies++
1082     if (isReplacingOverlay && transition.currentScene in element.stateByContent) copies++
1083     if (copies <= 1) {
1084         return true
1085     }
1086 
1087     val sharedTransformation = sharedElementTransformation(element.key, transition)
1088     if (sharedTransformation?.transformation?.enabled == false) {
1089         return true
1090     }
1091 
1092     return shouldPlaceSharedElement(layoutImpl, content, element.key, transition)
1093 }
1094 
transitionDoesNotInvolveAncestorContentnull1095 private fun transitionDoesNotInvolveAncestorContent(
1096     layoutImpl: SceneTransitionLayoutImpl,
1097     transition: TransitionState.Transition,
1098 ): Boolean {
1099     return layoutImpl.ancestors.fastAll {
1100         it.inContent != transition.fromContent && it.inContent != transition.toContent
1101     }
1102 }
1103 
1104 /**
1105  * Whether the element is opaque or not.
1106  *
1107  * Important: The logic here should closely match the logic in [elementAlpha]. Note that we don't
1108  * reuse [elementAlpha] and simply check if alpha == 1f because [isElementOpaque] is checked during
1109  * placement and we don't want to read the transition progress in that phase.
1110  */
isElementOpaquenull1111 private fun isElementOpaque(
1112     content: Content,
1113     element: Element,
1114     transition: TransitionState.Transition?,
1115 ): Boolean {
1116     if (transition == null) {
1117         return true
1118     }
1119 
1120     val fromState = element.stateByContent[transition.fromContent]
1121     val toState = element.stateByContent[transition.toContent]
1122 
1123     if (fromState == null && toState == null) {
1124         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
1125         // run anymore.
1126         return true
1127     }
1128 
1129     val isSharedElement = fromState != null && toState != null
1130     if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
1131         return true
1132     }
1133 
1134     return transition.transformationSpec.transformations(element.key, content.key)?.alpha == null
1135 }
1136 
1137 /**
1138  * Whether the element is opaque or not.
1139  *
1140  * Important: The logic here should closely match the logic in [isElementOpaque]. Note that we don't
1141  * reuse [elementAlpha] in [isElementOpaque] and simply check if alpha == 1f because
1142  * [isElementOpaque] is checked during placement and we don't want to read the transition progress
1143  * in that phase.
1144  */
elementAlphanull1145 private fun elementAlpha(
1146     layoutImpl: SceneTransitionLayoutImpl,
1147     element: Element,
1148     transition: TransitionState.Transition?,
1149     stateInContent: Element.State,
1150 ): Float {
1151     val alpha =
1152         computeValue(
1153                 layoutImpl,
1154                 stateInContent,
1155                 element,
1156                 transition,
1157                 contentValue = { 1f },
1158                 transformation = { it?.alpha },
1159                 currentValue = { 1f },
1160                 isSpecified = { true },
1161                 ::lerp,
1162             )
1163             .fastCoerceIn(0f, 1f)
1164 
1165     // If the element is fading during this transition and that it is drawn for the first time, make
1166     // sure that it doesn't instantly appear on screen.
1167     if (!element.wasDrawnInAnyContent && alpha > 0f) {
1168         element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f }
1169     }
1170 
1171     val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha)
1172     stateInContent.lastAlpha = interruptedAlpha
1173     return interruptedAlpha
1174 }
1175 
interruptedAlphanull1176 private fun interruptedAlpha(
1177     layoutImpl: SceneTransitionLayoutImpl,
1178     element: Element,
1179     transition: TransitionState.Transition?,
1180     stateInContent: Element.State,
1181     alpha: Float,
1182 ): Float {
1183     return computeInterruptedValue(
1184         layoutImpl,
1185         transition,
1186         value = alpha,
1187         unspecifiedValue = Element.AlphaUnspecified,
1188         zeroValue = 0f,
1189         getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption },
1190         setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it },
1191         getInterruptionDelta = { stateInContent.alphaInterruptionDelta },
1192         setInterruptionDelta = { delta ->
1193             setPlacementInterruptionDelta(
1194                 element = element,
1195                 stateInContent = stateInContent,
1196                 transition = transition,
1197                 delta = delta,
1198                 setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta },
1199             )
1200         },
1201         diff = { a, b -> a - b },
1202         add = { a, b, bProgress -> a + b * bProgress },
1203     )
1204 }
1205 
approachMeasurenull1206 private fun approachMeasure(
1207     layoutImpl: SceneTransitionLayoutImpl,
1208     element: Element,
1209     transition: TransitionState.Transition?,
1210     stateInContent: Element.State,
1211     measurable: Measurable,
1212     constraints: Constraints,
1213 ): Placeable {
1214     // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
1215     // case we store the resulting placeable here to make sure the element is not measured more than
1216     // once.
1217     var maybePlaceable: Placeable? = null
1218 
1219     val targetSize =
1220         computeValue(
1221             layoutImpl,
1222             stateInContent,
1223             element,
1224             transition,
1225             contentValue = { it.targetSize },
1226             transformation = { it?.size },
1227             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
1228             isSpecified = { it != Element.SizeUnspecified },
1229             ::lerp,
1230         )
1231 
1232     // The measurable was already measured, so we can't take interruptions into account here given
1233     // that we are not allowed to measure the same measurable twice.
1234     maybePlaceable?.let { placeable ->
1235         stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
1236         stateInContent.sizeInterruptionDelta = IntSize.Zero
1237         stateInContent.approachSize = Element.SizeUnspecified
1238         return placeable
1239     }
1240 
1241     val interruptedSize =
1242         computeInterruptedValue(
1243             layoutImpl,
1244             transition,
1245             value = targetSize,
1246             unspecifiedValue = Element.SizeUnspecified,
1247             zeroValue = IntSize.Zero,
1248             getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption },
1249             setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it },
1250             getInterruptionDelta = { stateInContent.sizeInterruptionDelta },
1251             setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it },
1252             diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) },
1253             add = { a, b, bProgress ->
1254                 IntSize(
1255                     (a.width + b.width * bProgress).roundToInt(),
1256                     (a.height + b.height * bProgress).roundToInt(),
1257                 )
1258             },
1259         )
1260 
1261     // Important: Set approachSize before child measurement. Could be used for their calculations.
1262     stateInContent.approachSize = interruptedSize
1263 
1264     return measurable.measure(
1265         Constraints.fixed(
1266             interruptedSize.width.coerceAtLeast(0),
1267             interruptedSize.height.coerceAtLeast(0),
1268         )
1269     )
1270 }
1271 
sizenull1272 private fun Placeable.size(): IntSize = IntSize(width, height)
1273 
1274 private fun ContentDrawScope.getDrawScale(
1275     layoutImpl: SceneTransitionLayoutImpl,
1276     element: Element,
1277     transition: TransitionState.Transition?,
1278     stateInContent: Element.State,
1279 ): Scale {
1280     val scale =
1281         computeValue(
1282             layoutImpl,
1283             stateInContent,
1284             element,
1285             transition,
1286             contentValue = { Scale.Default },
1287             transformation = { it?.drawScale },
1288             currentValue = { Scale.Default },
1289             isSpecified = { true },
1290             ::lerp,
1291         )
1292 
1293     fun Offset.specifiedOrCenter(): Offset {
1294         return this.takeIf { isSpecified } ?: center
1295     }
1296 
1297     val interruptedScale =
1298         computeInterruptedValue(
1299             layoutImpl,
1300             transition,
1301             value = scale,
1302             unspecifiedValue = Scale.Unspecified,
1303             zeroValue = Scale.Zero,
1304             getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption },
1305             setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it },
1306             getInterruptionDelta = { stateInContent.scaleInterruptionDelta },
1307             setInterruptionDelta = { delta ->
1308                 setPlacementInterruptionDelta(
1309                     element = element,
1310                     stateInContent = stateInContent,
1311                     transition = transition,
1312                     delta = delta,
1313                     setter = { stateInContent, delta ->
1314                         stateInContent.scaleInterruptionDelta = delta
1315                     },
1316                 )
1317             },
1318             diff = { a, b ->
1319                 Scale(
1320                     scaleX = a.scaleX - b.scaleX,
1321                     scaleY = a.scaleY - b.scaleY,
1322                     pivot =
1323                         if (a.pivot.isUnspecified && b.pivot.isUnspecified) {
1324                             Offset.Unspecified
1325                         } else {
1326                             a.pivot.specifiedOrCenter() - b.pivot.specifiedOrCenter()
1327                         },
1328                 )
1329             },
1330             add = { a, b, bProgress ->
1331                 Scale(
1332                     scaleX = a.scaleX + b.scaleX * bProgress,
1333                     scaleY = a.scaleY + b.scaleY * bProgress,
1334                     pivot =
1335                         if (a.pivot.isUnspecified && b.pivot.isUnspecified) {
1336                             Offset.Unspecified
1337                         } else {
1338                             a.pivot.specifiedOrCenter() + b.pivot.specifiedOrCenter() * bProgress
1339                         },
1340                 )
1341             },
1342         )
1343 
1344     stateInContent.lastScale = interruptedScale
1345     return interruptedScale
1346 }
1347 
1348 /**
1349  * Return the value that should be used depending on the current layout state and transition.
1350  *
1351  * Important: This function must remain inline because of all the lambda parameters. These lambdas
1352  * are necessary because getting some of them might require some computation, like measuring a
1353  * Measurable.
1354  *
1355  * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
1356  * @param currentContentState the content state of the content for which we are computing the value.
1357  *   Note that during interruptions, this could be the state of a content that is neither
1358  *   [transition.toContent] nor [transition.fromContent].
1359  * @param element the element being animated.
1360  * @param contentValue the value being animated.
1361  * @param transformation the transformation associated to the value being animated.
1362  * @param currentValue the value that would be used if it is not transformed. Note that this is
1363  *   different than [idleValue] even if the value is not transformed directly because it could be
1364  *   impacted by the transformations on other elements, like a parent that is being translated or
1365  *   resized.
1366  * @param lerp the linear interpolation function used to interpolate between two values of this
1367  *   value type.
1368  */
computeValuenull1369 private inline fun <T> computeValue(
1370     layoutImpl: SceneTransitionLayoutImpl,
1371     currentContentState: Element.State,
1372     element: Element,
1373     transition: TransitionState.Transition?,
1374     contentValue: (Element.State) -> T,
1375     transformation:
1376         (ElementTransformations?) -> TransformationWithRange<PropertyTransformation<T>>?,
1377     currentValue: () -> T,
1378     isSpecified: (T) -> Boolean,
1379     lerp: (T, T, Float) -> T,
1380 ): T {
1381     if (transition == null) {
1382         // There is no ongoing transition. Even if this element SceneTransitionLayout is not
1383         // animated, the layout itself might be animated (e.g. by another parent
1384         // SceneTransitionLayout), in which case this element still need to participate in the
1385         // layout phase.
1386         return currentValue()
1387     }
1388 
1389     val fromContent = transition.fromContent
1390     val toContent = transition.toContent
1391 
1392     val fromState = element.stateByContent[fromContent]
1393     val toState = element.stateByContent[toContent]
1394 
1395     if (fromState == null && toState == null) {
1396         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
1397         // run anymore.
1398         return contentValue(currentContentState)
1399     }
1400 
1401     val currentContent = currentContentState.contents.last()
1402 
1403     // The element is shared: interpolate between the value in fromContent and toContent.
1404     // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
1405     // elements follow the finger direction.
1406     val isSharedElement = fromState != null && toState != null
1407     if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
1408         return interpolateSharedElement(
1409             transition = transition,
1410             contentValue = contentValue,
1411             fromState = fromState!!,
1412             toState = toState!!,
1413             isSpecified = isSpecified,
1414             lerp = lerp,
1415         )
1416     }
1417 
1418     // If we are replacing an overlay and the element is both in a single overlay and in the current
1419     // scene, interpolate the state of the element using the current scene as the other scene.
1420     var currentSceneState: Element.State? = null
1421     if (!isSharedElement && transition is TransitionState.Transition.ReplaceOverlay) {
1422         currentSceneState = element.stateByContent[transition.currentScene]
1423         if (currentSceneState != null && isSharedElementEnabled(element.key, transition)) {
1424             return interpolateSharedElement(
1425                 transition = transition,
1426                 contentValue = contentValue,
1427                 fromState = fromState ?: currentSceneState,
1428                 toState = toState ?: currentSceneState,
1429                 isSpecified = isSpecified,
1430                 lerp = lerp,
1431             )
1432         }
1433     }
1434 
1435     // The content for which we compute the transformation. Note that this is not necessarily
1436     // [currentContent] because [currentContent] could be a different content than the transition
1437     // fromContent or toContent during interruptions or when a ancestor transition is running.
1438     val transformationContentKey: ContentKey =
1439         getTransformationContentKey(
1440             isDisabledSharedElement = isSharedElement,
1441             currentContent = currentContent,
1442             layoutImpl = layoutImpl,
1443             transition = transition,
1444             element = element,
1445             currentSceneState = currentSceneState,
1446         )
1447     // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
1448     // end (for leaving elements) of the transition.
1449     val targetState: Element.State = element.stateByContent.getValue(transformationContentKey)
1450     val idleValue = contentValue(targetState)
1451 
1452     val transformationWithRange =
1453         transformation(
1454             transition.transformationSpec.transformations(element.key, transformationContentKey)
1455         )
1456 
1457     val isElementEntering =
1458         when {
1459             transformationContentKey == toContent -> true
1460             transformationContentKey == fromContent -> false
1461             isAncestorTransition(layoutImpl, transition) ->
1462                 isEnteringAncestorTransition(layoutImpl, transition)
1463             transformationContentKey == transition.currentScene -> toState == null
1464             else -> transformationContentKey == toContent
1465         }
1466 
1467     val previewTransformation =
1468         transition.previewTransformationSpec?.let {
1469             transformation(it.transformations(element.key, transformationContentKey))
1470         }
1471 
1472     if (previewTransformation != null) {
1473         return computePreviewTransformationValue(
1474             transition,
1475             idleValue,
1476             transformationContentKey,
1477             isElementEntering,
1478             previewTransformation,
1479             element,
1480             layoutImpl,
1481             transformationWithRange,
1482             lerp,
1483         )
1484     }
1485 
1486     if (transformationWithRange == null) {
1487         // If there is no transformation explicitly associated to this element value, let's use
1488         // the value given by the system (like the current position and size given by the layout
1489         // pass).
1490         return currentValue()
1491     }
1492 
1493     val transformation = transformationWithRange.transformation
1494     when (transformation) {
1495         is CustomPropertyTransformation ->
1496             return with(transformation) {
1497                 layoutImpl.propertyTransformationScope.transform(
1498                     transformationContentKey,
1499                     element.key,
1500                     transition,
1501                     transition.coroutineScope,
1502                 )
1503             }
1504         is InterpolatedPropertyTransformation -> {
1505             /* continue */
1506         }
1507     }
1508 
1509     val targetValue =
1510         with(transformation) {
1511             layoutImpl.propertyTransformationScope.transform(
1512                 transformationContentKey,
1513                 element.key,
1514                 transition,
1515                 idleValue,
1516             )
1517         }
1518 
1519     // Make sure we don't read progress if values are the same and we don't need to interpolate, so
1520     // we don't invalidate the phase where this is read.
1521     if (targetValue == idleValue) {
1522         return targetValue
1523     }
1524 
1525     val progress = transition.progress
1526     // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
1527     val rangeProgress = transformationWithRange.range?.progress(progress) ?: progress
1528 
1529     return if (isElementEntering) {
1530         lerp(targetValue, idleValue, rangeProgress)
1531     } else {
1532         lerp(idleValue, targetValue, rangeProgress)
1533     }
1534 }
1535 
getTransformationContentKeynull1536 private fun getTransformationContentKey(
1537     isDisabledSharedElement: Boolean,
1538     currentContent: ContentKey,
1539     layoutImpl: SceneTransitionLayoutImpl,
1540     transition: TransitionState.Transition,
1541     element: Element,
1542     currentSceneState: Element.State?,
1543 ): ContentKey {
1544     return when {
1545         isDisabledSharedElement -> {
1546             currentContent
1547         }
1548         isAncestorTransition(layoutImpl, transition) -> {
1549             if (
1550                 element.stateByContent[transition.fromContent] != null &&
1551                     transition.transformationSpec.hasTransformation(
1552                         element.key,
1553                         transition.fromContent,
1554                     )
1555             ) {
1556                 transition.fromContent
1557             } else if (
1558                 element.stateByContent[transition.toContent] != null &&
1559                     transition.transformationSpec.hasTransformation(
1560                         element.key,
1561                         transition.toContent,
1562                     )
1563             ) {
1564                 transition.toContent
1565             } else {
1566                 throw IllegalStateException(
1567                     "Ancestor transition is active but no transformation " +
1568                         "spec was found. The ancestor transition should have only been selected " +
1569                         "when a transformation for that element and content was defined."
1570                 )
1571             }
1572         }
1573         currentSceneState != null && currentContent == transition.currentScene -> {
1574             currentContent
1575         }
1576         element.stateByContent[transition.fromContent] != null -> {
1577             transition.fromContent
1578         }
1579         else -> {
1580             transition.toContent
1581         }
1582     }
1583 }
1584 
computePreviewTransformationValuenull1585 private inline fun <T> computePreviewTransformationValue(
1586     transition: TransitionState.Transition,
1587     idleValue: T,
1588     transformationContentKey: ContentKey,
1589     isEntering: Boolean,
1590     previewTransformation: TransformationWithRange<PropertyTransformation<T>>,
1591     element: Element,
1592     layoutImpl: SceneTransitionLayoutImpl,
1593     transformationWithRange: TransformationWithRange<PropertyTransformation<T>>?,
1594     lerp: (T, T, Float) -> T,
1595 ): T {
1596     val isInPreviewStage = transition.isInPreviewStage
1597 
1598     val previewTargetValue =
1599         with(
1600             previewTransformation.transformation.requireInterpolatedTransformation(
1601                 element,
1602                 transition,
1603             ) {
1604                 "Custom transformations in preview specs should not be possible"
1605             }
1606         ) {
1607             layoutImpl.propertyTransformationScope.transform(
1608                 transformationContentKey,
1609                 element.key,
1610                 transition,
1611                 idleValue,
1612             )
1613         }
1614 
1615     val targetValueOrNull =
1616         transformationWithRange?.let { transformation ->
1617             with(
1618                 transformation.transformation.requireInterpolatedTransformation(
1619                     element,
1620                     transition,
1621                 ) {
1622                     "Custom transformations are not allowed for properties with a preview"
1623                 }
1624             ) {
1625                 layoutImpl.propertyTransformationScope.transform(
1626                     transformationContentKey,
1627                     element.key,
1628                     transition,
1629                     idleValue,
1630                 )
1631             }
1632         }
1633 
1634     // Make sure we don't read progress if values are the same and we don't need to interpolate,
1635     // so we don't invalidate the phase where this is read.
1636     when {
1637         isInPreviewStage && isEntering && previewTargetValue == targetValueOrNull ->
1638             return previewTargetValue
1639         isInPreviewStage && !isEntering && idleValue == previewTargetValue -> return idleValue
1640         previewTargetValue == targetValueOrNull && idleValue == previewTargetValue ->
1641             return idleValue
1642         else -> {}
1643     }
1644 
1645     val previewProgress = transition.previewProgress
1646     // progress is not needed for all cases of the below when block, therefore read it lazily
1647     // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range
1648     val previewRangeProgress =
1649         previewTransformation.range?.progress(previewProgress) ?: previewProgress
1650 
1651     if (isInPreviewStage) {
1652         // if we're in the preview stage of the transition, interpolate between start state and
1653         // preview target state:
1654         return if (isEntering) {
1655             // i.e. in the entering case between previewTargetValue and targetValue (or
1656             // idleValue if no transformation is defined in the second stage transition)...
1657             lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress)
1658         } else {
1659             // ...and in the exiting case between the idleValue and the previewTargetValue.
1660             lerp(idleValue, previewTargetValue, previewRangeProgress)
1661         }
1662     }
1663 
1664     // if we're in the second stage of the transition, interpolate between the state the
1665     // element was left at the end of the preview-phase and the target state:
1666     return if (isEntering) {
1667         // i.e. in the entering case between preview-end-state and the idleValue...
1668         lerp(
1669             lerp(previewTargetValue, targetValueOrNull ?: idleValue, previewRangeProgress),
1670             idleValue,
1671             transformationWithRange?.range?.progress(transition.progress) ?: transition.progress,
1672         )
1673     } else {
1674         if (targetValueOrNull == null) {
1675             // ... and in the exiting case, the element should remain in the preview-end-state
1676             // if no further transformation is defined in the second-stage transition...
1677             lerp(idleValue, previewTargetValue, previewRangeProgress)
1678         } else {
1679             // ...and otherwise it should be interpolated between preview-end-state and
1680             // targetValue
1681             lerp(
1682                 lerp(idleValue, previewTargetValue, previewRangeProgress),
1683                 targetValueOrNull,
1684                 transformationWithRange.range?.progress(transition.progress) ?: transition.progress,
1685             )
1686         }
1687     }
1688 }
1689 
isAncestorTransitionnull1690 private fun isAncestorTransition(
1691     layoutImpl: SceneTransitionLayoutImpl,
1692     transition: TransitionState.Transition,
1693 ): Boolean {
1694     return layoutImpl.ancestors.fastAny {
1695         it.inContent == transition.fromContent || it.inContent == transition.toContent
1696     }
1697 }
1698 
isEnteringAncestorTransitionnull1699 private fun isEnteringAncestorTransition(
1700     layoutImpl: SceneTransitionLayoutImpl,
1701     transition: TransitionState.Transition,
1702 ): Boolean {
1703     return layoutImpl.ancestors.fastAny { it.inContent == transition.toContent }
1704 }
1705 
requireInterpolatedTransformationnull1706 private inline fun <T> PropertyTransformation<T>.requireInterpolatedTransformation(
1707     element: Element,
1708     transition: TransitionState.Transition,
1709     errorMessage: () -> String,
1710 ): InterpolatedPropertyTransformation<T> {
1711     return when (this) {
1712         is InterpolatedPropertyTransformation -> this
1713         is CustomPropertyTransformation -> {
1714             val elem = element.key.debugName
1715             val fromContent = transition.fromContent
1716             val toContent = transition.toContent
1717             error("${errorMessage()} (element=$elem fromContent=$fromContent toContent=$toContent)")
1718         }
1719     }
1720 }
1721 
interpolateSharedElementnull1722 private inline fun <T> interpolateSharedElement(
1723     transition: TransitionState.Transition,
1724     contentValue: (Element.State) -> T,
1725     fromState: Element.State,
1726     toState: Element.State,
1727     isSpecified: (T) -> Boolean,
1728     lerp: (T, T, Float) -> T,
1729 ): T {
1730     val start = contentValue(fromState)
1731     val end = contentValue(toState)
1732 
1733     // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
1734     // nodes before the intermediate layout pass.
1735     if (!isSpecified(start)) return end
1736     if (!isSpecified(end)) return start
1737 
1738     // Make sure we don't read progress if values are the same and we don't need to interpolate,
1739     // so we don't invalidate the phase where this is read.
1740     return if (start == end) start else lerp(start, end, transition.progress)
1741 }
1742