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 17 package com.android.systemui.biometrics; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorSet; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.graphics.drawable.Drawable; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.view.animation.AccelerateDecelerateInterpolator; 32 import android.view.animation.LinearInterpolator; 33 34 import androidx.annotation.ColorInt; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 38 import com.android.systemui.R; 39 40 /** 41 * UDFPS fingerprint drawable that is shown when enrolling 42 */ 43 public class UdfpsEnrollDrawable extends UdfpsDrawable { 44 private static final String TAG = "UdfpsAnimationEnroll"; 45 46 private static final long HINT_COLOR_ANIM_DELAY_MS = 233L; 47 private static final long HINT_COLOR_ANIM_DURATION_MS = 517L; 48 private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L; 49 private static final long TARGET_ANIM_DURATION_LONG = 800L; 50 private static final long TARGET_ANIM_DURATION_SHORT = 600L; 51 // 1 + SCALE_MAX is the maximum that the moving target will animate to 52 private static final float SCALE_MAX = 0.25f; 53 54 private static final float HINT_PADDING_DP = 10f; 55 private static final float HINT_MAX_WIDTH_DP = 6f; 56 private static final float HINT_ANGLE = 40f; 57 58 private final Handler mHandler = new Handler(Looper.getMainLooper()); 59 60 @NonNull private final Drawable mMovingTargetFpIcon; 61 @NonNull private final Paint mSensorOutlinePaint; 62 @NonNull private final Paint mBlueFill; 63 64 @Nullable private RectF mSensorRect; 65 @Nullable private UdfpsEnrollHelper mEnrollHelper; 66 67 // Moving target animator set 68 @Nullable AnimatorSet mTargetAnimatorSet; 69 // Moving target location 70 float mCurrentX; 71 float mCurrentY; 72 // Moving target size 73 float mCurrentScale = 1.f; 74 75 @ColorInt private final int mHintColorFaded; 76 @ColorInt private final int mHintColorHighlight; 77 private final float mHintMaxWidthPx; 78 private final float mHintPaddingPx; 79 80 @NonNull private final Animator.AnimatorListener mTargetAnimListener; 81 82 private boolean mShouldShowTipHint = false; 83 @NonNull private final Paint mTipHintPaint; 84 @Nullable private AnimatorSet mTipHintAnimatorSet; 85 @Nullable private ValueAnimator mTipHintColorAnimator; 86 @Nullable private ValueAnimator mTipHintWidthAnimator; 87 @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener; 88 @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener; 89 @NonNull private final Animator.AnimatorListener mTipHintPulseListener; 90 91 private boolean mShouldShowEdgeHint = false; 92 @NonNull private final Paint mEdgeHintPaint; 93 @Nullable private AnimatorSet mEdgeHintAnimatorSet; 94 @Nullable private ValueAnimator mEdgeHintColorAnimator; 95 @Nullable private ValueAnimator mEdgeHintWidthAnimator; 96 @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener; 97 @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener; 98 @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener; 99 100 private boolean mShowingNewUdfpsEnroll = false; 101 UdfpsEnrollDrawable(@onNull Context context)102 UdfpsEnrollDrawable(@NonNull Context context) { 103 super(context); 104 105 mSensorOutlinePaint = new Paint(0 /* flags */); 106 mSensorOutlinePaint.setAntiAlias(true); 107 mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_moving_target_fill)); 108 mSensorOutlinePaint.setStyle(Paint.Style.FILL); 109 110 mBlueFill = new Paint(0 /* flags */); 111 mBlueFill.setAntiAlias(true); 112 mBlueFill.setColor(context.getColor(R.color.udfps_moving_target_fill)); 113 mBlueFill.setStyle(Paint.Style.FILL); 114 115 mMovingTargetFpIcon = context.getResources() 116 .getDrawable(R.drawable.ic_kg_fingerprint, null); 117 mMovingTargetFpIcon.setTint(mContext.getColor(R.color.udfps_enroll_icon)); 118 mMovingTargetFpIcon.mutate(); 119 120 mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon)); 121 122 mHintColorFaded = context.getColor(R.color.udfps_moving_target_fill); 123 mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress); 124 mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP); 125 mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP); 126 127 mTargetAnimListener = new Animator.AnimatorListener() { 128 @Override 129 public void onAnimationStart(Animator animation) {} 130 131 @Override 132 public void onAnimationEnd(Animator animation) { 133 updateTipHintVisibility(); 134 } 135 136 @Override 137 public void onAnimationCancel(Animator animation) {} 138 139 @Override 140 public void onAnimationRepeat(Animator animation) {} 141 }; 142 143 mTipHintPaint = new Paint(0 /* flags */); 144 mTipHintPaint.setAntiAlias(true); 145 mTipHintPaint.setColor(mHintColorFaded); 146 mTipHintPaint.setStyle(Paint.Style.STROKE); 147 mTipHintPaint.setStrokeCap(Paint.Cap.ROUND); 148 mTipHintPaint.setStrokeWidth(0f); 149 mTipHintColorUpdateListener = animation -> { 150 mTipHintPaint.setColor((int) animation.getAnimatedValue()); 151 invalidateSelf(); 152 }; 153 mTipHintWidthUpdateListener = animation -> { 154 mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue()); 155 invalidateSelf(); 156 }; 157 mTipHintPulseListener = new Animator.AnimatorListener() { 158 @Override 159 public void onAnimationStart(Animator animation) {} 160 161 @Override 162 public void onAnimationEnd(Animator animation) { 163 mHandler.postDelayed(() -> { 164 mTipHintColorAnimator = 165 ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded); 166 mTipHintColorAnimator.setInterpolator(new LinearInterpolator()); 167 mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS); 168 mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener); 169 mTipHintColorAnimator.start(); 170 }, HINT_COLOR_ANIM_DELAY_MS); 171 } 172 173 @Override 174 public void onAnimationCancel(Animator animation) {} 175 176 @Override 177 public void onAnimationRepeat(Animator animation) {} 178 }; 179 180 mEdgeHintPaint = new Paint(0 /* flags */); 181 mEdgeHintPaint.setAntiAlias(true); 182 mEdgeHintPaint.setColor(mHintColorFaded); 183 mEdgeHintPaint.setStyle(Paint.Style.STROKE); 184 mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND); 185 mEdgeHintPaint.setStrokeWidth(0f); 186 mEdgeHintColorUpdateListener = animation -> { 187 mEdgeHintPaint.setColor((int) animation.getAnimatedValue()); 188 invalidateSelf(); 189 }; 190 mEdgeHintWidthUpdateListener = animation -> { 191 mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue()); 192 invalidateSelf(); 193 }; 194 mEdgeHintPulseListener = new Animator.AnimatorListener() { 195 @Override 196 public void onAnimationStart(Animator animation) {} 197 198 @Override 199 public void onAnimationEnd(Animator animation) { 200 mHandler.postDelayed(() -> { 201 mEdgeHintColorAnimator = 202 ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded); 203 mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator()); 204 mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS); 205 mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener); 206 mEdgeHintColorAnimator.start(); 207 }, HINT_COLOR_ANIM_DELAY_MS); 208 } 209 210 @Override 211 public void onAnimationCancel(Animator animation) {} 212 213 @Override 214 public void onAnimationRepeat(Animator animation) {} 215 }; 216 mShowingNewUdfpsEnroll = context.getResources().getBoolean( 217 com.android.internal.R.bool.config_udfpsSupportsNewUi); 218 } 219 setEnrollHelper(@onNull UdfpsEnrollHelper helper)220 void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) { 221 mEnrollHelper = helper; 222 } 223 224 @Override onSensorRectUpdated(@onNull RectF sensorRect)225 public void onSensorRectUpdated(@NonNull RectF sensorRect) { 226 super.onSensorRectUpdated(sensorRect); 227 mSensorRect = sensorRect; 228 } 229 230 @Override updateFingerprintIconBounds(@onNull Rect bounds)231 protected void updateFingerprintIconBounds(@NonNull Rect bounds) { 232 super.updateFingerprintIconBounds(bounds); 233 mMovingTargetFpIcon.setBounds(bounds); 234 invalidateSelf(); 235 } 236 onEnrollmentProgress(int remaining, int totalSteps)237 void onEnrollmentProgress(int remaining, int totalSteps) { 238 if (mEnrollHelper == null) { 239 return; 240 } 241 242 if (!mEnrollHelper.isCenterEnrollmentStage()) { 243 if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) { 244 mTargetAnimatorSet.end(); 245 } 246 247 final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint(); 248 if (mCurrentX != point.x || mCurrentY != point.y) { 249 final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x); 250 x.addUpdateListener(animation -> { 251 mCurrentX = (float) animation.getAnimatedValue(); 252 invalidateSelf(); 253 }); 254 255 final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y); 256 y.addUpdateListener(animation -> { 257 mCurrentY = (float) animation.getAnimatedValue(); 258 invalidateSelf(); 259 }); 260 261 final boolean isMovingToCenter = point.x == 0f && point.y == 0f; 262 final long duration = isMovingToCenter 263 ? TARGET_ANIM_DURATION_SHORT 264 : TARGET_ANIM_DURATION_LONG; 265 266 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI); 267 scale.setDuration(duration); 268 scale.addUpdateListener(animation -> { 269 // Grow then shrink 270 mCurrentScale = 1 271 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); 272 invalidateSelf(); 273 }); 274 275 mTargetAnimatorSet = new AnimatorSet(); 276 277 mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 278 mTargetAnimatorSet.setDuration(duration); 279 mTargetAnimatorSet.addListener(mTargetAnimListener); 280 mTargetAnimatorSet.playTogether(x, y, scale); 281 mTargetAnimatorSet.start(); 282 } else { 283 updateTipHintVisibility(); 284 } 285 } else { 286 updateTipHintVisibility(); 287 } 288 289 updateEdgeHintVisibility(); 290 } 291 updateTipHintVisibility()292 private void updateTipHintVisibility() { 293 final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage(); 294 if (mShouldShowTipHint == shouldShow) { 295 return; 296 } 297 mShouldShowTipHint = shouldShow; 298 299 if (mShowingNewUdfpsEnroll) { 300 return; 301 } 302 303 if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) { 304 mTipHintWidthAnimator.cancel(); 305 } 306 307 final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f; 308 mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth); 309 mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); 310 mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener); 311 312 if (shouldShow) { 313 startTipHintPulseAnimation(); 314 } else { 315 mTipHintWidthAnimator.start(); 316 } 317 318 } 319 updateEdgeHintVisibility()320 private void updateEdgeHintVisibility() { 321 final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage(); 322 if (mShouldShowEdgeHint == shouldShow) { 323 return; 324 } 325 mShouldShowEdgeHint = shouldShow; 326 327 if (mShowingNewUdfpsEnroll) { 328 return; 329 } 330 331 if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) { 332 mEdgeHintWidthAnimator.cancel(); 333 } 334 335 final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f; 336 mEdgeHintWidthAnimator = 337 ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth); 338 mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); 339 mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener); 340 341 if (shouldShow) { 342 startEdgeHintPulseAnimation(); 343 } else { 344 mEdgeHintWidthAnimator.start(); 345 } 346 } 347 startTipHintPulseAnimation()348 private void startTipHintPulseAnimation() { 349 if (mShowingNewUdfpsEnroll) { 350 return; 351 } 352 353 mHandler.removeCallbacksAndMessages(null); 354 if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) { 355 mTipHintAnimatorSet.cancel(); 356 } 357 if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) { 358 mTipHintColorAnimator.cancel(); 359 } 360 361 mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight); 362 mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); 363 mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener); 364 mTipHintColorAnimator.addListener(mTipHintPulseListener); 365 366 mTipHintAnimatorSet = new AnimatorSet(); 367 mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 368 mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator); 369 mTipHintAnimatorSet.start(); 370 } 371 startEdgeHintPulseAnimation()372 private void startEdgeHintPulseAnimation() { 373 if (mShowingNewUdfpsEnroll) { 374 return; 375 } 376 377 mHandler.removeCallbacksAndMessages(null); 378 if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) { 379 mEdgeHintAnimatorSet.cancel(); 380 } 381 if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) { 382 mEdgeHintColorAnimator.cancel(); 383 } 384 385 mEdgeHintColorAnimator = 386 ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight); 387 mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); 388 mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener); 389 mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener); 390 391 mEdgeHintAnimatorSet = new AnimatorSet(); 392 mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 393 mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator); 394 mEdgeHintAnimatorSet.start(); 395 } 396 isTipHintVisible()397 private boolean isTipHintVisible() { 398 return mTipHintPaint.getStrokeWidth() > 0f; 399 } 400 isEdgeHintVisible()401 private boolean isEdgeHintVisible() { 402 return mEdgeHintPaint.getStrokeWidth() > 0f; 403 } 404 405 @Override draw(@onNull Canvas canvas)406 public void draw(@NonNull Canvas canvas) { 407 if (isIlluminationShowing()) { 408 return; 409 } 410 411 // Draw moving target 412 if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { 413 canvas.save(); 414 canvas.translate(mCurrentX, mCurrentY); 415 416 if (mSensorRect != null) { 417 canvas.scale(mCurrentScale, mCurrentScale, 418 mSensorRect.centerX(), mSensorRect.centerY()); 419 canvas.drawOval(mSensorRect, mBlueFill); 420 } 421 422 mMovingTargetFpIcon.draw(canvas); 423 canvas.restore(); 424 } else { 425 if (mSensorRect != null) { 426 canvas.drawOval(mSensorRect, mSensorOutlinePaint); 427 } 428 mFingerprintDrawable.draw(canvas); 429 mFingerprintDrawable.setAlpha(mAlpha); 430 mSensorOutlinePaint.setAlpha(mAlpha); 431 } 432 433 if (mShowingNewUdfpsEnroll) { 434 return; 435 } 436 437 // Draw the finger tip or edges hint. 438 if (isTipHintVisible() || isEdgeHintVisible()) { 439 canvas.save(); 440 441 // Make arcs start from the top, rather than the right. 442 canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY()); 443 444 final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f; 445 final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f; 446 final float hintXOffset = halfSensorWidth + mHintPaddingPx; 447 final float hintYOffset = halfSensorHeight + mHintPaddingPx; 448 449 if (isTipHintVisible()) { 450 canvas.drawArc( 451 mSensorRect.centerX() - hintXOffset, 452 mSensorRect.centerY() - hintYOffset, 453 mSensorRect.centerX() + hintXOffset, 454 mSensorRect.centerY() + hintYOffset, 455 -HINT_ANGLE / 2f, 456 HINT_ANGLE, 457 false /* useCenter */, 458 mTipHintPaint); 459 } 460 461 if (isEdgeHintVisible()) { 462 // Draw right edge hint. 463 canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY()); 464 canvas.drawArc( 465 mSensorRect.centerX() - hintXOffset, 466 mSensorRect.centerY() - hintYOffset, 467 mSensorRect.centerX() + hintXOffset, 468 mSensorRect.centerY() + hintYOffset, 469 -HINT_ANGLE / 2f, 470 HINT_ANGLE, 471 false /* useCenter */, 472 mEdgeHintPaint); 473 474 // Draw left edge hint. 475 canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY()); 476 canvas.drawArc( 477 mSensorRect.centerX() - hintXOffset, 478 mSensorRect.centerY() - hintYOffset, 479 mSensorRect.centerX() + hintXOffset, 480 mSensorRect.centerY() + hintYOffset, 481 -HINT_ANGLE / 2f, 482 HINT_ANGLE, 483 false /* useCenter */, 484 mEdgeHintPaint); 485 } 486 487 canvas.restore(); 488 } 489 } 490 491 @Override setAlpha(int alpha)492 public void setAlpha(int alpha) { 493 super.setAlpha(alpha); 494 mSensorOutlinePaint.setAlpha(alpha); 495 mBlueFill.setAlpha(alpha); 496 mMovingTargetFpIcon.setAlpha(alpha); 497 invalidateSelf(); 498 } 499 } 500