1 /*
<lambda>null2  * Copyright (C) 2024 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.systemui.surfaceeffects.loadingeffect
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.graphics.Paint
23 import android.graphics.RenderEffect
24 import android.view.View
25 import com.android.systemui.surfaceeffects.PaintDrawCallback
26 import com.android.systemui.surfaceeffects.RenderEffectDrawCallback
27 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
28 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
29 
30 /**
31  * Plays loading effect with the given configuration.
32  *
33  * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct
34  *   the [LoadingEffect] if the base type needs to be changed.
35  * @param config immutable parameters that are used for drawing the effect.
36  * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect
37  *   with [Canvas.drawPaint].
38  * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the
39  *   effect with [RenderEffect].
40  * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional.
41  *
42  * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the
43  * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to
44  * render the effect:
45  * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!)
46  * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.)
47  * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.)
48  *
49  * <p>First approach is more performant than other ones because [RenderEffect] forces an
50  * intermediate render pass of the View to a texture to feed into it.
51  *
52  * <p>If going with the first approach, your custom [View] would look like as follow:
53  * <pre>{@code
54  *     private var paint: Paint? = null
55  *     // Override [View.onDraw].
56  *     override fun onDraw(canvas: Canvas) {
57  *         // RuntimeShader requires hardwareAcceleration.
58  *         if (!canvas.isHardwareAccelerated) return
59  *
60  *         paint?.let { canvas.drawPaint(it) }
61  *     }
62  *
63  *     // This is called [Callback.onDraw]
64  *     fun draw(paint: Paint) {
65  *         this.paint = paint
66  *
67  *         // Must call invalidate to trigger View#onDraw
68  *         invalidate()
69  *     }
70  * }</pre>
71  *
72  * <p>If going with the second approach, it doesn't require an extra custom [View], and it is as
73  * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the
74  * effect with other [RenderEffect].
75  *
76  * <p>Third approach is an option, but it's more of a boilerplate so you would like to stick with
77  * the second option. If you want to go with this option for some reason, below is the example:
78  * <pre>{@code
79  *     // Initialize the shader and paint to use to pass into the [Canvas].
80  *     private val renderNode = RenderNode("LoadingEffect")
81  *
82  *     // Override [View.onDraw].
83  *     override fun onDraw(canvas: Canvas) {
84  *         // RuntimeShader requires hardwareAcceleration.
85  *         if (!canvas.isHardwareAccelerated) return
86  *
87  *         if (renderNode.hasDisplayList()) {
88  *             canvas.drawRenderNode(renderNode)
89  *         }
90  *     }
91  *
92  *     // This is called [Callback.onDraw]
93  *     fun draw(renderEffect: RenderEffect) {
94  *         renderNode.setPosition(0, 0, width, height)
95  *         renderNode.setRenderEffect(renderEffect)
96  *
97  *         val recordingCanvas = renderNode.beginRecording()
98  *         // We need at least 1 drawing instruction.
99  *         recordingCanvas.drawColor(Color.TRANSPARENT)
100  *         renderNode.endRecording()
101  *
102  *         // Must call invalidate to trigger View#onDraw
103  *         invalidate()
104  *     }
105  * }</pre>
106  */
107 class LoadingEffect
108 private constructor(
109     baseType: TurbulenceNoiseShader.Companion.Type,
110     private val config: TurbulenceNoiseAnimationConfig,
111     private val paintCallback: PaintDrawCallback?,
112     private val renderEffectCallback: RenderEffectDrawCallback?,
113     private val animationStateChangedCallback: AnimationStateChangedCallback? = null
114 ) {
115     constructor(
116         baseType: TurbulenceNoiseShader.Companion.Type,
117         config: TurbulenceNoiseAnimationConfig,
118         paintCallback: PaintDrawCallback,
119         animationStateChangedCallback: AnimationStateChangedCallback? = null
120     ) : this(
121         baseType,
122         config,
123         paintCallback,
124         renderEffectCallback = null,
125         animationStateChangedCallback
126     )
127     constructor(
128         baseType: TurbulenceNoiseShader.Companion.Type,
129         config: TurbulenceNoiseAnimationConfig,
130         renderEffectCallback: RenderEffectDrawCallback,
131         animationStateChangedCallback: AnimationStateChangedCallback? = null
132     ) : this(
133         baseType,
134         config,
135         paintCallback = null,
136         renderEffectCallback,
137         animationStateChangedCallback
138     )
139 
140     private val turbulenceNoiseShader: TurbulenceNoiseShader =
141         TurbulenceNoiseShader(baseType).apply { applyConfig(config) }
142     private var currentAnimator: ValueAnimator? = null
143     private var state: AnimationState = AnimationState.NOT_PLAYING
144         set(value) {
145             if (field != value) {
146                 animationStateChangedCallback?.onStateChanged(field, value)
147                 field = value
148             }
149         }
150 
151     // We create a paint instance only if the client renders it with Paint.
152     private val paint =
153         if (paintCallback != null) {
154             Paint().apply { this.shader = turbulenceNoiseShader }
155         } else {
156             null
157         }
158 
159     /** Plays LoadingEffect. */
160     fun play() {
161         if (state != AnimationState.NOT_PLAYING) {
162             return // Ignore if any of the animation is playing.
163         }
164 
165         playEaseIn()
166     }
167 
168     // TODO(b/237282226): Support force finish.
169     /** Finishes the main animation, which triggers the ease-out animation. */
170     fun finish() {
171         if (state == AnimationState.MAIN) {
172             // Calling Animator#end sets the animation state back to the initial state. Using pause
173             // to avoid visual artifacts.
174             currentAnimator?.pause()
175             currentAnimator = null
176 
177             playEaseOut()
178         }
179     }
180 
181     /** Updates the noise color dynamically. */
182     fun updateColor(newColor: Int) {
183         turbulenceNoiseShader.setColor(newColor)
184     }
185 
186     /** Updates the noise color that's screen blended on top. */
187     fun updateScreenColor(newColor: Int) {
188         turbulenceNoiseShader.setScreenColor(newColor)
189     }
190 
191     /**
192      * Retrieves the noise offset x, y, z values. This is useful for replaying the animation
193      * smoothly from the last animation, by passing in the last values to the next animation.
194      */
195     fun getNoiseOffset(): Array<Float> {
196         return arrayOf(
197             turbulenceNoiseShader.noiseOffsetX,
198             turbulenceNoiseShader.noiseOffsetY,
199             turbulenceNoiseShader.noiseOffsetZ
200         )
201     }
202 
203     private fun playEaseIn() {
204         if (state != AnimationState.NOT_PLAYING) {
205             return
206         }
207         state = AnimationState.EASE_IN
208 
209         val animator = ValueAnimator.ofFloat(0f, 1f)
210         animator.duration = config.easeInDuration.toLong()
211 
212         // Animation should start from the initial position to avoid abrupt transition.
213         val initialX = turbulenceNoiseShader.noiseOffsetX
214         val initialY = turbulenceNoiseShader.noiseOffsetY
215         val initialZ = turbulenceNoiseShader.noiseOffsetZ
216 
217         animator.addUpdateListener { updateListener ->
218             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
219             val progress = updateListener.animatedValue as Float
220 
221             turbulenceNoiseShader.setNoiseMove(
222                 initialX + timeInSec * config.noiseMoveSpeedX,
223                 initialY + timeInSec * config.noiseMoveSpeedY,
224                 initialZ + timeInSec * config.noiseMoveSpeedZ
225             )
226 
227             // TODO: Replace it with a better curve.
228             turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)
229 
230             draw()
231         }
232 
233         animator.addListener(
234             object : AnimatorListenerAdapter() {
235                 override fun onAnimationEnd(animation: Animator) {
236                     currentAnimator = null
237                     playMain()
238                 }
239             }
240         )
241 
242         animator.start()
243         this.currentAnimator = animator
244     }
245 
246     private fun playMain() {
247         if (state != AnimationState.EASE_IN) {
248             return
249         }
250         state = AnimationState.MAIN
251 
252         val animator = ValueAnimator.ofFloat(0f, 1f)
253         animator.duration = config.maxDuration.toLong()
254 
255         // Animation should start from the initial position to avoid abrupt transition.
256         val initialX = turbulenceNoiseShader.noiseOffsetX
257         val initialY = turbulenceNoiseShader.noiseOffsetY
258         val initialZ = turbulenceNoiseShader.noiseOffsetZ
259 
260         turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
261 
262         animator.addUpdateListener { updateListener ->
263             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
264             turbulenceNoiseShader.setNoiseMove(
265                 initialX + timeInSec * config.noiseMoveSpeedX,
266                 initialY + timeInSec * config.noiseMoveSpeedY,
267                 initialZ + timeInSec * config.noiseMoveSpeedZ
268             )
269 
270             draw()
271         }
272 
273         animator.addListener(
274             object : AnimatorListenerAdapter() {
275                 override fun onAnimationEnd(animation: Animator) {
276                     currentAnimator = null
277                     playEaseOut()
278                 }
279             }
280         )
281 
282         animator.start()
283         this.currentAnimator = animator
284     }
285 
286     private fun playEaseOut() {
287         if (state != AnimationState.MAIN) {
288             return
289         }
290         state = AnimationState.EASE_OUT
291 
292         val animator = ValueAnimator.ofFloat(0f, 1f)
293         animator.duration = config.easeOutDuration.toLong()
294 
295         // Animation should start from the initial position to avoid abrupt transition.
296         val initialX = turbulenceNoiseShader.noiseOffsetX
297         val initialY = turbulenceNoiseShader.noiseOffsetY
298         val initialZ = turbulenceNoiseShader.noiseOffsetZ
299 
300         animator.addUpdateListener { updateListener ->
301             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
302             val progress = updateListener.animatedValue as Float
303 
304             turbulenceNoiseShader.setNoiseMove(
305                 initialX + timeInSec * config.noiseMoveSpeedX,
306                 initialY + timeInSec * config.noiseMoveSpeedY,
307                 initialZ + timeInSec * config.noiseMoveSpeedZ
308             )
309 
310             // TODO: Replace it with a better curve.
311             turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
312 
313             draw()
314         }
315 
316         animator.addListener(
317             object : AnimatorListenerAdapter() {
318                 override fun onAnimationEnd(animation: Animator) {
319                     currentAnimator = null
320                     state = AnimationState.NOT_PLAYING
321                 }
322             }
323         )
324 
325         animator.start()
326         this.currentAnimator = animator
327     }
328 
329     private fun draw() {
330         paintCallback?.onDraw(paint!!)
331         renderEffectCallback?.onDraw(
332             RenderEffect.createRuntimeShaderEffect(
333                 turbulenceNoiseShader,
334                 TurbulenceNoiseShader.BACKGROUND_UNIFORM
335             )
336         )
337     }
338 
339     /**
340      * States of the loading effect animation.
341      *
342      * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN],
343      * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't necessarily
344      * mean the acceleration and deceleration in the animation curve. They simply mean each stage of
345      * the animation. (i.e. Intro, core, and rest)
346      */
347     enum class AnimationState {
348         EASE_IN,
349         MAIN,
350         EASE_OUT,
351         NOT_PLAYING
352     }
353 
354     /** Optional callback that is triggered when the animation state changes. */
355     interface AnimationStateChangedCallback {
356         /**
357          * A callback that's triggered when the [AnimationState] changes. Example usage is
358          * performing a cleanup when [AnimationState] becomes [NOT_PLAYING].
359          */
360         fun onStateChanged(oldState: AnimationState, newState: AnimationState) {}
361     }
362 
363     private companion object {
364         private const val MS_TO_SEC = 0.001f
365     }
366 }
367