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