• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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 package com.android.systemui.biometrics
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.graphics.Canvas
24 import android.graphics.Color
25 import android.graphics.Paint
26 import android.graphics.Point
27 import android.util.AttributeSet
28 import android.view.View
29 import android.view.animation.PathInterpolator
30 import com.android.internal.graphics.ColorUtils
31 import com.android.systemui.animation.Interpolators
32 import com.android.systemui.surfaceeffects.ripple.RippleShader
33 
34 private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
35 
36 /**
37  * Handles two ripple effects: dwell ripple and unlocked ripple
38  * Dwell Ripple:
39  *     - startDwellRipple: dwell ripple expands outwards around the biometric area
40  *     - retractDwellRipple: retracts the dwell ripple to radius 0 to signal a failure
41  *     - fadeDwellRipple: fades the dwell ripple away to alpha 0
42  * Unlocked ripple:
43  *     - startUnlockedRipple: ripple expands from biometric auth location to the edges of the screen
44  */
45 class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
46     private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f)
47 
48     private val dwellPulseDuration = 100L
49     private val dwellExpandDuration = 2000L - dwellPulseDuration
50 
51     private var drawDwell: Boolean = false
52     private var drawRipple: Boolean = false
53 
54     private var lockScreenColorVal = Color.WHITE
55     private val fadeDuration = 83L
56     private val retractDuration = 400L
57     private val dwellShader = DwellRippleShader()
58     private val dwellPaint = Paint()
59     private val rippleShader = RippleShader()
60     private val ripplePaint = Paint()
61     private var unlockedRippleAnimator: Animator? = null
62     private var fadeDwellAnimator: Animator? = null
63     private var retractDwellAnimator: Animator? = null
64     private var dwellPulseOutAnimator: Animator? = null
65     private var dwellRadius: Float = 0f
66         set(value) {
67             dwellShader.maxRadius = value
68             field = value
69         }
70     private var dwellOrigin: Point = Point()
71         set(value) {
72             dwellShader.origin = value
73             field = value
74         }
75     private var radius: Float = 0f
76         set(value) {
77             field = value * .9f
78             rippleShader.rippleSize.setMaxSize(field * 2f, field * 2f)
79         }
80     private var origin: Point = Point()
81         set(value) {
82             rippleShader.setCenter(value.x.toFloat(), value.y.toFloat())
83             field = value
84         }
85 
86     init {
87         rippleShader.rawProgress = 0f
88         rippleShader.pixelDensity = resources.displayMetrics.density
89         rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
90         updateRippleFadeParams()
91         ripplePaint.shader = rippleShader
92         setLockScreenColor(0xffffffff.toInt()) // default color
93 
94         dwellShader.color = 0xffffffff.toInt() // default color
95         dwellShader.progress = 0f
96         dwellShader.distortionStrength = .4f
97         dwellPaint.shader = dwellShader
98         visibility = GONE
99     }
100 
101     fun setSensorLocation(location: Point) {
102         origin = location
103         radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
104     }
105 
106     fun setFingerprintSensorLocation(location: Point, sensorRadius: Float) {
107         origin = location
108         radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
109         dwellOrigin = location
110         dwellRadius = sensorRadius * 1.5f
111     }
112 
113     /**
114      * Animate dwell ripple inwards back to radius 0
115      */
116     fun retractDwellRipple() {
117         if (retractDwellAnimator?.isRunning == true || fadeDwellAnimator?.isRunning == true) {
118             return // let the animation finish
119         }
120 
121         if (dwellPulseOutAnimator?.isRunning == true) {
122             val retractDwellRippleAnimator = ValueAnimator.ofFloat(dwellShader.progress, 0f)
123                     .apply {
124                 interpolator = retractInterpolator
125                 duration = retractDuration
126                 addUpdateListener { animator ->
127                     val now = animator.currentPlayTime
128                     dwellShader.progress = animator.animatedValue as Float
129                     dwellShader.time = now.toFloat()
130 
131                     invalidate()
132                 }
133             }
134 
135             val retractAlphaAnimator = ValueAnimator.ofInt(255, 0).apply {
136                 interpolator = Interpolators.LINEAR
137                 duration = retractDuration
138                 addUpdateListener { animator ->
139                     dwellShader.color = ColorUtils.setAlphaComponent(
140                             dwellShader.color,
141                             animator.animatedValue as Int
142                     )
143                     invalidate()
144                 }
145             }
146 
147             retractDwellAnimator = AnimatorSet().apply {
148                 playTogether(retractDwellRippleAnimator, retractAlphaAnimator)
149                 addListener(object : AnimatorListenerAdapter() {
150                     override fun onAnimationStart(animation: Animator?) {
151                         dwellPulseOutAnimator?.cancel()
152                         drawDwell = true
153                     }
154 
155                     override fun onAnimationEnd(animation: Animator?) {
156                         drawDwell = false
157                         resetDwellAlpha()
158                     }
159                 })
160                 start()
161             }
162         }
163     }
164 
165     /**
166      * Animate ripple fade to alpha=0
167      */
168     fun fadeDwellRipple() {
169         if (fadeDwellAnimator?.isRunning == true) {
170             return // let the animation finish
171         }
172 
173         if (dwellPulseOutAnimator?.isRunning == true || retractDwellAnimator?.isRunning == true) {
174             fadeDwellAnimator = ValueAnimator.ofInt(Color.alpha(dwellShader.color), 0).apply {
175                 interpolator = Interpolators.LINEAR
176                 duration = fadeDuration
177                 addUpdateListener { animator ->
178                     dwellShader.color = ColorUtils.setAlphaComponent(
179                             dwellShader.color,
180                             animator.animatedValue as Int
181                     )
182                     invalidate()
183                 }
184                 addListener(object : AnimatorListenerAdapter() {
185                     override fun onAnimationStart(animation: Animator?) {
186                         retractDwellAnimator?.cancel()
187                         dwellPulseOutAnimator?.cancel()
188                         drawDwell = true
189                     }
190 
191                     override fun onAnimationEnd(animation: Animator?) {
192                         drawDwell = false
193                         resetDwellAlpha()
194                     }
195                 })
196                 start()
197             }
198         }
199     }
200 
201     /**
202      * Plays a ripple animation that grows to the dwellRadius with distortion.
203      */
204     fun startDwellRipple(isDozing: Boolean) {
205         if (unlockedRippleAnimator?.isRunning == true || dwellPulseOutAnimator?.isRunning == true) {
206             return
207         }
208 
209         updateDwellRippleColor(isDozing)
210 
211         val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(0f, .8f).apply {
212             interpolator = Interpolators.LINEAR
213             duration = dwellPulseDuration
214             addUpdateListener { animator ->
215                 val now = animator.currentPlayTime
216                 dwellShader.progress = animator.animatedValue as Float
217                 dwellShader.time = now.toFloat()
218 
219                 invalidate()
220             }
221         }
222 
223         // slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple
224         val expandDwellRippleAnimator = ValueAnimator.ofFloat(.8f, 1f).apply {
225             interpolator = Interpolators.LINEAR_OUT_SLOW_IN
226             duration = dwellExpandDuration
227             addUpdateListener { animator ->
228                 val now = animator.currentPlayTime
229                 dwellShader.progress = animator.animatedValue as Float
230                 dwellShader.time = now.toFloat()
231 
232                 invalidate()
233             }
234         }
235 
236         dwellPulseOutAnimator = AnimatorSet().apply {
237             playSequentially(
238                     dwellPulseOutRippleAnimator,
239                     expandDwellRippleAnimator
240             )
241             addListener(object : AnimatorListenerAdapter() {
242                 override fun onAnimationStart(animation: Animator?) {
243                     retractDwellAnimator?.cancel()
244                     fadeDwellAnimator?.cancel()
245                     visibility = VISIBLE
246                     drawDwell = true
247                 }
248 
249                 override fun onAnimationEnd(animation: Animator?) {
250                     drawDwell = false
251                 }
252             })
253             start()
254         }
255     }
256 
257     /**
258      * Ripple that bursts outwards from the position of the sensor to the edges of the screen
259      */
260     fun startUnlockedRipple(onAnimationEnd: Runnable?) {
261         unlockedRippleAnimator?.cancel()
262 
263         val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
264             duration = AuthRippleController.RIPPLE_ANIMATION_DURATION
265             addUpdateListener { animator ->
266                 val now = animator.currentPlayTime
267                 rippleShader.rawProgress = animator.animatedValue as Float
268                 rippleShader.time = now.toFloat()
269 
270                 invalidate()
271             }
272         }
273 
274         unlockedRippleAnimator = rippleAnimator.apply {
275             addListener(object : AnimatorListenerAdapter() {
276                 override fun onAnimationStart(animation: Animator?) {
277                     drawRipple = true
278                     visibility = VISIBLE
279                 }
280 
281                 override fun onAnimationEnd(animation: Animator?) {
282                     onAnimationEnd?.run()
283                     drawRipple = false
284                     visibility = GONE
285                     unlockedRippleAnimator = null
286                 }
287             })
288         }
289         unlockedRippleAnimator?.start()
290     }
291 
292     fun setLockScreenColor(color: Int) {
293         lockScreenColorVal = color
294         rippleShader.color = ColorUtils.setAlphaComponent(
295                 lockScreenColorVal,
296                 62
297         )
298     }
299 
300     fun updateDwellRippleColor(isDozing: Boolean) {
301         if (isDozing) {
302             dwellShader.color = Color.WHITE
303         } else {
304             dwellShader.color = lockScreenColorVal
305         }
306         resetDwellAlpha()
307     }
308 
309     fun resetDwellAlpha() {
310         dwellShader.color = ColorUtils.setAlphaComponent(
311                 dwellShader.color,
312                 255
313         )
314     }
315 
316     private fun updateRippleFadeParams() {
317         with(rippleShader) {
318             baseRingFadeParams.fadeInStart = 0f
319             baseRingFadeParams.fadeInEnd = .2f
320             baseRingFadeParams.fadeOutStart = .2f
321             baseRingFadeParams.fadeOutEnd = 1f
322 
323             centerFillFadeParams.fadeInStart = 0f
324             centerFillFadeParams.fadeInEnd = .15f
325             centerFillFadeParams.fadeOutStart = .15f
326             centerFillFadeParams.fadeOutEnd = .56f
327         }
328     }
329 
330     override fun onDraw(canvas: Canvas?) {
331         // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
332         // the active effect area. Values here should be kept in sync with the
333         // animation implementation in the ripple shader. (Twice bigger)
334         if (drawDwell) {
335             val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) *
336                     (1 - dwellShader.progress)) * dwellRadius * 2f
337             canvas?.drawCircle(dwellOrigin.x.toFloat(), dwellOrigin.y.toFloat(),
338                     maskRadius, dwellPaint)
339         }
340 
341         if (drawRipple) {
342             canvas?.drawCircle(origin.x.toFloat(), origin.y.toFloat(),
343                     rippleShader.rippleSize.currentWidth, ripplePaint)
344         }
345     }
346 }
347