1 /*
<lambda>null2  * Copyright (C) 2021 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.ripple
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Canvas
25 import android.graphics.Paint
26 import android.util.AttributeSet
27 import android.view.View
28 import androidx.core.graphics.ColorUtils
29 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape
30 
31 /**
32  * A generic expanding ripple effect.
33  *
34  * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
35  * then call [startRipple] to trigger the ripple expansion.
36  */
37 open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
38 
39     protected lateinit var rippleShader: RippleShader
40     lateinit var rippleShape: RippleShape
41         private set
42 
43     private val ripplePaint = Paint()
44     protected val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
45 
46     var duration: Long = 1750
47 
48     fun setMaxSize(maxWidth: Float, maxHeight: Float) {
49         rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight)
50     }
51 
52     private var centerX: Float = 0.0f
53     private var centerY: Float = 0.0f
54     fun setCenter(x: Float, y: Float) {
55         this.centerX = x
56         this.centerY = y
57         rippleShader.setCenter(x, y)
58     }
59 
60     override fun onConfigurationChanged(newConfig: Configuration?) {
61         rippleShader.pixelDensity = resources.displayMetrics.density
62         super.onConfigurationChanged(newConfig)
63     }
64 
65     override fun onAttachedToWindow() {
66         rippleShader.pixelDensity = resources.displayMetrics.density
67         super.onAttachedToWindow()
68     }
69 
70     /** Initializes the shader. Must be called before [startRipple]. */
71     fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
72         this.rippleShape = rippleShape
73         rippleShader = RippleShader(rippleShape)
74 
75         rippleShader.color = RippleShader.RIPPLE_DEFAULT_COLOR
76         rippleShader.rawProgress = 0f
77         rippleShader.sparkleStrength = RippleShader.RIPPLE_SPARKLE_STRENGTH
78         rippleShader.pixelDensity = resources.displayMetrics.density
79 
80         ripplePaint.shader = rippleShader
81     }
82 
83     /**
84      * Sets the fade parameters for the base ring.
85      *
86      * <p>Base ring indicates a blurred ring below the sparkle ring. See
87      * [RippleShader.baseRingFadeParams].
88      */
89     @JvmOverloads
90     fun setBaseRingFadeParams(
91         fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart,
92         fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd,
93         fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart,
94         fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd
95     ) {
96         setFadeParams(
97             rippleShader.baseRingFadeParams,
98             fadeInStart,
99             fadeInEnd,
100             fadeOutStart,
101             fadeOutEnd
102         )
103     }
104 
105     /**
106      * Sets the fade parameters for the sparkle ring.
107      *
108      * <p>Sparkle ring refers to the ring that's drawn on top of the base ring. See
109      * [RippleShader.sparkleRingFadeParams].
110      */
111     @JvmOverloads
112     fun setSparkleRingFadeParams(
113         fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart,
114         fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd,
115         fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart,
116         fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd
117     ) {
118         setFadeParams(
119             rippleShader.sparkleRingFadeParams,
120             fadeInStart,
121             fadeInEnd,
122             fadeOutStart,
123             fadeOutEnd
124         )
125     }
126 
127     /**
128      * Sets the fade parameters for the center fill.
129      *
130      * <p>One common use case is set all the params to 1, which completely removes the center fill.
131      * See [RippleShader.centerFillFadeParams].
132      */
133     @JvmOverloads
134     fun setCenterFillFadeParams(
135         fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart,
136         fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd,
137         fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart,
138         fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd
139     ) {
140         setFadeParams(
141             rippleShader.centerFillFadeParams,
142             fadeInStart,
143             fadeInEnd,
144             fadeOutStart,
145             fadeOutEnd
146         )
147     }
148 
149     private fun setFadeParams(
150         fadeParams: RippleShader.FadeParams,
151         fadeInStart: Float,
152         fadeInEnd: Float,
153         fadeOutStart: Float,
154         fadeOutEnd: Float
155     ) {
156         with(fadeParams) {
157             this.fadeInStart = fadeInStart
158             this.fadeInEnd = fadeInEnd
159             this.fadeOutStart = fadeOutStart
160             this.fadeOutEnd = fadeOutEnd
161         }
162     }
163 
164     /**
165      * Sets blur multiplier at start and end of the progress.
166      *
167      * <p>It interpolates between [start] and [end]. No need to set it if using default blur.
168      */
169     fun setBlur(start: Float, end: Float) {
170         rippleShader.blurStart = start
171         rippleShader.blurEnd = end
172     }
173 
174     /**
175      * Sets the list of [RippleShader.SizeAtProgress].
176      *
177      * <p>Note that this clears the list before it sets with the new data.
178      */
179     fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) {
180         rippleShader.rippleSize.setSizeAtProgresses(*targetSizes)
181     }
182 
183     @JvmOverloads
184     fun startRipple(onAnimationEnd: Runnable? = null) {
185         if (animator.isRunning) {
186             return // Ignore if ripple effect is already playing
187         }
188         animator.duration = duration
189         animator.addUpdateListener { updateListener ->
190             val now = updateListener.currentPlayTime
191             val progress = updateListener.animatedValue as Float
192             rippleShader.rawProgress = progress
193             rippleShader.distortionStrength = 1 - progress
194             rippleShader.time = now.toFloat()
195             invalidate()
196         }
197         animator.addListener(
198             object : AnimatorListenerAdapter() {
199                 override fun onAnimationEnd(animation: Animator) {
200                     onAnimationEnd?.run()
201                 }
202             }
203         )
204         animator.start()
205     }
206 
207     /**
208      * Set the color to be used for the ripple.
209      *
210      * The alpha value of the color will be applied to the ripple. The alpha range is [0-255].
211      */
212     fun setColor(color: Int, alpha: Int = RippleShader.RIPPLE_DEFAULT_ALPHA) {
213         rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
214     }
215 
216     /** Set the intensity of the sparkles. */
217     fun setSparkleStrength(strength: Float) {
218         rippleShader.sparkleStrength = strength
219     }
220 
221     /** Indicates whether the ripple animation is playing. */
222     fun rippleInProgress(): Boolean = animator.isRunning
223 
224     override fun onDraw(canvas: Canvas) {
225         if (!canvas.isHardwareAccelerated) {
226             // Drawing with the ripple shader requires hardware acceleration, so skip if it's
227             // unsupported.
228             return
229         }
230         // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
231         // active effect area. Values here should be kept in sync with the animation implementation
232         // in the ripple shader.
233         if (rippleShape == RippleShape.CIRCLE) {
234             val maskRadius = rippleShader.rippleSize.currentWidth
235             canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
236         } else if (rippleShape == RippleShape.ELLIPSE) {
237             val maskWidth = rippleShader.rippleSize.currentWidth * 2
238             val maskHeight = rippleShader.rippleSize.currentHeight * 2
239             canvas.drawRect(
240                 /* left= */ centerX - maskWidth,
241                 /* top= */ centerY - maskHeight,
242                 /* right= */ centerX + maskWidth,
243                 /* bottom= */ centerY + maskHeight,
244                 ripplePaint
245             )
246         } else { // RippleShape.RoundedBox
247             // No masking for the rounded box, as it has more blur which requires larger bounds.
248             // Masking creates sharp bounds even when the masking is 4 times bigger.
249             canvas.drawPaint(ripplePaint)
250         }
251     }
252 }
253