1 /*
2  * Copyright (C) 2022 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.systemui.surfaceeffects.turbulencenoise
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.ValueAnimator
21 import android.content.Context
22 import android.graphics.Canvas
23 import android.graphics.Paint
24 import android.util.AttributeSet
25 import android.view.View
26 import androidx.annotation.VisibleForTesting
27 import androidx.core.graphics.ColorUtils
28 
29 /**
30  * View that renders turbulence noise effect.
31  *
32  * <p>Use [TurbulenceNoiseController] to control the turbulence animation. If you want to make some
33  * other turbulence noise effects, either add functionality to [TurbulenceNoiseController] or create
34  * another controller instead of extend or modify the [TurbulenceNoiseView].
35  *
36  * <p>Please keep the [TurbulenceNoiseView] (or View in general) not aware of the state.
37  *
38  * <p>Please avoid inheriting the View if possible. Instead, reconsider adding a controller for a
39  * new case.
40  */
41 class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
42 
43     companion object {
44         private const val MS_TO_SEC = 0.001f
45     }
46 
47     private val turbulenceNoiseShader = TurbulenceNoiseShader()
48     private val paint = Paint().apply { this.shader = turbulenceNoiseShader }
49     @VisibleForTesting var noiseConfig: TurbulenceNoiseAnimationConfig? = null
50     @VisibleForTesting var currentAnimator: ValueAnimator? = null
51 
52     override fun onDraw(canvas: Canvas) {
53         if (!canvas.isHardwareAccelerated) {
54             // Drawing with the turbulence noise shader requires hardware acceleration, so skip
55             // if it's unsupported.
56             return
57         }
58 
59         canvas.drawPaint(paint)
60     }
61 
62     /** Updates the color during the animation. No-op if there's no animation playing. */
63     internal fun updateColor(color: Int) {
64         noiseConfig?.let {
65             turbulenceNoiseShader.setColor(ColorUtils.setAlphaComponent(color, it.opacity))
66         }
67     }
68 
69     /** Plays the turbulence noise with no easing. */
70     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
71     fun play(onAnimationEnd: Runnable? = null) {
72         if (noiseConfig == null) {
73             return
74         }
75         val config = noiseConfig!!
76 
77         val animator = ValueAnimator.ofFloat(0f, 1f)
78         animator.duration = config.maxDuration.toLong()
79 
80         // Animation should start from the initial position to avoid abrupt transition.
81         val initialX = turbulenceNoiseShader.noiseOffsetX
82         val initialY = turbulenceNoiseShader.noiseOffsetY
83         val initialZ = turbulenceNoiseShader.noiseOffsetZ
84 
85         animator.addUpdateListener { updateListener ->
86             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
87             turbulenceNoiseShader.setNoiseMove(
88                 initialX + timeInSec * config.noiseMoveSpeedX,
89                 initialY + timeInSec * config.noiseMoveSpeedY,
90                 initialZ + timeInSec * config.noiseMoveSpeedZ
91             )
92 
93             turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
94 
95             invalidate()
96         }
97 
98         animator.addListener(
99             object : AnimatorListenerAdapter() {
100                 override fun onAnimationEnd(animation: Animator) {
101                     currentAnimator = null
102                     onAnimationEnd?.run()
103                 }
104             }
105         )
106 
107         animator.start()
108         currentAnimator = animator
109     }
110 
111     /** Plays the turbulence noise with linear ease-in. */
112     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
113     fun playEaseIn(offsetX: Float = 0f, offsetY: Float = 0f, onAnimationEnd: Runnable? = null) {
114         if (noiseConfig == null) {
115             return
116         }
117         val config = noiseConfig!!
118 
119         val animator = ValueAnimator.ofFloat(0f, 1f)
120         animator.duration = config.easeInDuration.toLong()
121 
122         // Animation should start from the initial position to avoid abrupt transition.
123         val initialX = turbulenceNoiseShader.noiseOffsetX
124         val initialY = turbulenceNoiseShader.noiseOffsetY
125         val initialZ = turbulenceNoiseShader.noiseOffsetZ
126 
127         animator.addUpdateListener { updateListener ->
128             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
129             val progress = updateListener.animatedValue as Float
130 
131             turbulenceNoiseShader.setNoiseMove(
132                 offsetX + initialX + timeInSec * config.noiseMoveSpeedX,
133                 offsetY + initialY + timeInSec * config.noiseMoveSpeedY,
134                 initialZ + timeInSec * config.noiseMoveSpeedZ
135             )
136 
137             // TODO: Replace it with a better curve.
138             turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)
139 
140             invalidate()
141         }
142 
143         animator.addListener(
144             object : AnimatorListenerAdapter() {
145                 override fun onAnimationEnd(animation: Animator) {
146                     currentAnimator = null
147                     onAnimationEnd?.run()
148                 }
149             }
150         )
151 
152         animator.start()
153         currentAnimator = animator
154     }
155 
156     /** Plays the turbulence noise with linear ease-out. */
157     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
158     fun playEaseOut(onAnimationEnd: Runnable? = null) {
159         if (noiseConfig == null) {
160             return
161         }
162         val config = noiseConfig!!
163 
164         val animator = ValueAnimator.ofFloat(0f, 1f)
165         animator.duration = config.easeOutDuration.toLong()
166 
167         // Animation should start from the initial position to avoid abrupt transition.
168         val initialX = turbulenceNoiseShader.noiseOffsetX
169         val initialY = turbulenceNoiseShader.noiseOffsetY
170         val initialZ = turbulenceNoiseShader.noiseOffsetZ
171 
172         animator.addUpdateListener { updateListener ->
173             val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
174             val progress = updateListener.animatedValue as Float
175 
176             turbulenceNoiseShader.setNoiseMove(
177                 initialX + timeInSec * config.noiseMoveSpeedX,
178                 initialY + timeInSec * config.noiseMoveSpeedY,
179                 initialZ + timeInSec * config.noiseMoveSpeedZ
180             )
181 
182             // TODO: Replace it with a better curve.
183             turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
184 
185             invalidate()
186         }
187 
188         animator.addListener(
189             object : AnimatorListenerAdapter() {
190                 override fun onAnimationEnd(animation: Animator) {
191                     currentAnimator = null
192                     onAnimationEnd?.run()
193                 }
194             }
195         )
196 
197         animator.start()
198         currentAnimator = animator
199     }
200 
201     /** Finishes the current animation if playing and plays the next animation if given. */
202     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
203     fun finish(nextAnimation: Runnable? = null) {
204         // Calling Animator#end sets the animation state back to the initial state. Using pause to
205         // avoid visual artifacts.
206         currentAnimator?.pause()
207         currentAnimator = null
208 
209         nextAnimation?.run()
210     }
211 
212     /** Applies shader uniforms. Must be called before playing animation. */
213     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
214     fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
215         noiseConfig = config
216         with(turbulenceNoiseShader) {
217             setGridCount(config.gridCount)
218             setColor(config.color)
219             setBackgroundColor(config.backgroundColor)
220             setSize(config.width, config.height)
221             setPixelDensity(config.pixelDensity)
222             setInverseNoiseLuminosity(inverse = false)
223             setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness)
224         }
225         paint.blendMode = config.blendMode
226     }
227 
228     internal fun clearConfig() {
229         noiseConfig = null
230     }
231 }
232