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
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.internal.graphics.ColorUtils
39 import com.android.systemui.Interpolators
40 import com.android.systemui.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 /**
59  * Drawable that can draw an animated gradient when tapped.
60  */
61 @Keep
62 class LightSourceDrawable : Drawable() {
63 
64     private var pressed = false
65     private var themeAttrs: IntArray? = null
66     private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f)
67     private var paint = Paint()
68 
69     var highlightColor = Color.WHITE
70     set(value) {
71         if (field == value) {
72             return
73         }
74         field = value
75         invalidateSelf()
76     }
77 
78     /**
79      * Draw a small highlight under the finger before expanding (or cancelling) it.
80      */
81     private var active: Boolean = false
82         set(value) {
83             if (value == field) {
84                 return
85             }
86             field = value
87 
88             if (value) {
89                 rippleAnimation?.cancel()
90                 rippleData.alpha = 1f
91                 rippleData.progress = RIPPLE_DOWN_PROGRESS
92             } else {
93                 rippleAnimation?.cancel()
<lambda>null94                 rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
95                     duration = RIPPLE_CANCEL_DURATION
96                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN
97                     addUpdateListener {
98                         rippleData.alpha = it.animatedValue as Float
99                         invalidateSelf()
100                     }
101                     addListener(object : AnimatorListenerAdapter() {
102                         var cancelled = false
103                         override fun onAnimationCancel(animation: Animator?) {
104                             cancelled = true
105                         }
106 
107                         override fun onAnimationEnd(animation: Animator?) {
108                             if (cancelled) {
109                                 return
110                             }
111                             rippleData.progress = 0f
112                             rippleData.alpha = 0f
113                             rippleAnimation = null
114                             invalidateSelf()
115                         }
116                     })
117                     start()
118                 }
119             }
120             invalidateSelf()
121         }
122 
123     private var rippleAnimation: Animator? = null
124 
125     /**
126      * Draw background and gradient.
127      */
drawnull128     override fun draw(canvas: Canvas) {
129         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
130         val centerColor =
131                 ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
132         paint.shader = RadialGradient(rippleData.x, rippleData.y, radius,
133                 intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP)
134         canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
135     }
136 
getOutlinenull137     override fun getOutline(outline: Outline) {
138         // No bounds, parent will clip it
139     }
140 
getOpacitynull141     override fun getOpacity(): Int {
142         return PixelFormat.TRANSPARENT
143     }
144 
inflatenull145     override fun inflate(
146         r: Resources,
147         parser: XmlPullParser,
148         attrs: AttributeSet,
149         theme: Resources.Theme?
150     ) {
151         val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
152         themeAttrs = a.extractThemeAttrs()
153         updateStateFromTypedArray(a)
154         a.recycle()
155     }
156 
updateStateFromTypedArraynull157     private fun updateStateFromTypedArray(a: TypedArray) {
158         if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) {
159             rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f)
160         }
161         if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) {
162             rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
163         }
164         if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
165             rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
166                     100f
167         }
168     }
169 
canApplyThemenull170     override fun canApplyTheme(): Boolean {
171         return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
172     }
173 
applyThemenull174     override fun applyTheme(t: Resources.Theme) {
175         super.applyTheme(t)
176         themeAttrs?.let {
177             val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
178             updateStateFromTypedArray(a)
179             a.recycle()
180         }
181     }
182 
setColorFilternull183     override fun setColorFilter(p0: ColorFilter?) {
184         throw UnsupportedOperationException("Color filters are not supported")
185     }
186 
setAlphanull187     override fun setAlpha(value: Int) {
188         throw UnsupportedOperationException("Alpha is not supported")
189     }
190 
191     /**
192      * Draws an animated ripple that expands fading away.
193      */
illuminatenull194     private fun illuminate() {
195         rippleData.alpha = 1f
196         invalidateSelf()
197 
198         rippleAnimation?.cancel()
199         rippleAnimation = AnimatorSet().apply {
200             playTogether(ValueAnimator.ofFloat(1f, 0f).apply {
201                 startDelay = 133
202                 duration = RIPPLE_ANIM_DURATION - startDelay
203                 interpolator = Interpolators.LINEAR_OUT_SLOW_IN
204                 addUpdateListener {
205                     rippleData.alpha = it.animatedValue as Float
206                     invalidateSelf()
207                 }
208             }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
209                 duration = RIPPLE_ANIM_DURATION
210                 interpolator = Interpolators.LINEAR_OUT_SLOW_IN
211                 addUpdateListener {
212                     rippleData.progress = it.animatedValue as Float
213                     invalidateSelf()
214                 }
215             })
216             addListener(object : AnimatorListenerAdapter() {
217                 override fun onAnimationEnd(animation: Animator?) {
218                     rippleData.progress = 0f
219                     rippleAnimation = null
220                     invalidateSelf()
221                 }
222             })
223             start()
224         }
225     }
226 
setHotspotnull227     override fun setHotspot(x: Float, y: Float) {
228         rippleData.x = x
229         rippleData.y = y
230         if (active) {
231             invalidateSelf()
232         }
233     }
234 
isStatefulnull235     override fun isStateful(): Boolean {
236         return true
237     }
238 
hasFocusStateSpecifiednull239     override fun hasFocusStateSpecified(): Boolean {
240         return true
241     }
242 
isProjectednull243     override fun isProjected(): Boolean {
244         return true
245     }
246 
getDirtyBoundsnull247     override fun getDirtyBounds(): Rect {
248         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
249         val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(),
250                 (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt())
251         bounds.union(super.getDirtyBounds())
252         return bounds
253     }
254 
onStateChangenull255     override fun onStateChange(stateSet: IntArray?): Boolean {
256         val changed = super.onStateChange(stateSet)
257         if (stateSet == null) {
258             return changed
259         }
260 
261         val wasPressed = pressed
262         var enabled = false
263         pressed = false
264         var focused = false
265         var hovered = false
266 
267         for (state in stateSet) {
268             when (state) {
269                 com.android.internal.R.attr.state_enabled -> {
270                     enabled = true
271                 }
272                 com.android.internal.R.attr.state_focused -> {
273                     focused = true
274                 }
275                 com.android.internal.R.attr.state_pressed -> {
276                     pressed = true
277                 }
278                 com.android.internal.R.attr.state_hovered -> {
279                     hovered = true
280                 }
281             }
282         }
283 
284         active = enabled && (pressed || focused || hovered)
285         if (wasPressed && !pressed) {
286             illuminate()
287         }
288 
289         return changed
290     }
291 }