1 package com.android.systemui.statusbar 2 3 import android.content.Context 4 import android.graphics.Canvas 5 import android.graphics.Color 6 import android.graphics.Matrix 7 import android.graphics.Paint 8 import android.graphics.PointF 9 import android.graphics.PorterDuff 10 import android.graphics.PorterDuffColorFilter 11 import android.graphics.PorterDuffXfermode 12 import android.graphics.RadialGradient 13 import android.graphics.Shader 14 import android.util.AttributeSet 15 import android.view.View 16 import com.android.systemui.animation.Interpolators 17 import java.util.function.Consumer 18 19 /** 20 * Provides methods to modify the various properties of a [LightRevealScrim] to reveal between 0% to 21 * 100% of the view(s) underneath the scrim. 22 */ 23 interface LightRevealEffect { setRevealAmountOnScrimnull24 fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) 25 26 companion object { 27 28 /** 29 * Returns the percent that the given value is past the threshold value. For example, 0.9 is 30 * 50% of the way past 0.8. 31 */ 32 fun getPercentPastThreshold(value: Float, threshold: Float): Float { 33 return (value - threshold).coerceAtLeast(0f) * (1f / (1f - threshold)) 34 } 35 } 36 } 37 38 /** 39 * Light reveal effect that shows light entering the phone from the bottom of the screen. The light 40 * enters from the bottom-middle as a narrow oval, and moves upward, eventually widening to fill the 41 * screen. 42 */ 43 object LiftReveal : LightRevealEffect { 44 45 /** Widen the oval of light after 35%, so it will eventually fill the screen. */ 46 private const val WIDEN_OVAL_THRESHOLD = 0.35f 47 48 /** After 85%, fade out the black color at the end of the gradient. */ 49 private const val FADE_END_COLOR_OUT_THRESHOLD = 0.85f 50 51 /** The initial width of the light oval, in percent of scrim width. */ 52 private const val OVAL_INITIAL_WIDTH_PERCENT = 0.5f 53 54 /** The initial top value of the light oval, in percent of scrim height. */ 55 private const val OVAL_INITIAL_TOP_PERCENT = 1.1f 56 57 /** The initial bottom value of the light oval, in percent of scrim height. */ 58 private const val OVAL_INITIAL_BOTTOM_PERCENT = 1.2f 59 60 /** Interpolator to use for the reveal amount. */ 61 private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE 62 setRevealAmountOnScrimnull63 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 64 val interpolatedAmount = INTERPOLATOR.getInterpolation(amount) 65 val ovalWidthIncreaseAmount = 66 LightRevealEffect.getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD) 67 68 val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f 69 70 with(scrim) { 71 revealGradientEndColorAlpha = 1f - LightRevealEffect.getPercentPastThreshold( 72 amount, FADE_END_COLOR_OUT_THRESHOLD) 73 setRevealGradientBounds( 74 scrim.width * initialWidthMultiplier + 75 -scrim.width * ovalWidthIncreaseAmount, 76 scrim.height * OVAL_INITIAL_TOP_PERCENT - 77 scrim.height * interpolatedAmount, 78 scrim.width * (1f - initialWidthMultiplier) + 79 scrim.width * ovalWidthIncreaseAmount, 80 scrim.height * OVAL_INITIAL_BOTTOM_PERCENT + 81 scrim.height * interpolatedAmount) 82 } 83 } 84 } 85 86 class CircleReveal( 87 /** X-value of the circle center of the reveal. */ 88 val centerX: Float, 89 /** Y-value of the circle center of the reveal. */ 90 val centerY: Float, 91 /** Radius of initial state of circle reveal */ 92 val startRadius: Float, 93 /** Radius of end state of circle reveal */ 94 val endRadius: Float 95 ) : LightRevealEffect { setRevealAmountOnScrimnull96 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 97 // reveal amount updates already have an interpolator, so we intentionally use the 98 // non-interpolated amount 99 val fadeAmount = LightRevealEffect.getPercentPastThreshold(amount, 0.5f) 100 val radius = startRadius + ((endRadius - startRadius) * amount) 101 scrim.revealGradientEndColorAlpha = 1f - fadeAmount 102 scrim.setRevealGradientBounds( 103 centerX - radius /* left */, 104 centerY - radius /* top */, 105 centerX + radius /* right */, 106 centerY + radius /* bottom */ 107 ) 108 } 109 } 110 111 class PowerButtonReveal( 112 /** Approximate Y-value of the center of the power button on the physical device. */ 113 val powerButtonY: Float 114 ) : LightRevealEffect { 115 116 /** 117 * How far off the side of the screen to start the power button reveal, in terms of percent of 118 * the screen width. This ensures that the initial part of the animation (where the reveal is 119 * just a sliver) starts just off screen. 120 */ 121 private val OFF_SCREEN_START_AMOUNT = 0.05f 122 123 private val WIDTH_INCREASE_MULTIPLIER = 1.25f 124 setRevealAmountOnScrimnull125 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 126 val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount) 127 val fadeAmount = 128 LightRevealEffect.getPercentPastThreshold(interpolatedAmount, 0.5f) 129 130 with(scrim) { 131 revealGradientEndColorAlpha = 1f - fadeAmount 132 setRevealGradientBounds( 133 width * (1f + OFF_SCREEN_START_AMOUNT) - 134 width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount, 135 powerButtonY - 136 height * interpolatedAmount, 137 width * (1f + OFF_SCREEN_START_AMOUNT) + 138 width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount, 139 powerButtonY + 140 height * interpolatedAmount) 141 } 142 } 143 } 144 145 /** 146 * Scrim view that partially reveals the content underneath it using a [RadialGradient] with a 147 * transparent center. The center position, size, and stops of the gradient can be manipulated to 148 * reveal views below the scrim as if they are being 'lit up'. 149 */ 150 class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) { 151 152 /** 153 * Listener that is called if the scrim's opaqueness changes 154 */ 155 lateinit var isScrimOpaqueChangedListener: Consumer<Boolean> 156 157 /** 158 * How much of the underlying views are revealed, in percent. 0 means they will be completely 159 * obscured and 1 means they'll be fully visible. 160 */ 161 var revealAmount: Float = 1f 162 set(value) { 163 if (field != value) { 164 field = value 165 166 revealEffect.setRevealAmountOnScrim(value, this) 167 updateScrimOpaque() 168 invalidate() 169 } 170 } 171 172 /** 173 * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount] 174 * changes. 175 */ 176 var revealEffect: LightRevealEffect = LiftReveal 177 set(value) { 178 if (field != value) { 179 field = value 180 181 revealEffect.setRevealAmountOnScrim(revealAmount, this) 182 invalidate() 183 } 184 } 185 186 var revealGradientCenter = PointF() 187 var revealGradientWidth: Float = 0f 188 var revealGradientHeight: Float = 0f 189 190 var revealGradientEndColor: Int = Color.BLACK 191 set(value) { 192 if (field != value) { 193 field = value 194 setPaintColorFilter() 195 } 196 } 197 198 var revealGradientEndColorAlpha = 0f 199 set(value) { 200 if (field != value) { 201 field = value 202 setPaintColorFilter() 203 } 204 } 205 206 /** 207 * Is the scrim currently fully opaque 208 */ 209 var isScrimOpaque = false 210 private set(value) { 211 if (field != value) { 212 field = value 213 isScrimOpaqueChangedListener.accept(field) 214 } 215 } 216 updateScrimOpaquenull217 private fun updateScrimOpaque() { 218 isScrimOpaque = revealAmount == 0.0f && alpha == 1.0f && visibility == VISIBLE 219 } 220 setAlphanull221 override fun setAlpha(alpha: Float) { 222 super.setAlpha(alpha) 223 updateScrimOpaque() 224 } 225 setVisibilitynull226 override fun setVisibility(visibility: Int) { 227 super.setVisibility(visibility) 228 updateScrimOpaque() 229 } 230 231 /** 232 * Paint used to draw a transparent-to-white radial gradient. This will be scaled and translated 233 * via local matrix in [onDraw] so we never need to construct a new shader. 234 */ <lambda>null235 private val gradientPaint = Paint().apply { 236 shader = RadialGradient( 237 0f, 0f, 1f, 238 intArrayOf(Color.TRANSPARENT, Color.WHITE), floatArrayOf(0f, 1f), 239 Shader.TileMode.CLAMP) 240 241 // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same 242 // window, rather than outright replacing them. 243 xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) 244 } 245 246 /** 247 * Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to 248 * [revealGradientCenter] and set its size to [revealGradientWidth]/[revealGradientHeight], 249 * without needing to construct a new shader each time those properties change. 250 */ 251 private val shaderGradientMatrix = Matrix() 252 253 init { 254 revealEffect.setRevealAmountOnScrim(revealAmount, this) 255 setPaintColorFilter() 256 invalidate() 257 } 258 259 /** 260 * Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is 261 * simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and 262 * [revealGradientHeight] for you. 263 * 264 * This method does not call [invalidate] - you should do so once you're done changing 265 * properties. 266 */ setRevealGradientBoundsnull267 public fun setRevealGradientBounds(left: Float, top: Float, right: Float, bottom: Float) { 268 revealGradientWidth = right - left 269 revealGradientHeight = bottom - top 270 271 revealGradientCenter.x = left + (revealGradientWidth / 2f) 272 revealGradientCenter.y = top + (revealGradientHeight / 2f) 273 } 274 onDrawnull275 override fun onDraw(canvas: Canvas?) { 276 if (canvas == null || revealGradientWidth <= 0 || revealGradientHeight <= 0) { 277 if (revealAmount < 1f) { 278 canvas?.drawColor(revealGradientEndColor) 279 } 280 return 281 } 282 283 with(shaderGradientMatrix) { 284 setScale(revealGradientWidth, revealGradientHeight, 0f, 0f) 285 postTranslate(revealGradientCenter.x, revealGradientCenter.y) 286 287 gradientPaint.shader.setLocalMatrix(this) 288 } 289 290 // Draw the gradient over the screen, then multiply the end color by it. 291 canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint) 292 } 293 setPaintColorFilternull294 private fun setPaintColorFilter() { 295 gradientPaint.colorFilter = PorterDuffColorFilter( 296 Color.argb( 297 (revealGradientEndColorAlpha * 255).toInt(), 298 Color.red(revealGradientEndColor), 299 Color.green(revealGradientEndColor), 300 Color.blue(revealGradientEndColor)), 301 PorterDuff.Mode.MULTIPLY) 302 } 303 }