• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.R
40 import com.android.systemui.animation.Interpolators
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(alpha: Int) {
188         if (alpha == paint.alpha) {
189             return
190         }
191 
192         paint.alpha = alpha
193         invalidateSelf()
194     }
195 
196     /**
197      * Draws an animated ripple that expands fading away.
198      */
illuminatenull199     private fun illuminate() {
200         rippleData.alpha = 1f
201         invalidateSelf()
202 
203         rippleAnimation?.cancel()
204         rippleAnimation = AnimatorSet().apply {
205             playTogether(ValueAnimator.ofFloat(1f, 0f).apply {
206                 startDelay = 133
207                 duration = RIPPLE_ANIM_DURATION - startDelay
208                 interpolator = Interpolators.LINEAR_OUT_SLOW_IN
209                 addUpdateListener {
210                     rippleData.alpha = it.animatedValue as Float
211                     invalidateSelf()
212                 }
213             }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
214                 duration = RIPPLE_ANIM_DURATION
215                 interpolator = Interpolators.LINEAR_OUT_SLOW_IN
216                 addUpdateListener {
217                     rippleData.progress = it.animatedValue as Float
218                     invalidateSelf()
219                 }
220             })
221             addListener(object : AnimatorListenerAdapter() {
222                 override fun onAnimationEnd(animation: Animator?) {
223                     rippleData.progress = 0f
224                     rippleAnimation = null
225                     invalidateSelf()
226                 }
227             })
228             start()
229         }
230     }
231 
setHotspotnull232     override fun setHotspot(x: Float, y: Float) {
233         rippleData.x = x
234         rippleData.y = y
235         if (active) {
236             invalidateSelf()
237         }
238     }
239 
isStatefulnull240     override fun isStateful(): Boolean {
241         return true
242     }
243 
hasFocusStateSpecifiednull244     override fun hasFocusStateSpecified(): Boolean {
245         return true
246     }
247 
isProjectednull248     override fun isProjected(): Boolean {
249         return true
250     }
251 
getDirtyBoundsnull252     override fun getDirtyBounds(): Rect {
253         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
254         val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(),
255                 (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt())
256         bounds.union(super.getDirtyBounds())
257         return bounds
258     }
259 
onStateChangenull260     override fun onStateChange(stateSet: IntArray?): Boolean {
261         val changed = super.onStateChange(stateSet)
262         if (stateSet == null) {
263             return changed
264         }
265 
266         val wasPressed = pressed
267         var enabled = false
268         pressed = false
269         var focused = false
270         var hovered = false
271 
272         for (state in stateSet) {
273             when (state) {
274                 com.android.internal.R.attr.state_enabled -> {
275                     enabled = true
276                 }
277                 com.android.internal.R.attr.state_focused -> {
278                     focused = true
279                 }
280                 com.android.internal.R.attr.state_pressed -> {
281                     pressed = true
282                 }
283                 com.android.internal.R.attr.state_hovered -> {
284                     hovered = true
285                 }
286             }
287         }
288 
289         active = enabled && (pressed || focused || hovered)
290         if (wasPressed && !pressed) {
291             illuminate()
292         }
293 
294         return changed
295     }
296 }