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