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 }