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