• 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.app.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                         invalidate()
159                     }
160                 })
161                 start()
162             }
163         }
164     }
165 
166     /**
167      * Animate ripple fade to alpha=0
168      */
169     fun fadeDwellRipple() {
170         if (fadeDwellAnimator?.isRunning == true) {
171             return // let the animation finish
172         }
173 
174         if (dwellPulseOutAnimator?.isRunning == true || retractDwellAnimator?.isRunning == true) {
175             fadeDwellAnimator = ValueAnimator.ofInt(Color.alpha(dwellShader.color), 0).apply {
176                 interpolator = Interpolators.LINEAR
177                 duration = fadeDuration
178                 addUpdateListener { animator ->
179                     dwellShader.color = ColorUtils.setAlphaComponent(
180                             dwellShader.color,
181                             animator.animatedValue as Int
182                     )
183                     invalidate()
184                 }
185                 addListener(object : AnimatorListenerAdapter() {
186                     override fun onAnimationStart(animation: Animator) {
187                         retractDwellAnimator?.cancel()
188                         dwellPulseOutAnimator?.cancel()
189                         drawDwell = true
190                     }
191 
192                     override fun onAnimationEnd(animation: Animator) {
193                         drawDwell = false
194                         resetDwellAlpha()
195                         invalidate()
196                     }
197                 })
198                 start()
199             }
200         }
201     }
202 
203     /**
204      * Plays a ripple animation that grows to the dwellRadius with distortion.
205      */
206     fun startDwellRipple(isDozing: Boolean) {
207         if (unlockedRippleAnimator?.isRunning == true || dwellPulseOutAnimator?.isRunning == true) {
208             return
209         }
210 
211         updateDwellRippleColor(isDozing)
212 
213         val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(0f, .8f).apply {
214             interpolator = Interpolators.LINEAR
215             duration = dwellPulseDuration
216             addUpdateListener { animator ->
217                 val now = animator.currentPlayTime
218                 dwellShader.progress = animator.animatedValue as Float
219                 dwellShader.time = now.toFloat()
220 
221                 invalidate()
222             }
223         }
224 
225         // slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple
226         val expandDwellRippleAnimator = ValueAnimator.ofFloat(.8f, 1f).apply {
227             interpolator = Interpolators.LINEAR_OUT_SLOW_IN
228             duration = dwellExpandDuration
229             addUpdateListener { animator ->
230                 val now = animator.currentPlayTime
231                 dwellShader.progress = animator.animatedValue as Float
232                 dwellShader.time = now.toFloat()
233 
234                 invalidate()
235             }
236         }
237 
238         dwellPulseOutAnimator = AnimatorSet().apply {
239             playSequentially(
240                     dwellPulseOutRippleAnimator,
241                     expandDwellRippleAnimator
242             )
243             addListener(object : AnimatorListenerAdapter() {
244                 override fun onAnimationStart(animation: Animator) {
245                     retractDwellAnimator?.cancel()
246                     fadeDwellAnimator?.cancel()
247                     visibility = VISIBLE
248                     drawDwell = true
249                 }
250 
251                 override fun onAnimationEnd(animation: Animator) {
252                     drawDwell = false
253                     invalidate()
254                 }
255             })
256             start()
257         }
258     }
259 
260     /**
261      * Ripple that bursts outwards from the position of the sensor to the edges of the screen
262      */
263     fun startUnlockedRipple(onAnimationEnd: Runnable?) {
264         unlockedRippleAnimator?.cancel()
265 
266         val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
267             duration = AuthRippleController.RIPPLE_ANIMATION_DURATION
268             addUpdateListener { animator ->
269                 val now = animator.currentPlayTime
270                 rippleShader.rawProgress = animator.animatedValue as Float
271                 rippleShader.time = now.toFloat()
272 
273                 invalidate()
274             }
275         }
276 
277         unlockedRippleAnimator = rippleAnimator.apply {
278             addListener(object : AnimatorListenerAdapter() {
279                 override fun onAnimationStart(animation: Animator) {
280                     drawRipple = true
281                     visibility = VISIBLE
282                 }
283 
284                 override fun onAnimationEnd(animation: Animator) {
285                     onAnimationEnd?.run()
286                     drawRipple = false
287                     visibility = GONE
288                     unlockedRippleAnimator = null
289                 }
290             })
291         }
292         unlockedRippleAnimator?.start()
293     }
294 
295     fun setLockScreenColor(color: Int) {
296         lockScreenColorVal = color
297         rippleShader.color = ColorUtils.setAlphaComponent(
298                 lockScreenColorVal,
299                 62
300         )
301     }
302 
303     fun updateDwellRippleColor(isDozing: Boolean) {
304         if (isDozing) {
305             dwellShader.color = Color.WHITE
306         } else {
307             dwellShader.color = lockScreenColorVal
308         }
309         resetDwellAlpha()
310     }
311 
312     fun resetDwellAlpha() {
313         dwellShader.color = ColorUtils.setAlphaComponent(
314                 dwellShader.color,
315                 255
316         )
317     }
318 
319     private fun updateRippleFadeParams() {
320         with(rippleShader) {
321             baseRingFadeParams.fadeInStart = 0f
322             baseRingFadeParams.fadeInEnd = .2f
323             baseRingFadeParams.fadeOutStart = .2f
324             baseRingFadeParams.fadeOutEnd = 1f
325 
326             centerFillFadeParams.fadeInStart = 0f
327             centerFillFadeParams.fadeInEnd = .15f
328             centerFillFadeParams.fadeOutStart = .15f
329             centerFillFadeParams.fadeOutEnd = .56f
330         }
331     }
332 
333     override fun onDraw(canvas: Canvas) {
334         // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
335         // the active effect area. Values here should be kept in sync with the
336         // animation implementation in the ripple shader. (Twice bigger)
337         if (drawDwell) {
338             val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) *
339                     (1 - dwellShader.progress)) * dwellRadius * 2f
340             canvas?.drawCircle(dwellOrigin.x.toFloat(), dwellOrigin.y.toFloat(),
341                     maskRadius, dwellPaint)
342         }
343 
344         if (drawRipple) {
345             canvas?.drawCircle(origin.x.toFloat(), origin.y.toFloat(),
346                     rippleShader.rippleSize.currentWidth, ripplePaint)
347         }
348     }
349 }
350