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.DisposableEffectResult
22 import androidx.compose.runtime.DisposableEffectScope
23 import androidx.compose.runtime.State
24 import androidx.compose.runtime.derivedStateOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.ui.graphics.Color
27 import androidx.compose.ui.graphics.lerp
28 import androidx.compose.ui.unit.Dp
29 import androidx.compose.ui.unit.lerp
30 import com.android.compose.ui.util.lerp
31 
32 /**
33  * Animate a shared Int value.
34  *
35  * @see SceneScope.animateSharedValueAsState
36  */
37 @Composable
38 fun SceneScope.animateSharedIntAsState(
39     value: Int,
40     key: ValueKey,
41     element: ElementKey,
42     canOverflow: Boolean = true,
43 ): State<Int> {
44     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
45 }
46 
47 /**
48  * Animate a shared Float value.
49  *
50  * @see SceneScope.animateSharedValueAsState
51  */
52 @Composable
53 fun SceneScope.animateSharedFloatAsState(
54     value: Float,
55     key: ValueKey,
56     element: ElementKey,
57     canOverflow: Boolean = true,
58 ): State<Float> {
59     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
60 }
61 
62 /**
63  * Animate a shared Dp value.
64  *
65  * @see SceneScope.animateSharedValueAsState
66  */
67 @Composable
68 fun SceneScope.animateSharedDpAsState(
69     value: Dp,
70     key: ValueKey,
71     element: ElementKey,
72     canOverflow: Boolean = true,
73 ): State<Dp> {
74     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
75 }
76 
77 /**
78  * Animate a shared Color value.
79  *
80  * @see SceneScope.animateSharedValueAsState
81  */
82 @Composable
83 fun SceneScope.animateSharedColorAsState(
84     value: Color,
85     key: ValueKey,
86     element: ElementKey,
87 ): State<Color> {
88     return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
89 }
90 
91 @Composable
92 internal fun <T> animateSharedValueAsState(
93     layoutImpl: SceneTransitionLayoutImpl,
94     scene: Scene,
95     element: Element,
96     key: ValueKey,
97     value: T,
98     lerp: (T, T, Float) -> T,
99     canOverflow: Boolean,
100 ): State<T> {
101     val sharedValue = remember(key) { Element.SharedValue(key, value) }
102     if (value != sharedValue.value) {
103         sharedValue.value = value
104     }
105 
106     DisposableEffect(element, scene, sharedValue) {
107         addSharedValueToElement(element, scene, sharedValue)
108     }
109 
110     return remember(layoutImpl, element, sharedValue, lerp, canOverflow) {
111         derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) }
112     }
113 }
114 
115 private fun <T> DisposableEffectScope.addSharedValueToElement(
116     element: Element,
117     scene: Scene,
118     sharedValue: Element.SharedValue<T>,
119 ): DisposableEffectResult {
120     val sceneValues =
121         element.sceneValues[scene.key] ?: error("Element $element is not present in $scene")
122     val sharedValues = sceneValues.sharedValues
123 
124     sharedValues[sharedValue.key] = sharedValue
125     return onDispose { sharedValues.remove(sharedValue.key) }
126 }
127 
128 private fun <T> computeValue(
129     layoutImpl: SceneTransitionLayoutImpl,
130     element: Element,
131     sharedValue: Element.SharedValue<T>,
132     lerp: (T, T, Float) -> T,
133     canOverflow: Boolean,
134 ): T {
135     val state = layoutImpl.state.transitionState
136     if (
137         state !is TransitionState.Transition ||
138             state.fromScene == state.toScene ||
139             !layoutImpl.isTransitionReady(state)
140     ) {
141         return sharedValue.value
142     }
143 
144     fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
145         val sceneValues = element.sceneValues[scene] ?: return null
146         val value = sceneValues.sharedValues[sharedValue.key] ?: return null
147         return value as Element.SharedValue<T>
148     }
149 
150     val fromValue = sceneValue(state.fromScene)
151     val toValue = sceneValue(state.toScene)
152     return if (fromValue != null && toValue != null) {
153         val progress = if (canOverflow) state.progress else state.progress.coerceIn(0f, 1f)
154         lerp(fromValue.value, toValue.value, progress)
155     } else if (fromValue != null) {
156         fromValue.value
157     } else if (toValue != null) {
158         toValue.value
159     } else {
160         sharedValue.value
161     }
162 }
163