• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compose.animation.scene
18 
19 import androidx.compose.runtime.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 com.android.compose.animation.scene.content.state.TransitionState
36 import kotlin.math.roundToInt
37 
38 /**
39  * A [State] whose [value] is animated.
40  *
41  * Important: This animated value should always be ready *after* composition, e.g. during layout,
42  * drawing or inside a LaunchedEffect. If you read [value] during composition, it will probably
43  * throw an exception, for 2 important reasons:
44  * 1. You should never read animated values during composition, because this will probably lead to
45  *    bad performance.
46  * 2. Given that this value depends on the target value in different scenes, its current value
47  *    (depending on the current transition state) can only be computed once the full tree has been
48  *    composed.
49  *
50  * If you don't have the choice and *have to* get the value during composition, for instance because
51  * a Modifier or Composable reading this value does not have a lazy/lambda-based API, then you can
52  * access [unsafeCompositionState] and use a fallback value for the first frame where this animated
53  * value can not be computed yet. Note however that doing so will be bad for performance and might
54  * lead to late-by-one-frame flickers.
55  */
56 @Stable
57 interface AnimatedState<T> : State<T> {
58     /**
59      * Return a [State] that can be read during composition.
60      *
61      * Important: You should avoid using this as much as possible and instead read [value] during
62      * layout/drawing, otherwise you will probably end up with a few frames that have a value that
63      * is not correctly interpolated.
64      */
65     @Composable fun unsafeCompositionState(initialValue: T): State<T>
66 }
67 
68 /**
69  * Animate a scene Int value.
70  *
71  * @see ContentScope.animateContentValueAsState
72  */
73 @Composable
ContentScopenull74 fun ContentScope.animateContentIntAsState(
75     value: Int,
76     key: ValueKey,
77     canOverflow: Boolean = true,
78 ): AnimatedState<Int> {
79     return animateContentValueAsState(value, key, SharedIntType, canOverflow)
80 }
81 
82 /**
83  * Animate a shared element Int value.
84  *
85  * @see ElementScope.animateElementValueAsState
86  */
87 @Composable
animateElementIntAsStatenull88 fun ElementScope<*>.animateElementIntAsState(
89     value: Int,
90     key: ValueKey,
91     canOverflow: Boolean = true,
92 ): AnimatedState<Int> {
93     return animateElementValueAsState(value, key, SharedIntType, canOverflow)
94 }
95 
96 private object SharedIntType : SharedValueType<Int, Int> {
97     override val unspecifiedValue: Int = Int.MIN_VALUE
98     override val zeroDeltaValue: Int = 0
99 
lerpnull100     override fun lerp(a: Int, b: Int, progress: Float): Int =
101         androidx.compose.ui.util.lerp(a, b, progress)
102 
103     override fun diff(a: Int, b: Int): Int = a - b
104 
105     override fun addWeighted(a: Int, b: Int, bWeight: Float): Int = (a + b * bWeight).roundToInt()
106 }
107 
108 /**
109  * Animate a scene Float value.
110  *
111  * @see ContentScope.animateContentValueAsState
112  */
113 @Composable
114 fun ContentScope.animateContentFloatAsState(
115     value: Float,
116     key: ValueKey,
117     canOverflow: Boolean = true,
118 ): AnimatedState<Float> {
119     return animateContentValueAsState(value, key, SharedFloatType, canOverflow)
120 }
121 
122 @Deprecated(
123     "Use animateContentFloatAsState() instead",
124     replaceWith = ReplaceWith("animateContentFloatAsState(value, key, canOverflow)"),
125 )
126 @Composable
ContentScopenull127 fun ContentScope.animateSceneFloatAsState(
128     value: Float,
129     key: ValueKey,
130     canOverflow: Boolean = true,
131 ) = animateContentFloatAsState(value, key, canOverflow)
132 
133 /**
134  * Animate a shared element Float value.
135  *
136  * @see ElementScope.animateElementValueAsState
137  */
138 @Composable
139 fun ElementScope<*>.animateElementFloatAsState(
140     value: Float,
141     key: ValueKey,
142     canOverflow: Boolean = true,
143 ): AnimatedState<Float> {
144     return animateElementValueAsState(value, key, SharedFloatType, canOverflow)
145 }
146 
147 private object SharedFloatType : SharedValueType<Float, Float> {
148     override val unspecifiedValue: Float = Float.MIN_VALUE
149     override val zeroDeltaValue: Float = 0f
150 
lerpnull151     override fun lerp(a: Float, b: Float, progress: Float): Float =
152         androidx.compose.ui.util.lerp(a, b, progress)
153 
154     override fun diff(a: Float, b: Float): Float = a - b
155 
156     override fun addWeighted(a: Float, b: Float, bWeight: Float): Float = a + b * bWeight
157 }
158 
159 /**
160  * Animate a scene Dp value.
161  *
162  * @see ContentScope.animateContentValueAsState
163  */
164 @Composable
165 fun ContentScope.animateContentDpAsState(
166     value: Dp,
167     key: ValueKey,
168     canOverflow: Boolean = true,
169 ): AnimatedState<Dp> {
170     return animateContentValueAsState(value, key, SharedDpType, canOverflow)
171 }
172 
173 @Deprecated(
174     "Use animateContentDpAsState() instead",
175     replaceWith = ReplaceWith("animateContentDpAsState(value, key, canOverflow)"),
176 )
177 @Composable
animateSceneDpAsStatenull178 fun ContentScope.animateSceneDpAsState(value: Dp, key: ValueKey, canOverflow: Boolean = true) =
179     animateContentDpAsState(value, key, canOverflow)
180 
181 /**
182  * Animate a shared element Dp value.
183  *
184  * @see ElementScope.animateElementValueAsState
185  */
186 @Composable
187 fun ElementScope<*>.animateElementDpAsState(
188     value: Dp,
189     key: ValueKey,
190     canOverflow: Boolean = true,
191 ): AnimatedState<Dp> {
192     return animateElementValueAsState(value, key, SharedDpType, canOverflow)
193 }
194 
195 private object SharedDpType : SharedValueType<Dp, Dp> {
196     override val unspecifiedValue: Dp = Dp.Unspecified
197     override val zeroDeltaValue: Dp = 0.dp
198 
lerpnull199     override fun lerp(a: Dp, b: Dp, progress: Float): Dp {
200         return androidx.compose.ui.unit.lerp(a, b, progress)
201     }
202 
diffnull203     override fun diff(a: Dp, b: Dp): Dp = a - b
204 
205     override fun addWeighted(a: Dp, b: Dp, bWeight: Float): Dp = a + b * bWeight
206 }
207 
208 /**
209  * Animate a scene Color value.
210  *
211  * @see ContentScope.animateContentValueAsState
212  */
213 @Composable
214 fun ContentScope.animateContentColorAsState(value: Color, key: ValueKey): AnimatedState<Color> {
215     return animateContentValueAsState(value, key, SharedColorType, canOverflow = false)
216 }
217 
218 /**
219  * Animate a shared element Color value.
220  *
221  * @see ElementScope.animateElementValueAsState
222  */
223 @Composable
animateElementColorAsStatenull224 fun ElementScope<*>.animateElementColorAsState(value: Color, key: ValueKey): AnimatedState<Color> {
225     return animateElementValueAsState(value, key, SharedColorType, canOverflow = false)
226 }
227 
228 internal object SharedColorType : SharedValueType<Color, ColorDelta> {
229     override val unspecifiedValue: Color = Color.Unspecified
230     override val zeroDeltaValue: ColorDelta = ColorDelta(0f, 0f, 0f, 0f)
231 
lerpnull232     override fun lerp(a: Color, b: Color, progress: Float): Color {
233         return androidx.compose.ui.graphics.lerp(a, b, progress)
234     }
235 
diffnull236     override fun diff(a: Color, b: Color): ColorDelta {
237         // Similar to lerp, we convert colors to the Oklab color space to perform operations on
238         // colors.
239         val aOklab = a.convert(ColorSpaces.Oklab)
240         val bOklab = b.convert(ColorSpaces.Oklab)
241         return ColorDelta(
242             red = aOklab.red - bOklab.red,
243             green = aOklab.green - bOklab.green,
244             blue = aOklab.blue - bOklab.blue,
245             alpha = aOklab.alpha - bOklab.alpha,
246         )
247     }
248 
addWeightednull249     override fun addWeighted(a: Color, b: ColorDelta, bWeight: Float): Color {
250         val aOklab = a.convert(ColorSpaces.Oklab)
251         return Color(
252                 red = aOklab.red + b.red * bWeight,
253                 green = aOklab.green + b.green * bWeight,
254                 blue = aOklab.blue + b.blue * bWeight,
255                 alpha = aOklab.alpha + b.alpha * bWeight,
256                 colorSpace = ColorSpaces.Oklab,
257             )
258             .convert(a.colorSpace)
259     }
260 }
261 
262 /**
263  * Represents the diff between two colors in the Oklab color space.
264  *
265  * Note: This class is necessary because Color() checks the bounds of its values and UncheckedColor
266  * is internal.
267  */
268 internal class ColorDelta(val red: Float, val green: Float, val blue: Float, val alpha: Float)
269 
270 @Composable
animateSharedValueAsStatenull271 internal fun <T> animateSharedValueAsState(
272     layoutImpl: SceneTransitionLayoutImpl,
273     content: ContentKey,
274     element: ElementKey?,
275     key: ValueKey,
276     value: T,
277     type: SharedValueType<T, *>,
278     canOverflow: Boolean,
279 ): AnimatedState<T> {
280     DisposableEffect(layoutImpl, content, element, key) {
281         // Create the associated maps that hold the current value for each (element, content) pair.
282         val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
283         val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
284         val targetValues = sharedValue.targetValues
285         targetValues[content] = value
286 
287         onDispose {
288             // Remove the value associated to the current scene, and eventually remove the maps if
289             // they are empty.
290             targetValues.remove(content)
291 
292             if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
293                 valueMap.remove(element)
294 
295                 if (valueMap.isEmpty() && layoutImpl.sharedValues[key] === valueMap) {
296                     layoutImpl.sharedValues.remove(key)
297                 }
298             }
299         }
300     }
301 
302     // Update the current value. Note that side effects run after disposable effects, so we know
303     // that the associated maps were created at this point.
304     SideEffect {
305         if (value == type.unspecifiedValue) {
306             error("value is equal to $value, which is the undefined value for this type.")
307         }
308 
309         sharedValue<T, Any>(layoutImpl, key, element).targetValues[content] = value
310     }
311 
312     return remember(layoutImpl, content, element, canOverflow) {
313         AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow)
314     }
315 }
316 
sharedValuenull317 private fun <T, Delta> sharedValue(
318     layoutImpl: SceneTransitionLayoutImpl,
319     key: ValueKey,
320     element: ElementKey?,
321 ): SharedValue<T, Delta> {
322     return layoutImpl.sharedValues[key]?.get(element)?.let { it as SharedValue<T, Delta> }
323         ?: error(valueReadTooEarlyMessage(key))
324 }
325 
valueReadTooEarlyMessagenull326 private fun valueReadTooEarlyMessage(key: ValueKey) =
327     "Animated value $key was read before its target values were set. This probably " +
328         "means that you are reading it during composition, which you should not do. See the " +
329         "documentation of AnimatedState for more information."
330 
331 internal class SharedValue<T, Delta>(val type: SharedValueType<T, Delta>) {
332     /** The target value of this shared value for each content. */
333     val targetValues = SnapshotStateMap<ContentKey, T>()
334 
335     /** The last value of this shared value. */
336     var lastValue: T = type.unspecifiedValue
337 
338     /** The value of this shared value before the last interruption (if any). */
339     var valueBeforeInterruption: T = type.unspecifiedValue
340 
341     /** The delta value to add to this shared value to have smoother interruptions. */
342     var valueInterruptionDelta = type.zeroDeltaValue
343 
344     /** The last transition that was used when the value of this shared state. */
345     var lastTransition: TransitionState.Transition? = null
346 }
347 
348 private class AnimatedStateImpl<T, Delta>(
349     private val layoutImpl: SceneTransitionLayoutImpl,
350     private val content: ContentKey,
351     private val element: ElementKey?,
352     private val key: ValueKey,
353     private val canOverflow: Boolean,
354 ) : AnimatedState<T> {
355     override val value: T
356         get() = value()
357 
valuenull358     private fun value(): T {
359         val sharedValue = sharedValue<T, Delta>(layoutImpl, key, element)
360         val transition = transition(sharedValue)
361         val value: T =
362             valueOrNull(sharedValue, transition)
363                 // TODO(b/311600838): Remove this. We should not have to fallback to the current
364                 // scene value, but we have to because code of removed nodes can still run if they
365                 // are placed with a graphics layer.
366                 ?: sharedValue[content]
367                 ?: error(valueReadTooEarlyMessage(key))
368         val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
369         sharedValue.lastValue = interruptedValue
370         return interruptedValue
371     }
372 
getnull373     private operator fun SharedValue<T, *>.get(content: ContentKey): T? = targetValues[content]
374 
375     private fun valueOrNull(
376         sharedValue: SharedValue<T, *>,
377         transition: TransitionState.Transition?,
378     ): T? {
379         if (transition == null) {
380             return sharedValue[content]
381                 ?: sharedValue[layoutImpl.state.transitionState.currentScene]
382         }
383 
384         val fromValue = sharedValue[transition.fromContent]
385         val toValue = sharedValue[transition.toContent]
386         if (fromValue == null && toValue == null) {
387             return null
388         }
389 
390         if (fromValue != null && toValue != null) {
391             return interpolateSharedValue(fromValue, toValue, transition, sharedValue)
392         }
393 
394         if (transition is TransitionState.Transition.ReplaceOverlay) {
395             val currentSceneValue = sharedValue[transition.currentScene]
396             if (currentSceneValue != null) {
397                 return interpolateSharedValue(
398                     fromValue = fromValue ?: currentSceneValue,
399                     toValue = toValue ?: currentSceneValue,
400                     transition,
401                     sharedValue,
402                 )
403             }
404         }
405 
406         return fromValue ?: toValue
407     }
408 
interpolateSharedValuenull409     private fun interpolateSharedValue(
410         fromValue: T,
411         toValue: T,
412         transition: TransitionState.Transition,
413         sharedValue: SharedValue<T, *>,
414     ): T? {
415         if (fromValue == toValue) {
416             // Optimization: avoid reading progress if the values are the same, so we don't
417             // relayout/redraw for nothing.
418             return fromValue
419         }
420 
421         val progress =
422             if (canOverflow) {
423                 transition.progress
424             } else {
425                 transition.progress.fastCoerceIn(0f, 1f)
426             }
427 
428         return sharedValue.type.lerp(fromValue, toValue, progress)
429     }
430 
transitionnull431     private fun transition(sharedValue: SharedValue<T, Delta>): TransitionState.Transition? {
432         val targetValues = sharedValue.targetValues
433         val transition =
434             if (element != null) {
435                 layoutImpl.elements[element]?.let { element ->
436                     elementState(
437                         listOf(layoutImpl.state.transitionStates),
438                         elementKey = element.key,
439                         isInContent = { it in element.stateByContent },
440                     )
441                         as? TransitionState.Transition
442                 }
443             } else {
444                 layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
445                     transition.fromContent in targetValues || transition.toContent in targetValues
446                 }
447             }
448 
449         val previousTransition = sharedValue.lastTransition
450         sharedValue.lastTransition = transition
451 
452         if (transition != previousTransition && transition != null && previousTransition != null) {
453             // The previous transition was interrupted by another transition.
454             sharedValue.valueBeforeInterruption = sharedValue.lastValue
455             sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
456         } else if (transition == null && previousTransition != null) {
457             // The transition was just finished.
458             sharedValue.valueBeforeInterruption = sharedValue.type.unspecifiedValue
459             sharedValue.valueInterruptionDelta = sharedValue.type.zeroDeltaValue
460         }
461 
462         return transition
463     }
464 
465     /**
466      * Compute what [value] should be if we take the
467      * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into
468      * account.
469      */
computeInterruptedValuenull470     private fun computeInterruptedValue(
471         sharedValue: SharedValue<T, Delta>,
472         transition: TransitionState.Transition?,
473         value: T,
474     ): T {
475         val type = sharedValue.type
476         if (sharedValue.valueBeforeInterruption != type.unspecifiedValue) {
477             sharedValue.valueInterruptionDelta =
478                 type.diff(sharedValue.valueBeforeInterruption, value)
479             sharedValue.valueBeforeInterruption = type.unspecifiedValue
480         }
481 
482         val delta = sharedValue.valueInterruptionDelta
483         return if (delta == type.zeroDeltaValue || transition == null) {
484             value
485         } else {
486             val interruptionProgress = transition.interruptionProgress(layoutImpl)
487             if (interruptionProgress == 0f) {
488                 value
489             } else {
490                 type.addWeighted(value, delta, interruptionProgress)
491             }
492         }
493     }
494 
495     @Composable
unsafeCompositionStatenull496     override fun unsafeCompositionState(initialValue: T): State<T> {
497         val state = remember { mutableStateOf(initialValue) }
498 
499         val animatedState = this
500         LaunchedEffect(animatedState) {
501             snapshotFlow { animatedState.value }.collect { state.value = it }
502         }
503 
504         return state
505     }
506 }
507