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