/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.surfaceeffects.loadingeffect import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.graphics.Paint import android.graphics.RenderEffect import android.view.View import com.android.systemui.surfaceeffects.PaintDrawCallback import com.android.systemui.surfaceeffects.RenderEffectDrawCallback import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader /** * Plays loading effect with the given configuration. * * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct * the [LoadingEffect] if the base type needs to be changed. * @param config immutable parameters that are used for drawing the effect. * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect * with [Canvas.drawPaint]. * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the * effect with [RenderEffect]. * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional. * * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to * render the effect: * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!) * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.) * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.) * *
First approach is more performant than other ones because [RenderEffect] forces an * intermediate render pass of the View to a texture to feed into it. * *
If going with the first approach, your custom [View] would look like as follow: *
{@code
* private var paint: Paint? = null
* // Override [View.onDraw].
* override fun onDraw(canvas: Canvas) {
* // RuntimeShader requires hardwareAcceleration.
* if (!canvas.isHardwareAccelerated) return
*
* paint?.let { canvas.drawPaint(it) }
* }
*
* // This is called [Callback.onDraw]
* fun draw(paint: Paint) {
* this.paint = paint
*
* // Must call invalidate to trigger View#onDraw
* invalidate()
* }
* }
*
* If going with the second approach, it doesn't require an extra custom [View], and it is as * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the * effect with other [RenderEffect]. * *
Third approach is an option, but it's more of a boilerplate so you would like to stick with * the second option. If you want to go with this option for some reason, below is the example: *
{@code
* // Initialize the shader and paint to use to pass into the [Canvas].
* private val renderNode = RenderNode("LoadingEffect")
*
* // Override [View.onDraw].
* override fun onDraw(canvas: Canvas) {
* // RuntimeShader requires hardwareAcceleration.
* if (!canvas.isHardwareAccelerated) return
*
* if (renderNode.hasDisplayList()) {
* canvas.drawRenderNode(renderNode)
* }
* }
*
* // This is called [Callback.onDraw]
* fun draw(renderEffect: RenderEffect) {
* renderNode.setPosition(0, 0, width, height)
* renderNode.setRenderEffect(renderEffect)
*
* val recordingCanvas = renderNode.beginRecording()
* // We need at least 1 drawing instruction.
* recordingCanvas.drawColor(Color.TRANSPARENT)
* renderNode.endRecording()
*
* // Must call invalidate to trigger View#onDraw
* invalidate()
* }
* }
*/
class LoadingEffect
private constructor(
baseType: TurbulenceNoiseShader.Companion.Type,
private val config: TurbulenceNoiseAnimationConfig,
private val paintCallback: PaintDrawCallback?,
private val renderEffectCallback: RenderEffectDrawCallback?,
private val animationStateChangedCallback: AnimationStateChangedCallback? = null
) {
constructor(
baseType: TurbulenceNoiseShader.Companion.Type,
config: TurbulenceNoiseAnimationConfig,
paintCallback: PaintDrawCallback,
animationStateChangedCallback: AnimationStateChangedCallback? = null
) : this(
baseType,
config,
paintCallback,
renderEffectCallback = null,
animationStateChangedCallback
)
constructor(
baseType: TurbulenceNoiseShader.Companion.Type,
config: TurbulenceNoiseAnimationConfig,
renderEffectCallback: RenderEffectDrawCallback,
animationStateChangedCallback: AnimationStateChangedCallback? = null
) : this(
baseType,
config,
paintCallback = null,
renderEffectCallback,
animationStateChangedCallback
)
private val turbulenceNoiseShader: TurbulenceNoiseShader =
TurbulenceNoiseShader(baseType).apply { applyConfig(config) }
private var currentAnimator: ValueAnimator? = null
private var state: AnimationState = AnimationState.NOT_PLAYING
set(value) {
if (field != value) {
animationStateChangedCallback?.onStateChanged(field, value)
field = value
}
}
// We create a paint instance only if the client renders it with Paint.
private val paint =
if (paintCallback != null) {
Paint().apply { this.shader = turbulenceNoiseShader }
} else {
null
}
/** Plays LoadingEffect. */
fun play() {
if (state != AnimationState.NOT_PLAYING) {
return // Ignore if any of the animation is playing.
}
playEaseIn()
}
// TODO(b/237282226): Support force finish.
/** Finishes the main animation, which triggers the ease-out animation. */
fun finish() {
if (state == AnimationState.MAIN) {
// Calling Animator#end sets the animation state back to the initial state. Using pause
// to avoid visual artifacts.
currentAnimator?.pause()
currentAnimator = null
playEaseOut()
}
}
/** Updates the noise color dynamically. */
fun updateColor(newColor: Int) {
turbulenceNoiseShader.setColor(newColor)
}
/** Updates the noise color that's screen blended on top. */
fun updateScreenColor(newColor: Int) {
turbulenceNoiseShader.setScreenColor(newColor)
}
/**
* Retrieves the noise offset x, y, z values. This is useful for replaying the animation
* smoothly from the last animation, by passing in the last values to the next animation.
*/
fun getNoiseOffset(): ArrayThe state is designed to be follow the order below: [AnimationState.EASE_IN], * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't necessarily * mean the acceleration and deceleration in the animation curve. They simply mean each stage of * the animation. (i.e. Intro, core, and rest) */ enum class AnimationState { EASE_IN, MAIN, EASE_OUT, NOT_PLAYING } /** Optional callback that is triggered when the animation state changes. */ interface AnimationStateChangedCallback { /** * A callback that's triggered when the [AnimationState] changes. Example usage is * performing a cleanup when [AnimationState] becomes [NOT_PLAYING]. */ fun onStateChanged(oldState: AnimationState, newState: AnimationState) {} } private companion object { private const val MS_TO_SEC = 0.001f } }