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 }