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?.removeAllListeners() 115 fadeInAnimator?.removeAllUpdateListeners() 116 fadeInAnimator?.cancel() 117 timeAnimator?.cancel() 118 revealAnimator?.removeAllListeners() 119 revealAnimator?.removeAllUpdateListeners() 120 revealAnimator?.cancel() 121 122 fadeInAnimator = 123 ValueAnimator.ofFloat(transitionProgress, 1f).apply { 124 duration = FADE_IN_DURATION_MS 125 interpolator = Interpolators.STANDARD_DECELERATE 126 addUpdateListener { 127 transitionProgress = it.animatedValue as Float 128 loadingShader.setAlpha(transitionProgress) 129 // Match the timing with the fade animations. 130 blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX) 131 } 132 addListener( 133 object : AnimatorListenerAdapter() { 134 override fun onAnimationEnd(animation: Animator) { 135 animationState = AnimationState.FADE_IN_PLAYED 136 } 137 } 138 ) 139 start() 140 } 141 142 // Keep clouds moving until we finish loading 143 timeAnimator = 144 TimeAnimator().apply { 145 setTimeListener { _, totalTime, deltaTime -> flushUniforms(totalTime, deltaTime) } 146 start() 147 } 148 } 149 150 fun playRevealAnimation() { 151 when (revealType) { 152 RevealType.CIRCULAR -> playCircularRevealAnimation() 153 RevealType.FADE -> playFadeRevealAnimation() 154 } 155 } 156 157 private fun playCircularRevealAnimation() { 158 if ( 159 animationState == AnimationState.REVEAL_PLAYING || 160 animationState == AnimationState.REVEAL_PLAYED || 161 animationState == AnimationState.FADE_OUT_PLAYING || 162 animationState == AnimationState.FADE_OUT_PLAYED 163 ) 164 return 165 166 if (animationState == AnimationState.FADE_IN_PLAYING) { 167 fadeInAnimator?.removeAllListeners() 168 fadeInAnimator?.removeAllUpdateListeners() 169 fadeInAnimator?.cancel() 170 } 171 172 animationState = AnimationState.REVEAL_PLAYING 173 174 revealOverlay.visibility = View.VISIBLE 175 176 revealShader.setCenter(revealOverlay.width * 0.5f, revealOverlay.height * 0.5f) 177 178 revealAnimator?.cancel() 179 revealAnimator = 180 ValueAnimator.ofFloat(0f, 1f).apply { 181 duration = REVEAL_DURATION_MS 182 interpolator = Interpolators.STANDARD 183 184 addUpdateListener { 185 val progress = it.animatedValue as Float 186 187 // Draw a circle slightly larger than the screen. Need some offset due to large 188 // blur radius. 189 revealShader.setRadius( 190 progress * max(revealOverlay.width, revealOverlay.height) * 2f 191 ) 192 // Map [0,1] to [MAX, MIN]. 193 val blurAmount = 194 (1f - progress) * (MAX_REVEAL_BLUR_AMOUNT - MIN_REVEAL_BLUR_AMOUNT) + 195 MIN_REVEAL_BLUR_AMOUNT 196 revealShader.setBlur(blurAmount) 197 } 198 199 addListener( 200 object : AnimatorListenerAdapter() { 201 override fun onAnimationEnd(animation: Animator) { 202 resetCircularRevealAnimation() 203 } 204 } 205 ) 206 207 start() 208 } 209 } 210 211 private fun resetCircularRevealAnimation() { 212 animationState = AnimationState.REVEAL_PLAYED 213 214 revealOverlay.setRenderEffect(null) 215 revealOverlay.visibility = View.INVISIBLE 216 217 // Stop turbulence and reset everything. 218 timeAnimator?.cancel() 219 blurRadius = MIN_BLUR_PX 220 transitionProgress = 0f 221 } 222 223 private fun playFadeRevealAnimation() { 224 if ( 225 animationState == AnimationState.REVEAL_PLAYING || 226 animationState == AnimationState.REVEAL_PLAYED || 227 animationState == AnimationState.FADE_OUT_PLAYING || 228 animationState == AnimationState.FADE_OUT_PLAYED 229 ) 230 return 231 232 if (animationState == AnimationState.FADE_IN_PLAYING) { 233 fadeInAnimator?.removeAllListeners() 234 fadeInAnimator?.removeAllUpdateListeners() 235 fadeInAnimator?.cancel() 236 } 237 238 animationState = AnimationState.FADE_OUT_PLAYING 239 240 revealOverlay.visibility = View.VISIBLE 241 242 fadeInAnimator = 243 ValueAnimator.ofFloat(transitionProgress, 0f).apply { 244 duration = FADE_OUT_DURATION_MS 245 interpolator = Interpolators.STANDARD_DECELERATE 246 addUpdateListener { 247 transitionProgress = it.animatedValue as Float 248 loadingShader.setAlpha(transitionProgress) 249 // Match the timing with the fade animations. 250 blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX) 251 } 252 addListener( 253 object : AnimatorListenerAdapter() { 254 override fun onAnimationEnd(animation: Animator) { 255 resetFadeRevealAnimation() 256 } 257 } 258 ) 259 start() 260 } 261 } 262 263 private fun resetFadeRevealAnimation() { 264 animationState = AnimationState.FADE_OUT_PLAYED 265 266 revealOverlay.setRenderEffect(null) 267 268 // Stop turbulence and reset everything. 269 timeAnimator?.cancel() 270 blurRadius = MIN_BLUR_PX 271 transitionProgress = 0f 272 } 273 274 fun updateColor(colorScheme: ColorScheme) { 275 colorTurbulenceNoiseShader.apply { 276 setColor(colorScheme.accent1.s600) 277 setBackgroundColor(colorScheme.accent1.s900) 278 } 279 280 sparkleShader.setColor(colorScheme.accent1.s600) 281 loadingShader.setScreenColor(colorScheme.accent1.s900) 282 } 283 284 private fun flushUniforms(totalTime: Long, deltaTime: Long) { 285 elapsedTime += deltaTime 286 val time = elapsedTime / 1000f 287 val viewWidth = revealOverlay.width.toFloat() 288 val viewHeight = revealOverlay.height.toFloat() 289 290 colorTurbulenceNoiseShader.apply { 291 setSize(viewWidth, viewHeight) 292 setNoiseMove(time * NOISE_SPEED, 0f, time * NOISE_SPEED) 293 } 294 295 sparkleShader.apply { 296 setSize(viewWidth, viewHeight) 297 setNoiseMove(time * NOISE_SPEED, 0f, time * NOISE_SPEED) 298 setTime(time) 299 } 300 301 loadingShader.apply { 302 setSparkle(sparkleShader) 303 setColorTurbulenceMask(colorTurbulenceNoiseShader) 304 } 305 306 val renderEffect = RenderEffect.createRuntimeShaderEffect(loadingShader, "in_background") 307 308 // Update the blur effect only when loading animation is playing. 309 if ( 310 animationState == AnimationState.FADE_IN_PLAYING || 311 animationState == AnimationState.FADE_OUT_PLAYING 312 ) { 313 blurEffect = 314 RenderEffect.createBlurEffect( 315 blurRadius * pixelDensity, 316 blurRadius * pixelDensity, 317 Shader.TileMode.MIRROR 318 ) 319 } 320 321 // Animation time out 322 if ( 323 timeOutDuration != null && 324 totalTime > timeOutDuration && 325 animationState == AnimationState.FADE_IN_PLAYED 326 ) { 327 playRevealAnimation() 328 } 329 330 if (animationState == AnimationState.REVEAL_PLAYING) { 331 revealOverlay.setRenderEffect( 332 RenderEffect.createChainEffect( 333 RenderEffect.createRuntimeShaderEffect(revealShader, "in_src"), 334 RenderEffect.createChainEffect(renderEffect, blurEffect) 335 ) 336 ) 337 } else { 338 revealOverlay.setRenderEffect(RenderEffect.createChainEffect(renderEffect, blurEffect)) 339 } 340 } 341 342 /** Cancels the animation. Unlike end() , cancel() causes the animation to stop in its tracks */ 343 fun cancel() { 344 fadeInAnimator?.removeAllListeners() 345 fadeInAnimator?.removeAllUpdateListeners() 346 fadeInAnimator?.cancel() 347 timeAnimator?.cancel() 348 revealAnimator?.removeAllListeners() 349 revealAnimator?.removeAllUpdateListeners() 350 revealAnimator?.cancel() 351 } 352 353 /** Ends the animation, and causes the animation to skip to the end state */ 354 fun end() { 355 fadeInAnimator?.removeAllListeners() 356 fadeInAnimator?.removeAllUpdateListeners() 357 fadeInAnimator?.end() 358 timeAnimator?.end() 359 revealAnimator?.removeAllListeners() 360 revealAnimator?.removeAllUpdateListeners() 361 revealAnimator?.end() 362 when (revealType) { 363 RevealType.CIRCULAR -> resetCircularRevealAnimation() 364 RevealType.FADE -> resetFadeRevealAnimation() 365 } 366 } 367 368 fun setupRevealAnimation(seed: Long? = null, revealTransitionProgress: Float? = null) { 369 cancel() 370 371 revealOverlay.visibility = View.VISIBLE 372 373 elapsedTime = seed ?: (0L..10000L).random() 374 transitionProgress = revealTransitionProgress ?: 1f 375 376 // Fast forward to state at the end of fade in animation 377 blurRadius = maxOf(MAX_BLUR_PX * transitionProgress, MIN_BLUR_PX) 378 blurEffect = 379 RenderEffect.createBlurEffect( 380 blurRadius * pixelDensity, 381 blurRadius * pixelDensity, 382 Shader.TileMode.MIRROR 383 ) 384 animationState = AnimationState.FADE_IN_PLAYED 385 loadingShader.setAlpha(transitionProgress) 386 387 // Keep clouds moving until we finish loading 388 timeAnimator = 389 TimeAnimator().apply { 390 setTimeListener { _, totalTime, deltaTime -> flushUniforms(totalTime, deltaTime) } 391 start() 392 } 393 } 394 395 fun getElapsedTime(): Long { 396 return elapsedTime 397 } 398 399 fun getTransitionProgress(): Float { 400 return transitionProgress 401 } 402 403 companion object { 404 private const val NOISE_SPEED = 0.2f 405 private const val NOISE_SIZE = 1.7f 406 private const val MAX_BLUR_PX = 80f 407 private const val MIN_BLUR_PX = 1f 408 private const val FADE_IN_DURATION_MS = 1100L 409 private const val FADE_OUT_DURATION_MS = 1500L 410 const val TIME_OUT_DURATION_MS = 10000L 411 private const val REVEAL_DURATION_MS = 3600L 412 private const val MIN_REVEAL_BLUR_AMOUNT = 1f 413 private const val MAX_REVEAL_BLUR_AMOUNT = 2.5f 414 } 415 416 enum class AnimationState { 417 IDLE, 418 FADE_IN_PLAYING, 419 FADE_IN_PLAYED, 420 FADE_OUT_PLAYING, 421 FADE_OUT_PLAYED, 422 REVEAL_PLAYING, 423 REVEAL_PLAYED 424 } 425 } 426