• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 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 package com.android.wallpaper.picker.customization.animation.view
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.TimeAnimator
21 import android.animation.ValueAnimator
22 import android.graphics.Color
23 import android.graphics.RenderEffect
24 import android.graphics.Shader
25 import android.view.View
26 import com.android.systemui.monet.ColorScheme
27 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
28 import com.android.wallpaper.picker.customization.animation.Interpolators
29 import com.android.wallpaper.picker.customization.animation.shader.CircularRevealShader
30 import com.android.wallpaper.picker.customization.animation.shader.CompositeLoadingShader
31 import com.android.wallpaper.picker.customization.animation.shader.SparkleShader
32 import kotlin.math.max
33 
34 /** Renders loading and reveal animation. */
35 // TODO (b/281878827): remove this and use loading animation in SystemUIShaderLib when available
36 class LoadingAnimation(
37     /** The view used to play the loading and reveal animation */
38     private val revealOverlay: View,
39     /** The type of reveal animation to play */
40     private val revealType: RevealType = RevealType.CIRCULAR,
41     /**
42      * Amount of time before the loading animation times out and plays reveal animation, null
43      * represents no time out
44      */
45     private val timeOutDuration: Long? = null
46 ) {
47 
48     /** Type representing the reveal animation to be played in [LoadingAnimation] */
49     enum class RevealType {
50         /**
51          * Reveal animation that reveals the views beneath with an expanding circle starting from
52          * the center, ending with the loading view hidden
53          */
54         CIRCULAR,
55         /**
56          * Reveal animation that fades out the animation effects on the loading view, leaving the
57          * original loading view visible
58          */
59         FADE
60     }
61 
62     private val pixelDensity = revealOverlay.resources.displayMetrics.density
63 
64     private val loadingShader = CompositeLoadingShader()
65     private val colorTurbulenceNoiseShader =
66         TurbulenceNoiseShader().apply {
67             setPixelDensity(pixelDensity)
68             setGridCount(NOISE_SIZE)
69             setOpacity(1f)
70             setInverseNoiseLuminosity(inverse = true)
71             setBackgroundColor(Color.BLACK)
72         }
73     private val sparkleShader =
74         SparkleShader().apply {
75             setPixelDensity(pixelDensity)
76             setGridCount(NOISE_SIZE)
77         }
78     private val revealShader = CircularRevealShader()
79 
80     // Do not set blur radius to 0. It causes a crash.
81     private var blurRadius: Float = MIN_BLUR_PX
82 
83     private var elapsedTime = 0L
84     private var transitionProgress = 0f
85     // Responsible for fade in and blur on start of the loading.
86     private var fadeInAnimator: ValueAnimator? = null
87     private var timeAnimator: TimeAnimator? = null
88     private var revealAnimator: ValueAnimator? = null
89 
90     private var animationState = AnimationState.IDLE
91 
92     private var blurEffect =
93         RenderEffect.createBlurEffect(
94             blurRadius * pixelDensity,
95             blurRadius * pixelDensity,
96             Shader.TileMode.CLAMP
97         )
98 
99     fun playLoadingAnimation(seed: Long? = null) {
100         if (
101             animationState == AnimationState.FADE_IN_PLAYING ||
102                 animationState == AnimationState.FADE_IN_PLAYED
103         )
104             return
105 
106         if (animationState == AnimationState.REVEAL_PLAYING) revealAnimator?.cancel()
107 
108         animationState = AnimationState.FADE_IN_PLAYING
109 
110         revealOverlay.visibility = View.VISIBLE
111 
112         elapsedTime = seed ?: (0L..10000L).random()
113 
114         fadeInAnimator?.cancel()
115         timeAnimator?.cancel()
116         revealAnimator?.cancel()
117 
118         fadeInAnimator =
119             ValueAnimator.ofFloat(transitionProgress, 1f).apply {
120                 duration = FADE_IN_DURATION_MS
121                 interpolator = Interpolators.STANDARD_DECELERATE
122                 addUpdateListener {
123                     transitionProgress = it.animatedValue as Float
124                     loadingShader.setAlpha(transitionProgress)
125                     // Match the timing with the fade animations.
126                     blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX)
127                 }
128                 addListener(
129                     object : AnimatorListenerAdapter() {
130                         override fun onAnimationEnd(animation: Animator) {
131                             animationState = AnimationState.FADE_IN_PLAYED
132                         }
133                     }
134                 )
135                 start()
136             }
137 
138         // Keep clouds moving until we finish loading
139         timeAnimator =
140             TimeAnimator().apply {
141                 setTimeListener { _, totalTime, deltaTime -> flushUniforms(totalTime, deltaTime) }
142                 start()
143             }
144     }
145 
146     fun playRevealAnimation() {
147         when (revealType) {
148             RevealType.CIRCULAR -> playCircularRevealAnimation()
149             RevealType.FADE -> playFadeRevealAnimation()
150         }
151     }
152 
153     private fun playCircularRevealAnimation() {
154         if (
155             animationState == AnimationState.REVEAL_PLAYING ||
156                 animationState == AnimationState.REVEAL_PLAYED ||
157                 animationState == AnimationState.FADE_OUT_PLAYING ||
158                 animationState == AnimationState.FADE_OUT_PLAYED
159         )
160             return
161 
162         if (animationState == AnimationState.FADE_IN_PLAYING) fadeInAnimator?.cancel()
163 
164         animationState = AnimationState.REVEAL_PLAYING
165 
166         revealOverlay.visibility = View.VISIBLE
167 
168         revealShader.setCenter(revealOverlay.width * 0.5f, revealOverlay.height * 0.5f)
169 
170         revealAnimator?.cancel()
171         revealAnimator =
172             ValueAnimator.ofFloat(0f, 1f).apply {
173                 duration = REVEAL_DURATION_MS
174                 interpolator = Interpolators.STANDARD
175 
176                 addUpdateListener {
177                     val progress = it.animatedValue as Float
178 
179                     // Draw a circle slightly larger than the screen. Need some offset due to large
180                     // blur radius.
181                     revealShader.setRadius(
182                         progress * max(revealOverlay.width, revealOverlay.height) * 2f
183                     )
184                     // Map [0,1] to [MAX, MIN].
185                     val blurAmount =
186                         (1f - progress) * (MAX_REVEAL_BLUR_AMOUNT - MIN_REVEAL_BLUR_AMOUNT) +
187                             MIN_REVEAL_BLUR_AMOUNT
188                     revealShader.setBlur(blurAmount)
189                 }
190 
191                 addListener(
192                     object : AnimatorListenerAdapter() {
193                         override fun onAnimationEnd(animation: Animator) {
194                             resetCircularRevealAnimation()
195                         }
196                     }
197                 )
198 
199                 start()
200             }
201     }
202 
203     private fun resetCircularRevealAnimation() {
204         animationState = AnimationState.REVEAL_PLAYED
205 
206         revealOverlay.setRenderEffect(null)
207         revealOverlay.visibility = View.INVISIBLE
208 
209         // Stop turbulence and reset everything.
210         timeAnimator?.cancel()
211         blurRadius = MIN_BLUR_PX
212         transitionProgress = 0f
213     }
214 
215     private fun playFadeRevealAnimation() {
216         if (
217             animationState == AnimationState.REVEAL_PLAYING ||
218                 animationState == AnimationState.REVEAL_PLAYED ||
219                 animationState == AnimationState.FADE_OUT_PLAYING ||
220                 animationState == AnimationState.FADE_OUT_PLAYED
221         )
222             return
223 
224         if (animationState == AnimationState.FADE_IN_PLAYING) fadeInAnimator?.cancel()
225 
226         animationState = AnimationState.FADE_OUT_PLAYING
227 
228         revealOverlay.visibility = View.VISIBLE
229 
230         fadeInAnimator =
231             ValueAnimator.ofFloat(transitionProgress, 0f).apply {
232                 duration = FADE_OUT_DURATION_MS
233                 interpolator = Interpolators.STANDARD_DECELERATE
234                 addUpdateListener {
235                     transitionProgress = it.animatedValue as Float
236                     loadingShader.setAlpha(transitionProgress)
237                     // Match the timing with the fade animations.
238                     blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX)
239                 }
240                 addListener(
241                     object : AnimatorListenerAdapter() {
242                         override fun onAnimationEnd(animation: Animator) {
243                             resetFadeRevealAnimation()
244                         }
245                     }
246                 )
247                 start()
248             }
249     }
250 
251     private fun resetFadeRevealAnimation() {
252         animationState = AnimationState.FADE_OUT_PLAYED
253 
254         revealOverlay.setRenderEffect(null)
255 
256         // Stop turbulence and reset everything.
257         timeAnimator?.cancel()
258         blurRadius = MIN_BLUR_PX
259         transitionProgress = 0f
260     }
261 
262     fun updateColor(colorScheme: ColorScheme) {
263         colorTurbulenceNoiseShader.apply {
264             setColor(colorScheme.accent1.s600)
265             setBackgroundColor(colorScheme.accent1.s900)
266         }
267 
268         sparkleShader.setColor(colorScheme.accent1.s600)
269         loadingShader.setScreenColor(colorScheme.accent1.s900)
270     }
271 
272     private fun flushUniforms(totalTime: Long, deltaTime: Long) {
273         elapsedTime += deltaTime
274         val time = elapsedTime / 1000f
275         val viewWidth = revealOverlay.width.toFloat()
276         val viewHeight = revealOverlay.height.toFloat()
277 
278         colorTurbulenceNoiseShader.apply {
279             setSize(viewWidth, viewHeight)
280             setNoiseMove(time * NOISE_SPEED, 0f, time * NOISE_SPEED)
281         }
282 
283         sparkleShader.apply {
284             setSize(viewWidth, viewHeight)
285             setNoiseMove(time * NOISE_SPEED, 0f, time * NOISE_SPEED)
286             setTime(time)
287         }
288 
289         loadingShader.apply {
290             setSparkle(sparkleShader)
291             setColorTurbulenceMask(colorTurbulenceNoiseShader)
292         }
293 
294         val renderEffect = RenderEffect.createRuntimeShaderEffect(loadingShader, "in_background")
295 
296         // Update the blur effect only when loading animation is playing.
297         if (
298             animationState == AnimationState.FADE_IN_PLAYING ||
299                 animationState == AnimationState.FADE_OUT_PLAYING
300         ) {
301             blurEffect =
302                 RenderEffect.createBlurEffect(
303                     blurRadius * pixelDensity,
304                     blurRadius * pixelDensity,
305                     Shader.TileMode.MIRROR
306                 )
307         }
308 
309         // Animation time out
310         if (
311             timeOutDuration != null &&
312                 totalTime > timeOutDuration &&
313                 animationState == AnimationState.FADE_IN_PLAYED
314         ) {
315             playRevealAnimation()
316         }
317 
318         if (animationState == AnimationState.REVEAL_PLAYING) {
319             revealOverlay.setRenderEffect(
320                 RenderEffect.createChainEffect(
321                     RenderEffect.createRuntimeShaderEffect(revealShader, "in_src"),
322                     RenderEffect.createChainEffect(renderEffect, blurEffect)
323                 )
324             )
325         } else {
326             revealOverlay.setRenderEffect(RenderEffect.createChainEffect(renderEffect, blurEffect))
327         }
328     }
329 
330     /** Cancels the animation. Unlike end() , cancel() causes the animation to stop in its tracks */
331     fun cancel() {
332         fadeInAnimator?.cancel()
333         timeAnimator?.cancel()
334         revealAnimator?.removeAllListeners()
335         revealAnimator?.removeAllUpdateListeners()
336         revealAnimator?.cancel()
337     }
338 
339     /** Ends the animation, and causes the animation to skip to the end state */
340     fun end() {
341         fadeInAnimator?.end()
342         timeAnimator?.end()
343         revealAnimator?.removeAllListeners()
344         revealAnimator?.removeAllUpdateListeners()
345         revealAnimator?.end()
346         when (revealType) {
347             RevealType.CIRCULAR -> resetCircularRevealAnimation()
348             RevealType.FADE -> resetFadeRevealAnimation()
349         }
350     }
351 
352     fun setupRevealAnimation(seed: Long? = null, revealTransitionProgress: Float? = null) {
353         cancel()
354 
355         revealOverlay.visibility = View.VISIBLE
356 
357         elapsedTime = seed ?: (0L..10000L).random()
358         transitionProgress = revealTransitionProgress ?: 1f
359 
360         // Fast forward to state at the end of fade in animation
361         blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX)
362         blurEffect =
363             RenderEffect.createBlurEffect(
364                 blurRadius * pixelDensity,
365                 blurRadius * pixelDensity,
366                 Shader.TileMode.MIRROR
367             )
368         animationState = AnimationState.FADE_IN_PLAYED
369         loadingShader.setAlpha(transitionProgress)
370 
371         // Keep clouds moving until we finish loading
372         timeAnimator =
373             TimeAnimator().apply {
374                 setTimeListener { _, totalTime, deltaTime -> flushUniforms(totalTime, deltaTime) }
375                 start()
376             }
377     }
378 
379     fun getElapsedTime(): Long {
380         return elapsedTime
381     }
382 
383     fun getTransitionProgress(): Float {
384         return transitionProgress
385     }
386 
387     companion object {
388         private const val NOISE_SPEED = 0.2f
389         private const val NOISE_SIZE = 1.7f
390         private const val MAX_BLUR_PX = 80f
391         private const val MIN_BLUR_PX = 1f
392         private const val FADE_IN_DURATION_MS = 1100L
393         private const val FADE_OUT_DURATION_MS = 1500L
394         const val TIME_OUT_DURATION_MS = 10000L
395         private const val REVEAL_DURATION_MS = 3600L
396         private const val MIN_REVEAL_BLUR_AMOUNT = 1f
397         private const val MAX_REVEAL_BLUR_AMOUNT = 2.5f
398     }
399 
400     enum class AnimationState {
401         IDLE,
402         FADE_IN_PLAYING,
403         FADE_IN_PLAYED,
404         FADE_OUT_PLAYING,
405         FADE_OUT_PLAYED,
406         REVEAL_PLAYING,
407         REVEAL_PLAYED
408     }
409 }
410