• 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.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 }