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