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.util.MathUtils.lerp 16 import android.view.View 17 import com.android.systemui.animation.Interpolators 18 import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold 19 import java.util.function.Consumer 20 21 /** 22 * Provides methods to modify the various properties of a [LightRevealScrim] to reveal between 0% to 23 * 100% of the view(s) underneath the scrim. 24 */ 25 interface LightRevealEffect { 26 fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) 27 28 companion object { 29 30 /** 31 * Returns the percent that the given value is past the threshold value. For example, 0.9 is 32 * 50% of the way past 0.8. 33 */ 34 fun getPercentPastThreshold(value: Float, threshold: Float): Float { 35 return (value - threshold).coerceAtLeast(0f) * (1f / (1f - threshold)) 36 } 37 } 38 } 39 40 /** 41 * Light reveal effect that shows light entering the phone from the bottom of the screen. The light 42 * enters from the bottom-middle as a narrow oval, and moves upward, eventually widening to fill the 43 * screen. 44 */ 45 object LiftReveal : LightRevealEffect { 46 47 /** Widen the oval of light after 35%, so it will eventually fill the screen. */ 48 private const val WIDEN_OVAL_THRESHOLD = 0.35f 49 50 /** After 85%, fade out the black color at the end of the gradient. */ 51 private const val FADE_END_COLOR_OUT_THRESHOLD = 0.85f 52 53 /** The initial width of the light oval, in percent of scrim width. */ 54 private const val OVAL_INITIAL_WIDTH_PERCENT = 0.5f 55 56 /** The initial top value of the light oval, in percent of scrim height. */ 57 private const val OVAL_INITIAL_TOP_PERCENT = 1.1f 58 59 /** The initial bottom value of the light oval, in percent of scrim height. */ 60 private const val OVAL_INITIAL_BOTTOM_PERCENT = 1.2f 61 62 /** Interpolator to use for the reveal amount. */ 63 private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE 64 65 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 66 val interpolatedAmount = INTERPOLATOR.getInterpolation(amount) 67 val ovalWidthIncreaseAmount = 68 getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD) 69 70 val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f 71 72 with(scrim) { 73 revealGradientEndColorAlpha = 1f - getPercentPastThreshold( 74 amount, FADE_END_COLOR_OUT_THRESHOLD) 75 setRevealGradientBounds( 76 scrim.width * initialWidthMultiplier + 77 -scrim.width * ovalWidthIncreaseAmount, 78 scrim.height * OVAL_INITIAL_TOP_PERCENT - 79 scrim.height * interpolatedAmount, 80 scrim.width * (1f - initialWidthMultiplier) + 81 scrim.width * ovalWidthIncreaseAmount, 82 scrim.height * OVAL_INITIAL_BOTTOM_PERCENT + 83 scrim.height * interpolatedAmount) 84 } 85 } 86 } 87 88 class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect { 89 90 private val INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN_REVERSE 91 92 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 93 val interpolatedAmount = INTERPOLATOR.getInterpolation(amount) 94 95 scrim.startColorAlpha = 96 getPercentPastThreshold(1 - interpolatedAmount, 97 threshold = 1 - START_COLOR_REVEAL_PERCENTAGE) 98 99 scrim.revealGradientEndColorAlpha = 100 1f - getPercentPastThreshold(interpolatedAmount, 101 threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE) 102 103 // Start changing gradient bounds later to avoid harsh gradient in the beginning 104 val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1.0f, interpolatedAmount) 105 106 if (isVertical) { 107 scrim.setRevealGradientBounds( 108 left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount, 109 top = 0f, 110 right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount, 111 bottom = scrim.height.toFloat() 112 ) 113 } else { 114 scrim.setRevealGradientBounds( 115 left = 0f, 116 top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount, 117 right = scrim.width.toFloat(), 118 bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount 119 ) 120 } 121 } 122 123 private companion object { 124 // From which percentage we should start the gradient reveal width 125 // E.g. if 0 - starts with 0px width, 0.3f - starts with 30% width 126 private const val GRADIENT_START_BOUNDS_PERCENTAGE = 0.3f 127 128 // When to start changing alpha color of the gradient scrim 129 // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely 130 // transparent at 100% 131 private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE = 0.6f 132 133 // When to finish displaying start color fill that reveals the content 134 // E.g. if 0.3f - the content won't be visible at 0% and it will gradually 135 // reduce the alpha until 30% (at this point the color fill is invisible) 136 private const val START_COLOR_REVEAL_PERCENTAGE = 0.3f 137 } 138 } 139 140 class CircleReveal( 141 /** X-value of the circle center of the reveal. */ 142 val centerX: Float, 143 /** Y-value of the circle center of the reveal. */ 144 val centerY: Float, 145 /** Radius of initial state of circle reveal */ 146 val startRadius: Float, 147 /** Radius of end state of circle reveal */ 148 val endRadius: Float 149 ) : LightRevealEffect { 150 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 151 // reveal amount updates already have an interpolator, so we intentionally use the 152 // non-interpolated amount 153 val fadeAmount = getPercentPastThreshold(amount, 0.5f) 154 val radius = startRadius + ((endRadius - startRadius) * amount) 155 scrim.revealGradientEndColorAlpha = 1f - fadeAmount 156 scrim.setRevealGradientBounds( 157 centerX - radius /* left */, 158 centerY - radius /* top */, 159 centerX + radius /* right */, 160 centerY + radius /* bottom */ 161 ) 162 } 163 } 164 165 class PowerButtonReveal( 166 /** Approximate Y-value of the center of the power button on the physical device. */ 167 val powerButtonY: Float 168 ) : LightRevealEffect { 169 170 /** 171 * How far off the side of the screen to start the power button reveal, in terms of percent of 172 * the screen width. This ensures that the initial part of the animation (where the reveal is 173 * just a sliver) starts just off screen. 174 */ 175 private val OFF_SCREEN_START_AMOUNT = 0.05f 176 177 private val WIDTH_INCREASE_MULTIPLIER = 1.25f 178 179 override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { 180 val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount) 181 val fadeAmount = getPercentPastThreshold(interpolatedAmount, 0.5f) 182 183 with(scrim) { 184 revealGradientEndColorAlpha = 1f - fadeAmount 185 setRevealGradientBounds( 186 width * (1f + OFF_SCREEN_START_AMOUNT) - 187 width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount, 188 powerButtonY - 189 height * interpolatedAmount, 190 width * (1f + OFF_SCREEN_START_AMOUNT) + 191 width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount, 192 powerButtonY + 193 height * interpolatedAmount) 194 } 195 } 196 } 197 198 /** 199 * Scrim view that partially reveals the content underneath it using a [RadialGradient] with a 200 * transparent center. The center position, size, and stops of the gradient can be manipulated to 201 * reveal views below the scrim as if they are being 'lit up'. 202 */ 203 class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) { 204 205 /** 206 * Listener that is called if the scrim's opaqueness changes 207 */ 208 lateinit var isScrimOpaqueChangedListener: Consumer<Boolean> 209 210 /** 211 * How much of the underlying views are revealed, in percent. 0 means they will be completely 212 * obscured and 1 means they'll be fully visible. 213 */ 214 var revealAmount: Float = 1f 215 set(value) { 216 if (field != value) { 217 field = value 218 219 revealEffect.setRevealAmountOnScrim(value, this) 220 updateScrimOpaque() 221 invalidate() 222 } 223 } 224 225 /** 226 * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount] 227 * changes. 228 */ 229 var revealEffect: LightRevealEffect = LiftReveal 230 set(value) { 231 if (field != value) { 232 field = value 233 234 revealEffect.setRevealAmountOnScrim(revealAmount, this) 235 invalidate() 236 } 237 } 238 239 var revealGradientCenter = PointF() 240 var revealGradientWidth: Float = 0f 241 var revealGradientHeight: Float = 0f 242 243 /** 244 * Alpha of the fill that can be used in the beginning of the animation to hide the content. 245 * Normally the gradient bounds are animated from small size so the content is not visible, 246 * but if the start gradient bounds allow to see some content this could be used to make the 247 * reveal smoother. It can help to add fade in effect in the beginning of the animation. 248 * The color of the fill is determined by [revealGradientEndColor]. 249 * 250 * 0 - no fill and content is visible, 1 - the content is covered with the start color 251 */ 252 var startColorAlpha = 0f 253 set(value) { 254 if (field != value) { 255 field = value 256 invalidate() 257 } 258 } 259 260 var revealGradientEndColor: Int = Color.BLACK 261 set(value) { 262 if (field != value) { 263 field = value 264 setPaintColorFilter() 265 } 266 } 267 268 var revealGradientEndColorAlpha = 0f 269 set(value) { 270 if (field != value) { 271 field = value 272 setPaintColorFilter() 273 } 274 } 275 276 /** 277 * Is the scrim currently fully opaque 278 */ 279 var isScrimOpaque = false 280 private set(value) { 281 if (field != value) { 282 field = value 283 isScrimOpaqueChangedListener.accept(field) 284 } 285 } 286 287 private fun updateScrimOpaque() { 288 isScrimOpaque = revealAmount == 0.0f && alpha == 1.0f && visibility == VISIBLE 289 } 290 291 override fun setAlpha(alpha: Float) { 292 super.setAlpha(alpha) 293 updateScrimOpaque() 294 } 295 296 override fun setVisibility(visibility: Int) { 297 super.setVisibility(visibility) 298 updateScrimOpaque() 299 } 300 301 /** 302 * Paint used to draw a transparent-to-white radial gradient. This will be scaled and translated 303 * via local matrix in [onDraw] so we never need to construct a new shader. 304 */ 305 private val gradientPaint = Paint().apply { 306 shader = RadialGradient( 307 0f, 0f, 1f, 308 intArrayOf(Color.TRANSPARENT, Color.WHITE), floatArrayOf(0f, 1f), 309 Shader.TileMode.CLAMP) 310 311 // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same 312 // window, rather than outright replacing them. 313 xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER) 314 } 315 316 /** 317 * Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to 318 * [revealGradientCenter] and set its size to [revealGradientWidth]/[revealGradientHeight], 319 * without needing to construct a new shader each time those properties change. 320 */ 321 private val shaderGradientMatrix = Matrix() 322 323 init { 324 revealEffect.setRevealAmountOnScrim(revealAmount, this) 325 setPaintColorFilter() 326 invalidate() 327 } 328 329 /** 330 * Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is 331 * simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and 332 * [revealGradientHeight] for you. 333 * 334 * This method does not call [invalidate] - you should do so once you're done changing 335 * properties. 336 */ 337 public fun setRevealGradientBounds(left: Float, top: Float, right: Float, bottom: Float) { 338 revealGradientWidth = right - left 339 revealGradientHeight = bottom - top 340 341 revealGradientCenter.x = left + (revealGradientWidth / 2f) 342 revealGradientCenter.y = top + (revealGradientHeight / 2f) 343 } 344 345 override fun onDraw(canvas: Canvas?) { 346 if (canvas == null || revealGradientWidth <= 0 || revealGradientHeight <= 0) { 347 if (revealAmount < 1f) { 348 canvas?.drawColor(revealGradientEndColor) 349 } 350 return 351 } 352 353 if (startColorAlpha > 0f) { 354 canvas.drawColor(updateColorAlpha(revealGradientEndColor, startColorAlpha)) 355 } 356 357 with(shaderGradientMatrix) { 358 setScale(revealGradientWidth, revealGradientHeight, 0f, 0f) 359 postTranslate(revealGradientCenter.x, revealGradientCenter.y) 360 361 gradientPaint.shader.setLocalMatrix(this) 362 } 363 364 // Draw the gradient over the screen, then multiply the end color by it. 365 canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint) 366 } 367 368 private fun setPaintColorFilter() { 369 gradientPaint.colorFilter = PorterDuffColorFilter( 370 updateColorAlpha(revealGradientEndColor, revealGradientEndColorAlpha), 371 PorterDuff.Mode.MULTIPLY) 372 } 373 374 private fun updateColorAlpha(color: Int, alpha: Float): Int = 375 Color.argb( 376 (alpha * 255).toInt(), 377 Color.red(color), 378 Color.green(color), 379 Color.blue(color) 380 ) 381 }