xref: /aosp_15_r20/frameworks/base/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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