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