1 /*
2  * 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.Composable
20 import androidx.compose.runtime.DisposableEffect
21 import androidx.compose.runtime.SideEffect
22 import androidx.compose.runtime.derivedStateOf
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableStateOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.runtime.setValue
27 import androidx.compose.runtime.snapshots.Snapshot
28 import androidx.compose.runtime.snapshots.SnapshotStateMap
29 import androidx.compose.ui.ExperimentalComposeUiApi
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.draw.drawWithContent
32 import androidx.compose.ui.geometry.Offset
33 import androidx.compose.ui.geometry.isSpecified
34 import androidx.compose.ui.geometry.lerp
35 import androidx.compose.ui.graphics.graphicsLayer
36 import androidx.compose.ui.layout.IntermediateMeasureScope
37 import androidx.compose.ui.layout.Measurable
38 import androidx.compose.ui.layout.Placeable
39 import androidx.compose.ui.layout.intermediateLayout
40 import androidx.compose.ui.platform.testTag
41 import androidx.compose.ui.unit.Constraints
42 import androidx.compose.ui.unit.IntSize
43 import androidx.compose.ui.unit.round
44 import com.android.compose.animation.scene.transformation.PropertyTransformation
45 import com.android.compose.modifiers.thenIf
46 import com.android.compose.ui.util.lerp
47 
48 /** An element on screen, that can be composed in one or more scenes. */
49 internal class Element(val key: ElementKey) {
50     /**
51      * The last offset assigned to this element, relative to the SceneTransitionLayout containing
52      * it.
53      */
54     var lastOffset = Offset.Unspecified
55 
56     /** The last size assigned to this element. */
57     var lastSize = SizeUnspecified
58 
59     /** The last alpha assigned to this element. */
60     var lastAlpha = 1f
61 
62     /** The mapping between a scene and the values/state this element has in that scene, if any. */
63     val sceneValues = SnapshotStateMap<SceneKey, SceneValues>()
64 
65     override fun toString(): String {
66         return "Element(key=$key)"
67     }
68 
69     /** The target values of this element in a given scene. */
70     class SceneValues {
71         var size by mutableStateOf(SizeUnspecified)
72         var offset by mutableStateOf(Offset.Unspecified)
73         val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
74     }
75 
76     /** A shared value of this element. */
77     class SharedValue<T>(val key: ValueKey, initialValue: T) {
78         var value by mutableStateOf(initialValue)
79     }
80 
81     companion object {
82         val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
83     }
84 }
85 
86 /** The implementation of [SceneScope.element]. */
87 @Composable
88 @OptIn(ExperimentalComposeUiApi::class)
89 internal fun Modifier.element(
90     layoutImpl: SceneTransitionLayoutImpl,
91     scene: Scene,
92     key: ElementKey,
93 ): Modifier {
94     val sceneValues = remember(scene, key) { Element.SceneValues() }
95     val element =
96         // Get the element associated to [key] if it was already composed in another scene,
97         // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
98         // withoutReadObservation() because there is no need to recompose when that map is mutated.
99         Snapshot.withoutReadObservation {
100             val element =
101                 layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
102             val previousValues = element.sceneValues[scene.key]
103             if (previousValues == null) {
104                 element.sceneValues[scene.key] = sceneValues
105             } else if (previousValues != sceneValues) {
106                 error("$key was composed multiple times in $scene")
107             }
108 
109             element
110         }
111 
112     DisposableEffect(scene, sceneValues, element) {
113         onDispose {
114             element.sceneValues.remove(scene.key)
115 
116             // This was the last scene this element was in, so remove it from the map.
117             if (element.sceneValues.isEmpty()) {
118                 layoutImpl.elements.remove(element.key)
119             }
120         }
121     }
122 
123     val alpha =
124         remember(layoutImpl, element, scene) {
125             derivedStateOf { elementAlpha(layoutImpl, element, scene) }
126         }
127     val isOpaque by remember(alpha) { derivedStateOf { alpha.value == 1f } }
128     SideEffect {
129         if (isOpaque && element.lastAlpha != 1f) {
130             element.lastAlpha = 1f
131         }
132     }
133 
134     return drawWithContent {
135             if (shouldDrawElement(layoutImpl, scene, element)) {
136                 drawContent()
137             }
138         }
139         .modifierTransformations(layoutImpl, scene, element, sceneValues)
140         .intermediateLayout { measurable, constraints ->
141             val placeable =
142                 measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
143             layout(placeable.width, placeable.height) {
144                 place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
145             }
146         }
147         .thenIf(!isOpaque) {
148             Modifier.graphicsLayer {
149                 val alpha = alpha.value
150                 this.alpha = alpha
151                 element.lastAlpha = alpha
152             }
153         }
154         .testTag(key.name)
155 }
156 
157 private fun shouldDrawElement(
158     layoutImpl: SceneTransitionLayoutImpl,
159     scene: Scene,
160     element: Element,
161 ): Boolean {
162     val state = layoutImpl.state.transitionState
163 
164     // Always draw the element if there is no ongoing transition or if the element is not shared.
165     if (
166         state !is TransitionState.Transition ||
167             state.fromScene == state.toScene ||
168             !layoutImpl.isTransitionReady(state) ||
169             state.fromScene !in element.sceneValues ||
170             state.toScene !in element.sceneValues
171     ) {
172         return true
173     }
174 
175     val otherScene =
176         layoutImpl.scenes.getValue(
177             if (scene.key == state.fromScene) {
178                 state.toScene
179             } else {
180                 state.fromScene
181             }
182         )
183 
184     // When the element is shared, draw the one in the highest scene unless it is a background, i.e.
185     // it is usually drawn below everything else.
186     val isHighestScene = scene.zIndex > otherScene.zIndex
187     return if (element.key.isBackground) {
188         !isHighestScene
189     } else {
190         isHighestScene
191     }
192 }
193 
194 /**
195  * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
196  * throughout the current transition, if any.
197  */
198 private fun Modifier.modifierTransformations(
199     layoutImpl: SceneTransitionLayoutImpl,
200     scene: Scene,
201     element: Element,
202     sceneValues: Element.SceneValues,
203 ): Modifier {
204     when (val state = layoutImpl.state.transitionState) {
205         is TransitionState.Idle -> return this
206         is TransitionState.Transition -> {
207             val fromScene = state.fromScene
208             val toScene = state.toScene
209             if (fromScene == toScene) {
210                 // Same as idle.
211                 return this
212             }
213 
214             return layoutImpl.transitions
215                 .transitionSpec(fromScene, state.toScene)
216                 .transformations(element.key)
217                 .modifier
218                 .fold(this) { modifier, transformation ->
219                     with(transformation) {
220                         modifier.transform(layoutImpl, scene, element, sceneValues)
221                     }
222                 }
223         }
224     }
225 }
226 
227 private fun elementAlpha(
228     layoutImpl: SceneTransitionLayoutImpl,
229     element: Element,
230     scene: Scene
231 ): Float {
232     return computeValue(
233             layoutImpl,
234             scene,
235             element,
236             sceneValue = { 1f },
237             transformation = { it.alpha },
238             idleValue = 1f,
239             currentValue = { 1f },
240             lastValue = { element.lastAlpha },
241             ::lerp,
242         )
243         .coerceIn(0f, 1f)
244 }
245 
246 @OptIn(ExperimentalComposeUiApi::class)
247 private fun IntermediateMeasureScope.measure(
248     layoutImpl: SceneTransitionLayoutImpl,
249     scene: Scene,
250     element: Element,
251     sceneValues: Element.SceneValues,
252     measurable: Measurable,
253     constraints: Constraints,
254 ): Placeable {
255     // Update the size this element has in this scene when idle.
256     val targetSizeInScene = lookaheadSize
257     if (targetSizeInScene != sceneValues.size) {
258         // TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
259         sceneValues.size = targetSizeInScene
260     }
261 
262     // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
263     // case we store the resulting placeable here to make sure the element is not measured more than
264     // once.
265     var maybePlaceable: Placeable? = null
266 
267     fun Placeable.size() = IntSize(width, height)
268 
269     val targetSize =
270         computeValue(
271             layoutImpl,
272             scene,
273             element,
274             sceneValue = { it.size },
275             transformation = { it.size },
276             idleValue = lookaheadSize,
277             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
278             lastValue = {
279                 val lastSize = element.lastSize
280                 if (lastSize != Element.SizeUnspecified) {
281                     lastSize
282                 } else {
283                     measurable.measure(constraints).also { maybePlaceable = it }.size()
284                 }
285             },
286             ::lerp,
287         )
288 
289     val placeable =
290         maybePlaceable
291             ?: measurable.measure(
292                 Constraints.fixed(
293                     targetSize.width.coerceAtLeast(0),
294                     targetSize.height.coerceAtLeast(0),
295                 )
296             )
297 
298     element.lastSize = placeable.size()
299     return placeable
300 }
301 
302 @OptIn(ExperimentalComposeUiApi::class)
303 private fun IntermediateMeasureScope.place(
304     layoutImpl: SceneTransitionLayoutImpl,
305     scene: Scene,
306     element: Element,
307     sceneValues: Element.SceneValues,
308     placeable: Placeable,
309     placementScope: Placeable.PlacementScope,
310 ) {
311     with(placementScope) {
312         // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
313         // when idle.
314         val coords = coordinates!!
315         val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
316         if (targetOffsetInScene != sceneValues.offset) {
317             // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
318             sceneValues.offset = targetOffsetInScene
319         }
320 
321         val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
322         val targetOffset =
323             computeValue(
324                 layoutImpl,
325                 scene,
326                 element,
327                 sceneValue = { it.offset },
328                 transformation = { it.offset },
329                 idleValue = targetOffsetInScene,
330                 currentValue = { currentOffset },
331                 lastValue = {
332                     val lastValue = element.lastOffset
333                     if (lastValue.isSpecified) {
334                         lastValue
335                     } else {
336                         currentOffset
337                     }
338                 },
339                 ::lerp,
340             )
341 
342         element.lastOffset = targetOffset
343         placeable.place((targetOffset - currentOffset).round())
344     }
345 }
346 
347 /**
348  * Return the value that should be used depending on the current layout state and transition.
349  *
350  * Important: This function must remain inline because of all the lambda parameters. These lambdas
351  * are necessary because getting some of them might require some computation, like measuring a
352  * Measurable.
353  *
354  * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
355  * @param scene the scene containing [element].
356  * @param element the element being animated.
357  * @param sceneValue the value being animated.
358  * @param transformation the transformation associated to the value being animated.
359  * @param idleValue the value when idle, i.e. when there is no transition happening.
360  * @param currentValue the value that would be used if it is not transformed. Note that this is
361  *   different than [idleValue] even if the value is not transformed directly because it could be
362  *   impacted by the transformations on other elements, like a parent that is being translated or
363  *   resized.
364  * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
365  *   the first time the value is set.
366  * @param lerp the linear interpolation function used to interpolate between two values of this
367  *   value type.
368  */
369 private inline fun <T> computeValue(
370     layoutImpl: SceneTransitionLayoutImpl,
371     scene: Scene,
372     element: Element,
373     sceneValue: (Element.SceneValues) -> T,
374     transformation: (ElementTransformations) -> PropertyTransformation<T>?,
375     idleValue: T,
376     currentValue: () -> T,
377     lastValue: () -> T,
378     lerp: (T, T, Float) -> T,
379 ): T {
380     val state = layoutImpl.state.transitionState
381 
382     // There is no ongoing transition.
383     if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
384         return idleValue
385     }
386 
387     // A transition was started but it's not ready yet (not all elements have been composed/laid
388     // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
389     if (!layoutImpl.isTransitionReady(state)) {
390         return lastValue()
391     }
392 
393     val fromScene = state.fromScene
394     val toScene = state.toScene
395     val fromValues = element.sceneValues[fromScene]
396     val toValues = element.sceneValues[toScene]
397 
398     if (fromValues == null && toValues == null) {
399         error("This should not happen, element $element is neither in $fromScene or $toScene")
400     }
401 
402     // TODO(b/291053278): Handle overscroll correctly. We should probably coerce between [0f, 1f]
403     // here and consume overflows at drawing time, somehow reusing Compose OverflowEffect or some
404     // similar mechanism.
405     val transitionProgress = state.progress
406 
407     // The element is shared: interpolate between the value in fromScene and the value in toScene.
408     // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
409     // elements follow the finger direction.
410     if (fromValues != null && toValues != null) {
411         return lerp(
412             sceneValue(fromValues),
413             sceneValue(toValues),
414             transitionProgress,
415         )
416     }
417 
418     val transformation =
419         transformation(
420             layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
421         )
422         // If there is no transformation explicitly associated to this element value, let's use
423         // the value given by the system (like the current position and size given by the layout
424         // pass).
425         ?: return currentValue()
426 
427     // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
428     // end (for leaving elements) of the transition.
429     val targetValue =
430         transformation.transform(
431             layoutImpl,
432             scene,
433             element,
434             fromValues ?: toValues!!,
435             state,
436             idleValue,
437         )
438 
439     // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
440     val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress
441 
442     // Interpolate between the value at rest and the value before entering/after leaving.
443     val isEntering = fromValues == null
444     return if (isEntering) {
445         lerp(targetValue, idleValue, rangeProgress)
446     } else {
447         lerp(idleValue, targetValue, rangeProgress)
448     }
449 }
450