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