1 /*
2  * Copyright (C) 2020 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.media.controls.ui.drawable
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.ValueAnimator
23 import android.content.res.Resources
24 import android.content.res.TypedArray
25 import android.graphics.Canvas
26 import android.graphics.Color
27 import android.graphics.ColorFilter
28 import android.graphics.Outline
29 import android.graphics.Paint
30 import android.graphics.PixelFormat
31 import android.graphics.RadialGradient
32 import android.graphics.Rect
33 import android.graphics.Shader
34 import android.graphics.drawable.Drawable
35 import android.util.AttributeSet
36 import android.util.MathUtils.lerp
37 import androidx.annotation.Keep
38 import com.android.app.animation.Interpolators
39 import com.android.internal.graphics.ColorUtils
40 import com.android.systemui.res.R
41 import org.xmlpull.v1.XmlPullParser
42 
43 private const val RIPPLE_ANIM_DURATION = 800L
44 private const val RIPPLE_DOWN_PROGRESS = 0.05f
45 private const val RIPPLE_CANCEL_DURATION = 200L
46 private val GRADIENT_STOPS = floatArrayOf(0.2f, 1f)
47 
48 private data class RippleData(
49     var x: Float,
50     var y: Float,
51     var alpha: Float,
52     var progress: Float,
53     var minSize: Float,
54     var maxSize: Float,
55     var highlight: Float
56 )
57 
58 /** Drawable that can draw an animated gradient when tapped. */
59 @Keep
60 class LightSourceDrawable : Drawable() {
61 
62     private var pressed = false
63     private var themeAttrs: IntArray? = null
64     private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f)
65     private var paint = Paint()
66 
67     var highlightColor = Color.WHITE
68         set(value) {
69             if (field == value) {
70                 return
71             }
72             field = value
73             invalidateSelf()
74         }
75 
76     /** Draw a small highlight under the finger before expanding (or cancelling) it. */
77     private var active: Boolean = false
78         set(value) {
79             if (value == field) {
80                 return
81             }
82             field = value
83 
84             if (value) {
85                 rippleAnimation?.cancel()
86                 rippleData.alpha = 1f
87                 rippleData.progress = RIPPLE_DOWN_PROGRESS
88             } else {
89                 rippleAnimation?.cancel()
90                 rippleAnimation =
<lambda>null91                     ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
92                         duration = RIPPLE_CANCEL_DURATION
93                         interpolator = Interpolators.LINEAR_OUT_SLOW_IN
94                         addUpdateListener {
95                             rippleData.alpha = it.animatedValue as Float
96                             invalidateSelf()
97                         }
98                         addListener(
99                             object : AnimatorListenerAdapter() {
100                                 var cancelled = false
101                                 override fun onAnimationCancel(animation: Animator) {
102                                     cancelled = true
103                                 }
104 
105                                 override fun onAnimationEnd(animation: Animator) {
106                                     if (cancelled) {
107                                         return
108                                     }
109                                     rippleData.progress = 0f
110                                     rippleData.alpha = 0f
111                                     rippleAnimation = null
112                                     invalidateSelf()
113                                 }
114                             }
115                         )
116                         start()
117                     }
118             }
119             invalidateSelf()
120         }
121 
122     private var rippleAnimation: Animator? = null
123 
124     /** Draw background and gradient. */
drawnull125     override fun draw(canvas: Canvas) {
126         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
127         val centerColor =
128             ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
129         paint.shader =
130             RadialGradient(
131                 rippleData.x,
132                 rippleData.y,
133                 radius,
134                 intArrayOf(centerColor, Color.TRANSPARENT),
135                 GRADIENT_STOPS,
136                 Shader.TileMode.CLAMP
137             )
138         canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
139     }
140 
getOutlinenull141     override fun getOutline(outline: Outline) {
142         // No bounds, parent will clip it
143     }
144 
getOpacitynull145     override fun getOpacity(): Int {
146         return PixelFormat.TRANSPARENT
147     }
148 
inflatenull149     override fun inflate(
150         r: Resources,
151         parser: XmlPullParser,
152         attrs: AttributeSet,
153         theme: Resources.Theme?
154     ) {
155         val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
156         themeAttrs = a.extractThemeAttrs()
157         updateStateFromTypedArray(a)
158         a.recycle()
159     }
160 
updateStateFromTypedArraynull161     private fun updateStateFromTypedArray(a: TypedArray) {
162         if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) {
163             rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f)
164         }
165         if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) {
166             rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
167         }
168         if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
169             rippleData.highlight =
170                 a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
171         }
172     }
173 
canApplyThemenull174     override fun canApplyTheme(): Boolean {
175         return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
176     }
177 
applyThemenull178     override fun applyTheme(t: Resources.Theme) {
179         super.applyTheme(t)
180         themeAttrs?.let {
181             val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
182             updateStateFromTypedArray(a)
183             a.recycle()
184         }
185     }
186 
setColorFilternull187     override fun setColorFilter(p0: ColorFilter?) {
188         throw UnsupportedOperationException("Color filters are not supported")
189     }
190 
setAlphanull191     override fun setAlpha(alpha: Int) {
192         if (alpha == paint.alpha) {
193             return
194         }
195 
196         paint.alpha = alpha
197         invalidateSelf()
198     }
199 
200     /** Draws an animated ripple that expands fading away. */
illuminatenull201     private fun illuminate() {
202         rippleData.alpha = 1f
203         invalidateSelf()
204 
205         rippleAnimation?.cancel()
206         rippleAnimation =
207             AnimatorSet().apply {
208                 playTogether(
209                     ValueAnimator.ofFloat(1f, 0f).apply {
210                         startDelay = 133
211                         duration = RIPPLE_ANIM_DURATION - startDelay
212                         interpolator = Interpolators.LINEAR_OUT_SLOW_IN
213                         addUpdateListener {
214                             rippleData.alpha = it.animatedValue as Float
215                             invalidateSelf()
216                         }
217                     },
218                     ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
219                         duration = RIPPLE_ANIM_DURATION
220                         interpolator = Interpolators.LINEAR_OUT_SLOW_IN
221                         addUpdateListener {
222                             rippleData.progress = it.animatedValue as Float
223                             invalidateSelf()
224                         }
225                     }
226                 )
227                 addListener(
228                     object : AnimatorListenerAdapter() {
229                         override fun onAnimationEnd(animation: Animator) {
230                             rippleData.progress = 0f
231                             rippleAnimation = null
232                             invalidateSelf()
233                         }
234                     }
235                 )
236                 start()
237             }
238     }
239 
setHotspotnull240     override fun setHotspot(x: Float, y: Float) {
241         rippleData.x = x
242         rippleData.y = y
243         if (active) {
244             invalidateSelf()
245         }
246     }
247 
isStatefulnull248     override fun isStateful(): Boolean {
249         return true
250     }
251 
hasFocusStateSpecifiednull252     override fun hasFocusStateSpecified(): Boolean {
253         return true
254     }
255 
isProjectednull256     override fun isProjected(): Boolean {
257         return true
258     }
259 
getDirtyBoundsnull260     override fun getDirtyBounds(): Rect {
261         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
262         val bounds =
263             Rect(
264                 (rippleData.x - radius).toInt(),
265                 (rippleData.y - radius).toInt(),
266                 (rippleData.x + radius).toInt(),
267                 (rippleData.y + radius).toInt()
268             )
269         bounds.union(super.getDirtyBounds())
270         return bounds
271     }
272 
onStateChangenull273     override fun onStateChange(stateSet: IntArray): Boolean {
274         val changed = super.onStateChange(stateSet)
275 
276         val wasPressed = pressed
277         var enabled = false
278         pressed = false
279         var focused = false
280         var hovered = false
281 
282         for (state in stateSet) {
283             when (state) {
284                 com.android.internal.R.attr.state_enabled -> {
285                     enabled = true
286                 }
287                 com.android.internal.R.attr.state_focused -> {
288                     focused = true
289                 }
290                 com.android.internal.R.attr.state_pressed -> {
291                     pressed = true
292                 }
293                 com.android.internal.R.attr.state_hovered -> {
294                     hovered = true
295                 }
296             }
297         }
298 
299         active = enabled && (pressed || focused || hovered)
300         if (wasPressed && !pressed) {
301             illuminate()
302         }
303 
304         return changed
305     }
306 }
307