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