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.Composable
20 import androidx.compose.runtime.DisposableEffect
21 import androidx.compose.runtime.LaunchedEffect
22 import androidx.compose.runtime.SideEffect
23 import androidx.compose.runtime.Stable
24 import androidx.compose.runtime.State
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.remember
27 import androidx.compose.runtime.snapshotFlow
28 import androidx.compose.runtime.snapshots.SnapshotStateMap
29 import androidx.compose.ui.graphics.Color
30 import androidx.compose.ui.graphics.colorspace.ColorSpaces
31 import androidx.compose.ui.unit.Dp
32 import androidx.compose.ui.unit.dp
33 import androidx.compose.ui.util.fastCoerceIn
34 import androidx.compose.ui.util.fastLastOrNull
35 import kotlin.math.roundToInt
36 
37 /**
38  * A [State] whose [value] is animated.
39  *
40  * Important: This animated value should always be ready *after* composition, e.g. during layout,
41  * drawing or inside a LaunchedEffect. If you read [value] during composition, it will probably
42  * throw an exception, for 2 important reasons:
43  * 1. You should never read animated values during composition, because this will probably lead to
44  *    bad performance.
45  * 2. Given that this value depends on the target value in different scenes, its current value
46  *    (depending on the current transition state) can only be computed once the full tree has been
47  *    composed.
48  *
49  * If you don't have the choice and *have to* get the value during composition, for instance because
50  * a Modifier or Composable reading this value does not have a lazy/lambda-based API, then you can
51  * access [unsafeCompositionState] and use a fallback value for the first frame where this animated
52  * value can not be computed yet. Note however that doing so will be bad for performance and might
53  * lead to late-by-one-frame flickers.
54  */
55 @Stable
56 interface AnimatedState<T> : State<T> {
57     /**
58      * Return a [State] that can be read during composition.
59      *
60      * Important: You should avoid using this as much as possible and instead read [value] during
61      * layout/drawing, otherwise you will probably end up with a few frames that have a value that
62      * is not correctly interpolated.
63      */
64     @Composable fun unsafeCompositionState(initialValue: T): State<T>
65 }
66 
67 /**
68  * Animate a scene Int value.
69  *
70  * @see SceneScope.animateSceneValueAsState
71  */
72 @Composable
animateSceneIntAsStatenull73 fun SceneScope.animateSceneIntAsState(
74     value: Int,
75     key: ValueKey,
76     canOverflow: Boolean = true,
77 ): AnimatedState<Int> {
78     return animateSceneValueAsState(value, key, SharedIntType, canOverflow)
79 }
80 
81 /**
82  * Animate a shared element Int value.
83  *
84  * @see ElementScope.animateElementValueAsState
85  */
86 @Composable
animateElementIntAsStatenull87 fun ElementScope<*>.animateElementIntAsState(
88     value: Int,
89     key: ValueKey,
90     canOverflow: Boolean = true,
91 ): AnimatedState<Int> {
92     return animateElementValueAsState(value, key, SharedIntType, canOverflow)
93 }
94 
95 private object SharedIntType : SharedValueType<Int, Int> {
96     override val unspecifiedValue: Int = Int.MIN_VALUE
97     override val zeroDeltaValue: Int = 0
98 
lerpnull99     override fun lerp(a: Int, b: Int, progress: Float): Int =
100         androidx.compose.ui.util.lerp(a, b, progress)
101 
102     override fun diff(a: Int, b: Int): Int = a - b
103 
104     override fun addWeighted(a: Int, b: Int, bWeight: Float): Int = (a + b * bWeight).roundToInt()
105 }
106 
107 /**
108  * Animate a scene Float value.
109  *
110  * @see SceneScope.animateSceneValueAsState
111  */
112 @Composable
113 fun SceneScope.animateSceneFloatAsState(
114     value: Float,
115     key: ValueKey,
116     canOverflow: Boolean = true,
117 ): AnimatedState<Float> {
118     return animateSceneValueAsState(value, key, SharedFloatType, canOverflow)
119 }
120 
121 /**
122  * Animate a shared element Float value.
123  *
124  * @see ElementScope.animateElementValueAsState
125  */
126 @Composable
animateElementFloatAsStatenull127 fun ElementScope<*>.animateElementFloatAsState(
128     value: Float,
129     key: ValueKey,
130     canOverflow: Boolean = true,
131 ): AnimatedState<Float> {
132     return animateElementValueAsState(value, key, SharedFloatType, canOverflow)
133 }
134 
135 private object SharedFloatType : SharedValueType<Float, Float> {
136     override val unspecifiedValue: Float = Float.MIN_VALUE
137     override val zeroDeltaValue: Float = 0f
138 
lerpnull139     override fun lerp(a: Float, b: Float, progress: Float): Float =
140         androidx.compose.ui.util.lerp(a, b, progress)
141 
142     override fun diff(a: Float, b: Float): Float = a - b
143 
144     override fun addWeighted(a: Float, b: Float, bWeight: Float): Float = a + b * bWeight
145 }
146 
147 /**
148  * Animate a scene Dp value.
149  *
150  * @see SceneScope.animateSceneValueAsState
151  */
152 @Composable
153 fun SceneScope.animateSceneDpAsState(
154     value: Dp,
155     key: ValueKey,
156     canOverflow: Boolean = true,
157 ): AnimatedState<Dp> {
158     return animateSceneValueAsState(value, key, SharedDpType, canOverflow)
159 }
160 
161 /**
162  * Animate a shared element Dp value.
163  *
164  * @see ElementScope.animateElementValueAsState
165  */
166 @Composable
animateElementDpAsStatenull167 fun ElementScope<*>.animateElementDpAsState(
168     value: Dp,
169     key: ValueKey,
170     canOverflow: Boolean = true,
171 ): AnimatedState<Dp> {
172     return animateElementValueAsState(value, key, SharedDpType, canOverflow)
173 }
174 
175 private object SharedDpType : SharedValueType<Dp, Dp> {
176     override val unspecifiedValue: Dp = Dp.Unspecified
177     override val zeroDeltaValue: Dp = 0.dp
178 
lerpnull179     override fun lerp(a: Dp, b: Dp, progress: Float): Dp {
180         return androidx.compose.ui.unit.lerp(a, b, progress)
181     }
182 
diffnull183     override fun diff(a: Dp, b: Dp): Dp = a - b
184 
185     override fun addWeighted(a: Dp, b: Dp, bWeight: Float): Dp = a + b * bWeight
186 }
187 
188 /**
189  * Animate a scene Color value.
190  *
191  * @see SceneScope.animateSceneValueAsState
192  */
193 @Composable
194 fun SceneScope.animateSceneColorAsState(
195     value: Color,
196     key: ValueKey,
197 ): AnimatedState<Color> {
198     return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false)
199 }
200 
201 /**
202  * Animate a shared element Color value.
203  *
204  * @see ElementScope.animateElementValueAsState
205  */
206 @Composable
animateElementColorAsStatenull207 fun ElementScope<*>.animateElementColorAsState(
208     value: Color,
209     key: ValueKey,
210 ): AnimatedState<Color> {
211     return animateElementValueAsState(value, key, SharedColorType, canOverflow = false)
212 }
213 
214 private object SharedColorType : SharedValueType<Color, ColorDelta> {
215     override val unspecifiedValue: Color = Color.Unspecified
216     override val zeroDeltaValue: ColorDelta = ColorDelta(0f, 0f, 0f, 0f)
217 
lerpnull218     override fun lerp(a: Color, b: Color, progress: Float): Color {
219         return androidx.compose.ui.graphics.lerp(a, b, progress)
220     }
221 
diffnull222     override fun diff(a: Color, b: Color): ColorDelta {
223         // Similar to lerp, we convert colors to the Oklab color space to perform operations on
224         // colors.
225         val aOklab = a.convert(ColorSpaces.Oklab)
226         val bOklab = b.convert(ColorSpaces.Oklab)
227         return ColorDelta(
228             red = aOklab.red - bOklab.red,
229             green = aOklab.green - bOklab.green,
230             blue = aOklab.blue - bOklab.blue,
231             alpha = aOklab.alpha - bOklab.alpha,
232         )
233     }
234 
addWeightednull235     override fun addWeighted(a: Color, b: ColorDelta, bWeight: Float): Color {
236         val aOklab = a.convert(ColorSpaces.Oklab)
237         return Color(
238                 red = aOklab.red + b.red * bWeight,
239                 green = aOklab.green + b.green * bWeight,
240                 blue = aOklab.blue + b.blue * bWeight,
241                 alpha = aOklab.alpha + b.alpha * bWeight,
242                 colorSpace = ColorSpaces.Oklab,
243             )
244             .convert(aOklab.colorSpace)
245     }
246 }
247 
248 /**
249  * Represents the diff between two colors in the same color space.
250  *
251  * Note: This class is necessary because Color() checks the bounds of its values and UncheckedColor
252  * is internal.
253  */
254 private class ColorDelta(
255     val red: Float,
256     val green: Float,
257     val blue: Float,
258     val alpha: Float,
259 )
260 
261 @Composable
animateSharedValueAsStatenull262 internal fun <T> animateSharedValueAsState(
263     layoutImpl: SceneTransitionLayoutImpl,
264     scene: SceneKey,
265     element: ElementKey?,
266     key: ValueKey,
267     value: T,
268     type: SharedValueType<T, *>,
269     canOverflow: Boolean,
270 ): AnimatedState<T> {
271     DisposableEffect(layoutImpl, scene, element, key) {
272         // Create the associated maps that hold the current value for each (element, scene) pair.
273         val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
274         val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
275         val targetValues = sharedValue.targetValues
276         targetValues[scene] = value
277 
278         onDispose {
279             // Remove the value associated to the current scene, and eventually remove the maps if
280             // they are empty.
281             targetValues.remove(scene)
282 
283             if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
284                 valueMap.remove(element)
285 
286                 if (valueMap.isEmpty() && layoutImpl.sharedValues[key] === valueMap) {
287                     layoutImpl.sharedValues.remove(key)
288                 }
289             }
290         }
291     }
292 
293     // Update the current value. Note that side effects run after disposable effects, so we know
294     // that the associated maps were created at this point.
295     SideEffect {
296         if (value == type.unspecifiedValue) {
297             error("value is equal to $value, which is the undefined value for this type.")
298         }
299 
300         sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value
301     }
302 
303     return remember(layoutImpl, scene, element, canOverflow) {
304         AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow)
305     }
306 }
307 
sharedValuenull308 private fun <T, Delta> sharedValue(
309     layoutImpl: SceneTransitionLayoutImpl,
310     key: ValueKey,
311     element: ElementKey?
312 ): SharedValue<T, Delta> {
313     return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> }
314         ?: error(valueReadTooEarlyMessage(key))
315 }
316 
valueReadTooEarlyMessagenull317 private fun valueReadTooEarlyMessage(key: ValueKey) =
318     "Animated value $key was read before its target values were set. This probably " +
319         "means that you are reading it during composition, which you should not do. See the " +
320         "documentation of AnimatedState for more information."
321 
322 internal class SharedValue<T, Delta>(
323     val type: SharedValueType<T, Delta>,
324 ) {
325     /** The target value of this shared value for each scene. */
326     val targetValues = SnapshotStateMap<SceneKey, T>()
327 
328     /** The last value of this shared value. */
329     var lastValue: T = type.unspecifiedValue
330 
331     /** The value of this shared value before the last interruption (if any). */
332     var valueBeforeInterruption: T = type.unspecifiedValue
333 
334     /** The delta value to add to this shared value to have smoother interruptions. */
335     var valueInterruptionDelta = type.zeroDeltaValue
336 
337     /** The last transition that was used when the value of this shared state. */
338     var lastTransition: TransitionState.Transition? = null
339 }
340 
341 private class AnimatedStateImpl<T, Delta>(
342     private val layoutImpl: SceneTransitionLayoutImpl,
343     private val scene: SceneKey,
344     private val element: ElementKey?,
345     private val key: ValueKey,
346     private val canOverflow: Boolean,
347 ) : AnimatedState<T> {
348     override val value: T
349         get() = value()
350 
valuenull351     private fun value(): T {
352         val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element)
353         val transition = transition(sharedValue)
354         val value: T =
355             valueOrNull(sharedValue, transition)
356                 // TODO(b/311600838): Remove this. We should not have to fallback to the current
357                 // scene value, but we have to because code of removed nodes can still run if they
358                 // are placed with a graphics layer.
359                 ?: sharedValue[scene]
360                 ?: error(valueReadTooEarlyMessage(key))
361         val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
362         sharedValue.lastValue = interruptedValue
363         return interruptedValue
364     }
365 
getnull366     private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene]
367 
368     private fun valueOrNull(
369         sharedValue: SharedValue<T, *>,
370         transition: TransitionState.Transition?,
371     ): T? {
372         if (transition == null) {
373             return sharedValue[layoutImpl.state.transitionState.currentScene]
374         }
375 
376         val fromValue = sharedValue[transition.fromScene]
377         val toValue = sharedValue[transition.toScene]
378         return if (fromValue != null && toValue != null) {
379             if (fromValue == toValue) {
380                 // Optimization: avoid reading progress if the values are the same, so we don't
381                 // relayout/redraw for nothing.
382                 fromValue
383             } else {
384                 val overscrollSpec = transition.currentOverscrollSpec
385                 val progress =
386                     when {
387                         overscrollSpec == null -> {
388                             if (canOverflow) transition.progress
389                             else transition.progress.fastCoerceIn(0f, 1f)
390                         }
391                         overscrollSpec.scene == transition.toScene -> 1f
392                         else -> 0f
393                     }
394 
395                 sharedValue.type.lerp(fromValue, toValue, progress)
396             }
397         } else fromValue ?: toValue
398     }
399 
transitionnull400     private fun transition(sharedValue: SharedValue<T, Delta>): TransitionState.Transition? {
401         val targetValues = sharedValue.targetValues
402         val transition =
403             if (element != null) {
404                 layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
405                     layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
406                         transition.fromScene in sceneStates || transition.toScene in sceneStates
407                     }
408                 }
409             } else {
410                 layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
411                     transition.fromScene in targetValues || transition.toScene in targetValues
412                 }
413             }
414 
415         val previousTransition = sharedValue.lastTransition
416         sharedValue.lastTransition = transition
417 
418         if (transition != previousTransition && transition != null && previousTransition != null) {
419             // The previous transition was interrupted by another transition.
420             sharedValue.valueBeforeInterruption = sharedValue.lastValue
421             sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
422         } else if (transition == null && previousTransition != null) {
423             // The transition was just finished.
424             sharedValue.valueBeforeInterruption = sharedValue.type.unspecifiedValue
425             sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
426         }
427 
428         return transition
429     }
430 
431     /**
432      * Compute what [value] should be if we take the
433      * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into
434      * account.
435      */
computeInterruptedValuenull436     private fun computeInterruptedValue(
437         sharedValue: SharedValue<T, Delta>,
438         transition: TransitionState.Transition?,
439         value: T,
440     ): T {
441         val type = sharedValue.type
442         if (sharedValue.valueBeforeInterruption != type.unspecifiedValue) {
443             sharedValue.valueInterruptionDelta =
444                 type.diff(sharedValue.valueBeforeInterruption, value)
445             sharedValue.valueBeforeInterruption = type.unspecifiedValue
446         }
447 
448         val delta = sharedValue.valueInterruptionDelta
449         return if (delta == type.zeroDeltaValue || transition == null) {
450             value
451         } else {
452             val interruptionProgress = transition.interruptionProgress(layoutImpl)
453             if (interruptionProgress == 0f) {
454                 value
455             } else {
456                 type.addWeighted(value, delta, interruptionProgress)
457             }
458         }
459     }
460 
461     @Composable
unsafeCompositionStatenull462     override fun unsafeCompositionState(initialValue: T): State<T> {
463         val state = remember { mutableStateOf(initialValue) }
464 
465         val animatedState = this
466         LaunchedEffect(animatedState) {
467             snapshotFlow { animatedState.value }.collect { state.value = it }
468         }
469 
470         return state
471     }
472 }
473