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