1 /*
2  * Copyright 2021 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 androidx.compose.animation.core
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.State
24 import androidx.compose.runtime.collection.mutableVectorOf
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.remember
28 import androidx.compose.runtime.setValue
29 import androidx.compose.runtime.snapshotFlow
30 import androidx.compose.ui.geometry.Size
31 import androidx.compose.ui.unit.Dp
32 import kotlinx.coroutines.flow.first
33 
34 /**
35  * Creates a [InfiniteTransition] that runs infinite child animations. Child animations can be added
36  * using [InfiniteTransition.animateColor][androidx.compose.animation.animateColor],
37  * [InfiniteTransition.animateFloat], or [InfiniteTransition.animateValue]. Child animations will
38  * start running as soon as they enter the composition, and will not stop until they are removed
39  * from the composition.
40  *
41  * @param label A label for differentiating this animation from others in android studio.
42  * @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
43  */
44 @Composable
rememberInfiniteTransitionnull45 public fun rememberInfiniteTransition(label: String = "InfiniteTransition"): InfiniteTransition {
46     val infiniteTransition = remember { InfiniteTransition(label) }
47     infiniteTransition.run()
48     return infiniteTransition
49 }
50 
51 /**
52  * [InfiniteTransition] is responsible for running child animations. Child animations can be added
53  * using [InfiniteTransition.animateColor][androidx.compose.animation.animateColor],
54  * [InfiniteTransition.animateFloat], or [InfiniteTransition.animateValue]. Child animations will
55  * start running as soon as they enter the composition, and will not stop until they are removed
56  * from the composition.
57  *
58  * @param label A label for differentiating this animation from others in android studio.
59  * @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
60  */
61 public class InfiniteTransition internal constructor(public val label: String) {
62 
63     /**
64      * Each animation created using
65      * [InfiniteTransition.animateColor][androidx.compose.animation.animateColor],
66      * [InfiniteTransition.animateFloat], or [InfiniteTransition.animateValue] is represented as a
67      * [TransitionAnimationState] in [InfiniteTransition]. [typeConverter] converts the animation
68      * value from/to an [AnimationVector]. [label] differentiates this animation from others in
69      * android studio.
70      */
71     public inner class TransitionAnimationState<T, V : AnimationVector>
72     internal constructor(
73         internal var initialValue: T,
74         internal var targetValue: T,
75         public val typeConverter: TwoWayConverter<T, V>,
76         animationSpec: AnimationSpec<T>,
77         public val label: String
78     ) : State<T> {
79         override var value: T by mutableStateOf(initialValue)
80             internal set
81 
82         /** [AnimationSpec] that is used for current animation run. */
83         public var animationSpec: AnimationSpec<T> = animationSpec
84             private set
85 
86         /**
87          * All the animation configurations including initial value/velocity & target value for
88          * animating from [initialValue] to [targetValue] are captured in [animation].
89          */
90         public var animation: TargetBasedAnimation<T, V> =
91             TargetBasedAnimation(this.animationSpec, typeConverter, initialValue, targetValue)
92             internal set
93 
94         // This is used to signal parent for less work in a normal running mode, but in seeking
95         // this is ignored since time can go both ways.
96         internal var isFinished = false
97 
98         // If animation is refreshed during the run, start the new animation in the next frame
99         private var startOnTheNextFrame = false
100 
101         // When the animation changes, it needs to start from playtime 0 again, offsetting from
102         // parent's playtime to achieve that.
103         private var playTimeNanosOffset = 0L
104 
105         // This gets called when the initial/target value changes, which should be a rare case.
updateValuesnull106         internal fun updateValues(
107             initialValue: T,
108             targetValue: T,
109             animationSpec: AnimationSpec<T>
110         ) {
111             this.initialValue = initialValue
112             this.targetValue = targetValue
113             this.animationSpec = animationSpec
114             // Create a new animation if anything (i.e. initial/target) has changed
115             // TODO: Consider providing some continuity maybe?
116             animation =
117                 TargetBasedAnimation(animationSpec, typeConverter, initialValue, targetValue)
118             refreshChildNeeded = true
119             isFinished = false
120             startOnTheNextFrame = true
121         }
122 
123         /** Set play time for the [animation]. */
onPlayTimeChangednull124         internal fun onPlayTimeChanged(playTimeNanos: Long) {
125             refreshChildNeeded = false
126             if (startOnTheNextFrame) {
127                 startOnTheNextFrame = false
128                 playTimeNanosOffset = playTimeNanos
129             }
130             val playTime = playTimeNanos - playTimeNanosOffset
131             value = animation.getValueFromNanos(playTime)
132             isFinished = animation.isFinishedFromNanos(playTime)
133         }
134 
skipToEndnull135         internal fun skipToEnd() {
136             value = animation.targetValue
137             startOnTheNextFrame = true
138         }
139 
resetnull140         internal fun reset() {
141             startOnTheNextFrame = true
142         }
143     }
144 
145     private val _animations = mutableVectorOf<TransitionAnimationState<*, *>>()
146     private var refreshChildNeeded by mutableStateOf(false)
147     private var startTimeNanos = AnimationConstants.UnspecifiedTime
148     private var isRunning by mutableStateOf(true)
149 
150     /** List of [TransitionAnimationState]s that are in a [InfiniteTransition]. */
151     public val animations: List<TransitionAnimationState<*, *>>
152         get() = _animations.asMutableList()
153 
addAnimationnull154     internal fun addAnimation(animation: TransitionAnimationState<*, *>) {
155         _animations.add(animation)
156         refreshChildNeeded = true
157     }
158 
removeAnimationnull159     internal fun removeAnimation(animation: TransitionAnimationState<*, *>) {
160         _animations.remove(animation)
161     }
162 
163     @Suppress("ComposableNaming")
164     @Composable
runnull165     internal fun run() {
166         val toolingOverride = remember { mutableStateOf<State<Long>?>(null) }
167         if (isRunning || refreshChildNeeded) {
168             LaunchedEffect(this) {
169                 var durationScale = 1f
170                 // Restart every time duration scale changes
171                 while (true) {
172                     withInfiniteAnimationFrameNanos {
173                         val currentTimeNanos = toolingOverride.value?.value ?: it
174                         if (
175                             startTimeNanos == AnimationConstants.UnspecifiedTime ||
176                                 durationScale != coroutineContext.durationScale
177                         ) {
178                             startTimeNanos = it
179                             _animations.forEach { it.reset() }
180                             durationScale = coroutineContext.durationScale
181                         }
182                         if (durationScale == 0f) {
183                             // Finish right away
184                             _animations.forEach { it.skipToEnd() }
185                         } else {
186                             val playTimeNanos =
187                                 ((currentTimeNanos - startTimeNanos) / durationScale).toLong()
188                             onFrame(playTimeNanos)
189                         }
190                     }
191                     // Suspend until duration scale is non-zero
192                     if (durationScale == 0f) {
193                         snapshotFlow { coroutineContext.durationScale }.first { it > 0f }
194                     }
195                 }
196             }
197         }
198     }
199 
onFramenull200     private fun onFrame(playTimeNanos: Long) {
201         var allFinished = true
202         // Pulse new playtime
203         _animations.forEach {
204             if (!it.isFinished) {
205                 it.onPlayTimeChanged(playTimeNanos)
206             }
207             // Check isFinished flag again after the animation pulse
208             if (!it.isFinished) {
209                 allFinished = false
210             }
211         }
212         isRunning = !allFinished
213     }
214 }
215 
216 /**
217  * Creates an animation of type [T] that runs infinitely as a part of the given
218  * [InfiniteTransition]. Any data type can be animated so long as it can be converted from and to an
219  * [AnimationVector]. This conversion needs to be provided as a [typeConverter]. Some examples of
220  * such [TwoWayConverter] are: [Int.VectorConverter][Int.Companion.VectorConverter],
221  * [Dp.VectorConverter][Dp.Companion.VectorConverter],
222  * [Size.VectorConverter][Size.Companion.VectorConverter], etc
223  *
224  * Once the animation is created, it will run from [initialValue] to [targetValue] and repeat.
225  * Depending on the [RepeatMode] of the provided [animationSpec], the animation could either restart
226  * after each iteration (i.e. [RepeatMode.Restart]), or reverse after each iteration (i.e .
227  * [RepeatMode.Reverse]).
228  *
229  * If [initialValue] or [targetValue] is changed at any point during the animation, the animation
230  * will be restarted with the new [initialValue] and [targetValue]. __Note__: this means continuity
231  * will *not* be preserved.
232  *
233  * A [label] for differentiating this animation from others in android studio.
234  *
235  * @sample androidx.compose.animation.core.samples.InfiniteTransitionAnimateValueSample
236  * @see [InfiniteTransition.animateFloat]
237  * @see [androidx.compose.animation.animateColor]
238  */
239 @Composable
animateValuenull240 public fun <T, V : AnimationVector> InfiniteTransition.animateValue(
241     initialValue: T,
242     targetValue: T,
243     typeConverter: TwoWayConverter<T, V>,
244     animationSpec: InfiniteRepeatableSpec<T>,
245     label: String = "ValueAnimation"
246 ): State<T> {
247     val transitionAnimation = remember {
248         TransitionAnimationState(initialValue, targetValue, typeConverter, animationSpec, label)
249     }
250 
251     SideEffect {
252         if (
253             initialValue != transitionAnimation.initialValue ||
254                 targetValue != transitionAnimation.targetValue
255         ) {
256             transitionAnimation.updateValues(
257                 initialValue = initialValue,
258                 targetValue = targetValue,
259                 animationSpec = animationSpec
260             )
261         }
262     }
263 
264     DisposableEffect(transitionAnimation) {
265         addAnimation(transitionAnimation)
266         onDispose { removeAnimation(transitionAnimation) }
267     }
268     return transitionAnimation
269 }
270 
271 /**
272  * Creates an animation of Float type that runs infinitely as a part of the given
273  * [InfiniteTransition].
274  *
275  * Once the animation is created, it will run from [initialValue] to [targetValue] and repeat.
276  * Depending on the [RepeatMode] of the provided [animationSpec], the animation could either restart
277  * after each iteration (i.e. [RepeatMode.Restart]), or reverse after each iteration (i.e .
278  * [RepeatMode.Reverse]).
279  *
280  * If [initialValue] or [targetValue] is changed at any point during the animation, the animation
281  * will be restarted with the new [initialValue] and [targetValue]. __Note__: this means continuity
282  * will *not* be preserved.
283  *
284  * A [label] for differentiating this animation from others in android studio.
285  *
286  * @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
287  * @see [InfiniteTransition.animateValue]
288  * @see [androidx.compose.animation.animateColor]
289  */
290 @Composable
animateFloatnull291 public fun InfiniteTransition.animateFloat(
292     initialValue: Float,
293     targetValue: Float,
294     animationSpec: InfiniteRepeatableSpec<Float>,
295     label: String = "FloatAnimation"
296 ): State<Float> =
297     animateValue(initialValue, targetValue, Float.VectorConverter, animationSpec, label)
298 
299 @Deprecated(
300     "rememberInfiniteTransition APIs now have a new label parameter added.",
301     level = DeprecationLevel.HIDDEN
302 )
303 @Composable
304 public fun rememberInfiniteTransition(): InfiniteTransition {
305     return rememberInfiniteTransition("InfiniteTransition")
306 }
307 
308 @Deprecated(
309     "animateValue APIs now have a new label parameter added.",
310     level = DeprecationLevel.HIDDEN
311 )
312 @Composable
animateValuenull313 public fun <T, V : AnimationVector> InfiniteTransition.animateValue(
314     initialValue: T,
315     targetValue: T,
316     typeConverter: TwoWayConverter<T, V>,
317     animationSpec: InfiniteRepeatableSpec<T>,
318 ): State<T> {
319     return animateValue(
320         initialValue = initialValue,
321         targetValue = targetValue,
322         typeConverter = typeConverter,
323         animationSpec = animationSpec,
324         label = "ValueAnimation"
325     )
326 }
327 
328 @Deprecated(
329     "animateFloat APIs now have a new label parameter added.",
330     level = DeprecationLevel.HIDDEN
331 )
332 @Composable
animateFloatnull333 public fun InfiniteTransition.animateFloat(
334     initialValue: Float,
335     targetValue: Float,
336     animationSpec: InfiniteRepeatableSpec<Float>
337 ): State<Float> {
338     return animateFloat(
339         initialValue = initialValue,
340         targetValue = targetValue,
341         animationSpec = animationSpec,
342         label = "FloatAnimation"
343     )
344 }
345