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.ValueAnimator
22 import android.content.res.ColorStateList
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.Xfermode
32 import android.graphics.drawable.Drawable
33 import android.util.AttributeSet
34 import android.util.MathUtils
35 import android.view.View
36 import androidx.annotation.Keep
37 import com.android.app.animation.Interpolators
38 import com.android.internal.graphics.ColorUtils
39 import com.android.internal.graphics.ColorUtils.blendARGB
40 import com.android.systemui.res.R
41 import org.xmlpull.v1.XmlPullParser
42 
43 private const val BACKGROUND_ANIM_DURATION = 370L
44 
45 /** Drawable that can draw an animated gradient when tapped. */
46 @Keep
47 class IlluminationDrawable : Drawable() {
48 
49     private var themeAttrs: IntArray? = null
50     private var cornerRadiusOverride = -1f
51     var cornerRadius = 0f
52         get() {
53             return if (cornerRadiusOverride >= 0) {
54                 cornerRadiusOverride
55             } else {
56                 field
57             }
58         }
59     private var highlightColor = Color.TRANSPARENT
60     private var tmpHsl = floatArrayOf(0f, 0f, 0f)
61     private var paint = Paint()
62     private var highlight = 0f
63     private val lightSources = arrayListOf<LightSourceDrawable>()
64 
65     private var backgroundColor = Color.TRANSPARENT
66         set(value) {
67             if (value == field) {
68                 return
69             }
70             field = value
71             animateBackground()
72         }
73 
74     private var backgroundAnimation: ValueAnimator? = null
75 
76     /** Draw background and gradient. */
drawnull77     override fun draw(canvas: Canvas) {
78         canvas.drawRoundRect(
79             0f,
80             0f,
81             bounds.width().toFloat(),
82             bounds.height().toFloat(),
83             cornerRadius,
84             cornerRadius,
85             paint
86         )
87     }
88 
getOutlinenull89     override fun getOutline(outline: Outline) {
90         outline.setRoundRect(bounds, cornerRadius)
91     }
92 
getOpacitynull93     override fun getOpacity(): Int {
94         return PixelFormat.TRANSPARENT
95     }
96 
inflatenull97     override fun inflate(
98         r: Resources,
99         parser: XmlPullParser,
100         attrs: AttributeSet,
101         theme: Resources.Theme?
102     ) {
103         val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
104         themeAttrs = a.extractThemeAttrs()
105         updateStateFromTypedArray(a)
106         a.recycle()
107     }
108 
updateStateFromTypedArraynull109     private fun updateStateFromTypedArray(a: TypedArray) {
110         if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) {
111             cornerRadius =
112                 a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius)
113         }
114         if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
115             highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
116         }
117     }
118 
canApplyThemenull119     override fun canApplyTheme(): Boolean {
120         return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
121     }
122 
applyThemenull123     override fun applyTheme(t: Resources.Theme) {
124         super.applyTheme(t)
125         themeAttrs?.let {
126             val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
127             updateStateFromTypedArray(a)
128             a.recycle()
129         }
130     }
131 
setColorFilternull132     override fun setColorFilter(p0: ColorFilter?) {
133         throw UnsupportedOperationException("Color filters are not supported")
134     }
135 
setAlphanull136     override fun setAlpha(alpha: Int) {
137         if (alpha == paint.alpha) {
138             return
139         }
140 
141         paint.alpha = alpha
142         invalidateSelf()
143 
144         lightSources.forEach { it.alpha = alpha }
145     }
146 
getAlphanull147     override fun getAlpha(): Int {
148         return paint.alpha
149     }
150 
setXfermodenull151     override fun setXfermode(mode: Xfermode?) {
152         if (mode == paint.xfermode) {
153             return
154         }
155 
156         paint.xfermode = mode
157         invalidateSelf()
158     }
159 
160     /**
161      * Cross fade background.
162      *
163      * @see setTintList
164      * @see backgroundColor
165      */
animateBackgroundnull166     private fun animateBackground() {
167         ColorUtils.colorToHSL(backgroundColor, tmpHsl)
168         val L = tmpHsl[2]
169         tmpHsl[2] =
170             MathUtils.constrain(
171                 if (L < 1f - highlight) {
172                     L + highlight
173                 } else {
174                     L - highlight
175                 },
176                 0f,
177                 1f
178             )
179 
180         val initialBackground = paint.color
181         val initialHighlight = highlightColor
182         val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
183 
184         backgroundAnimation?.cancel()
185         backgroundAnimation =
186             ValueAnimator.ofFloat(0f, 1f).apply {
187                 duration = BACKGROUND_ANIM_DURATION
188                 interpolator = Interpolators.FAST_OUT_LINEAR_IN
189                 addUpdateListener {
190                     val progress = it.animatedValue as Float
191                     paint.color = blendARGB(initialBackground, backgroundColor, progress)
192                     highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
193                     lightSources.forEach { it.highlightColor = highlightColor }
194                     invalidateSelf()
195                 }
196                 addListener(
197                     object : AnimatorListenerAdapter() {
198                         override fun onAnimationEnd(animation: Animator) {
199                             backgroundAnimation = null
200                         }
201                     }
202                 )
203                 start()
204             }
205     }
206 
setTintListnull207     override fun setTintList(tint: ColorStateList?) {
208         super.setTintList(tint)
209         backgroundColor = tint!!.defaultColor
210     }
211 
registerLightSourcenull212     fun registerLightSource(lightSource: View) {
213         if (lightSource.background is LightSourceDrawable) {
214             registerLightSource(lightSource.background as LightSourceDrawable)
215         } else if (lightSource.foreground is LightSourceDrawable) {
216             registerLightSource(lightSource.foreground as LightSourceDrawable)
217         }
218     }
219 
registerLightSourcenull220     private fun registerLightSource(lightSource: LightSourceDrawable) {
221         lightSource.alpha = paint.alpha
222         lightSources.add(lightSource)
223     }
224 
225     /** Set or remove the corner radius override. This is typically set during animations. */
setCornerRadiusOverridenull226     fun setCornerRadiusOverride(cornerRadius: Float?) {
227         cornerRadiusOverride = cornerRadius ?: -1f
228     }
229 }
230