1 /* 2 * 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.PointF 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.statusbar.charging.DwellRippleShader 33 import com.android.systemui.statusbar.charging.RippleShader 34 35 private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f 36 37 /** 38 * Expanding ripple effect 39 * - startUnlockedRipple for the transition from biometric authentication success to showing 40 * launcher. 41 * - startDwellRipple for the ripple expansion out when the user has their finger down on the UDFPS 42 * sensor area 43 * - retractRipple for the ripple animation inwards to signal a failure 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 retractDuration = 400L 56 private var alphaInDuration: Long = 0 57 private var unlockedRippleInProgress: Boolean = false 58 private val dwellShader = DwellRippleShader() 59 private val dwellPaint = Paint() 60 private val rippleShader = RippleShader() 61 private val ripplePaint = Paint() 62 private var retractAnimator: Animator? = null 63 private var dwellPulseOutAnimator: Animator? = null 64 private var dwellRadius: Float = 0f 65 set(value) { 66 dwellShader.maxRadius = value 67 field = value 68 } 69 private var dwellOrigin: PointF = PointF() 70 set(value) { 71 dwellShader.origin = value 72 field = value 73 } 74 private var radius: Float = 0f 75 set(value) { 76 rippleShader.radius = value 77 field = value 78 } 79 private var origin: PointF = PointF() 80 set(value) { 81 rippleShader.origin = value 82 field = value 83 } 84 85 init { 86 rippleShader.color = 0xffffffff.toInt() // default color 87 rippleShader.progress = 0f 88 rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH 89 ripplePaint.shader = rippleShader 90 91 dwellShader.color = 0xffffffff.toInt() // default color 92 dwellShader.progress = 0f 93 dwellShader.distortionStrength = .4f 94 dwellPaint.shader = dwellShader 95 visibility = GONE 96 } 97 98 fun setSensorLocation(location: PointF) { 99 origin = location 100 radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat() 101 } 102 103 fun setFingerprintSensorLocation(location: PointF, sensorRadius: Float) { 104 origin = location 105 radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat() 106 dwellOrigin = location 107 dwellRadius = sensorRadius * 1.5f 108 } 109 110 fun setAlphaInDuration(duration: Long) { 111 alphaInDuration = duration 112 } 113 114 /** 115 * Animate ripple inwards back to radius 0 116 */ 117 fun retractRipple() { 118 if (retractAnimator?.isRunning == true) { 119 return // let the animation finish 120 } 121 122 if (dwellPulseOutAnimator?.isRunning == true) { 123 val retractRippleAnimator = ValueAnimator.ofFloat(dwellShader.progress, 0f) 124 .apply { 125 interpolator = retractInterpolator 126 duration = retractDuration 127 addUpdateListener { animator -> 128 val now = animator.currentPlayTime 129 dwellShader.progress = animator.animatedValue as Float 130 dwellShader.time = now.toFloat() 131 132 invalidate() 133 } 134 } 135 136 val retractAlphaAnimator = ValueAnimator.ofInt(255, 0).apply { 137 interpolator = Interpolators.LINEAR 138 duration = retractDuration 139 addUpdateListener { animator -> 140 dwellShader.color = ColorUtils.setAlphaComponent( 141 dwellShader.color, 142 animator.animatedValue as Int 143 ) 144 invalidate() 145 } 146 } 147 148 retractAnimator = AnimatorSet().apply { 149 playTogether(retractRippleAnimator, retractAlphaAnimator) 150 addListener(object : AnimatorListenerAdapter() { 151 override fun onAnimationStart(animation: Animator?) { 152 dwellPulseOutAnimator?.cancel() 153 drawDwell = true 154 } 155 156 override fun onAnimationEnd(animation: Animator?) { 157 drawDwell = false 158 resetDwellAlpha() 159 } 160 }) 161 start() 162 } 163 } 164 } 165 166 /** 167 * Plays a ripple animation that grows to the dwellRadius with distortion. 168 */ 169 fun startDwellRipple(isDozing: Boolean) { 170 if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) { 171 return 172 } 173 174 updateDwellRippleColor(isDozing) 175 176 val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(0f, .8f).apply { 177 interpolator = Interpolators.LINEAR 178 duration = dwellPulseDuration 179 addUpdateListener { animator -> 180 val now = animator.currentPlayTime 181 dwellShader.progress = animator.animatedValue as Float 182 dwellShader.time = now.toFloat() 183 184 invalidate() 185 } 186 } 187 188 // slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple 189 val expandDwellRippleAnimator = ValueAnimator.ofFloat(.8f, 1f).apply { 190 interpolator = Interpolators.LINEAR_OUT_SLOW_IN 191 duration = dwellExpandDuration 192 addUpdateListener { animator -> 193 val now = animator.currentPlayTime 194 dwellShader.progress = animator.animatedValue as Float 195 dwellShader.time = now.toFloat() 196 197 invalidate() 198 } 199 } 200 201 dwellPulseOutAnimator = AnimatorSet().apply { 202 playSequentially( 203 dwellPulseOutRippleAnimator, 204 expandDwellRippleAnimator 205 ) 206 addListener(object : AnimatorListenerAdapter() { 207 override fun onAnimationStart(animation: Animator?) { 208 retractAnimator?.cancel() 209 visibility = VISIBLE 210 drawDwell = true 211 } 212 213 override fun onAnimationEnd(animation: Animator?) { 214 drawDwell = false 215 resetRippleAlpha() 216 } 217 }) 218 start() 219 } 220 } 221 222 /** 223 * Ripple that bursts outwards from the position of the sensor to the edges of the screen 224 */ 225 fun startUnlockedRipple(onAnimationEnd: Runnable?) { 226 if (unlockedRippleInProgress) { 227 return // Ignore if ripple effect is already playing 228 } 229 230 val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply { 231 interpolator = Interpolators.LINEAR_OUT_SLOW_IN 232 duration = AuthRippleController.RIPPLE_ANIMATION_DURATION 233 addUpdateListener { animator -> 234 val now = animator.currentPlayTime 235 rippleShader.progress = animator.animatedValue as Float 236 rippleShader.time = now.toFloat() 237 238 invalidate() 239 } 240 } 241 242 val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply { 243 duration = alphaInDuration 244 addUpdateListener { animator -> 245 rippleShader.color = ColorUtils.setAlphaComponent( 246 rippleShader.color, 247 animator.animatedValue as Int 248 ) 249 invalidate() 250 } 251 } 252 253 val animatorSet = AnimatorSet().apply { 254 playTogether( 255 rippleAnimator, 256 alphaInAnimator 257 ) 258 addListener(object : AnimatorListenerAdapter() { 259 override fun onAnimationStart(animation: Animator?) { 260 unlockedRippleInProgress = true 261 rippleShader.shouldFadeOutRipple = true 262 drawRipple = true 263 visibility = VISIBLE 264 } 265 266 override fun onAnimationEnd(animation: Animator?) { 267 onAnimationEnd?.run() 268 unlockedRippleInProgress = false 269 drawRipple = false 270 visibility = GONE 271 } 272 }) 273 } 274 animatorSet.start() 275 } 276 277 fun resetRippleAlpha() { 278 rippleShader.color = ColorUtils.setAlphaComponent( 279 rippleShader.color, 280 255 281 ) 282 } 283 284 fun setLockScreenColor(color: Int) { 285 lockScreenColorVal = color 286 rippleShader.color = lockScreenColorVal 287 resetRippleAlpha() 288 } 289 290 fun updateDwellRippleColor(isDozing: Boolean) { 291 if (isDozing) { 292 dwellShader.color = Color.WHITE 293 } else { 294 dwellShader.color = lockScreenColorVal 295 } 296 resetDwellAlpha() 297 } 298 299 fun resetDwellAlpha() { 300 dwellShader.color = ColorUtils.setAlphaComponent( 301 dwellShader.color, 302 255 303 ) 304 } 305 306 override fun onDraw(canvas: Canvas?) { 307 // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover 308 // the active effect area. Values here should be kept in sync with the 309 // animation implementation in the ripple shader. 310 if (drawDwell) { 311 val maskRadius = (1 - (1 - dwellShader.progress) * (1 - dwellShader.progress) * 312 (1 - dwellShader.progress)) * dwellRadius * 2f 313 canvas?.drawCircle(dwellOrigin.x, dwellOrigin.y, maskRadius, dwellPaint) 314 } 315 316 if (drawRipple) { 317 val mask = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * 318 (1 - rippleShader.progress)) * radius * 2f 319 canvas?.drawCircle(origin.x, origin.y, mask, ripplePaint) 320 } 321 } 322 } 323