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.ValueAnimator; 20 import android.content.Context; 21 import android.graphics.Canvas; 22 import android.graphics.ColorFilter; 23 import android.graphics.Paint; 24 import android.graphics.drawable.Drawable; 25 import android.view.animation.Interpolator; 26 import android.view.animation.OvershootInterpolator; 27 28 import androidx.annotation.ColorInt; 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import com.android.systemui.R; 33 34 /** 35 * UDFPS enrollment progress bar. 36 */ 37 public class UdfpsEnrollProgressBarDrawable extends Drawable { 38 private static final String TAG = "UdfpsProgressBar"; 39 40 private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L; 41 private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L; 42 private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L; 43 private static final long PROGRESS_ANIMATION_DURATION_MS = 400L; 44 private static final float STROKE_WIDTH_DP = 12f; 45 46 private final float mStrokeWidthPx; 47 @ColorInt private final int mProgressColor; 48 @ColorInt private final int mHelpColor; 49 @NonNull private final Drawable mCheckmarkDrawable; 50 @NonNull private final Interpolator mCheckmarkInterpolator; 51 @NonNull private final Paint mBackgroundPaint; 52 @NonNull private final Paint mFillPaint; 53 54 private boolean mAfterFirstTouch; 55 56 private int mRemainingSteps = 0; 57 private int mTotalSteps = 0; 58 private float mProgress = 0f; 59 @Nullable private ValueAnimator mProgressAnimator; 60 @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener; 61 62 private boolean mShowingHelp = false; 63 @Nullable private ValueAnimator mFillColorAnimator; 64 @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener; 65 66 private boolean mComplete = false; 67 private float mCheckmarkScale = 0f; 68 @Nullable private ValueAnimator mCheckmarkAnimator; 69 @NonNull private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener; 70 UdfpsEnrollProgressBarDrawable(@onNull Context context)71 public UdfpsEnrollProgressBarDrawable(@NonNull Context context) { 72 mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP); 73 mProgressColor = context.getColor(R.color.udfps_enroll_progress); 74 mHelpColor = context.getColor(R.color.udfps_enroll_progress_help); 75 mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark); 76 mCheckmarkDrawable.mutate(); 77 mCheckmarkInterpolator = new OvershootInterpolator(); 78 79 mBackgroundPaint = new Paint(); 80 mBackgroundPaint.setStrokeWidth(mStrokeWidthPx); 81 mBackgroundPaint.setColor(context.getColor(R.color.udfps_moving_target_fill)); 82 mBackgroundPaint.setAntiAlias(true); 83 mBackgroundPaint.setStyle(Paint.Style.STROKE); 84 mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); 85 86 // Progress fill should *not* use the extracted system color. 87 mFillPaint = new Paint(); 88 mFillPaint.setStrokeWidth(mStrokeWidthPx); 89 mFillPaint.setColor(mProgressColor); 90 mFillPaint.setAntiAlias(true); 91 mFillPaint.setStyle(Paint.Style.STROKE); 92 mFillPaint.setStrokeCap(Paint.Cap.ROUND); 93 94 mProgressUpdateListener = animation -> { 95 mProgress = (float) animation.getAnimatedValue(); 96 invalidateSelf(); 97 }; 98 99 mFillColorUpdateListener = animation -> { 100 mFillPaint.setColor((int) animation.getAnimatedValue()); 101 invalidateSelf(); 102 }; 103 104 mCheckmarkUpdateListener = animation -> { 105 mCheckmarkScale = (float) animation.getAnimatedValue(); 106 invalidateSelf(); 107 }; 108 } 109 onEnrollmentProgress(int remaining, int totalSteps)110 void onEnrollmentProgress(int remaining, int totalSteps) { 111 mAfterFirstTouch = true; 112 updateState(remaining, totalSteps, false /* showingHelp */); 113 } 114 onEnrollmentHelp(int remaining, int totalSteps)115 void onEnrollmentHelp(int remaining, int totalSteps) { 116 updateState(remaining, totalSteps, true /* showingHelp */); 117 } 118 onLastStepAcquired()119 void onLastStepAcquired() { 120 updateState(0, mTotalSteps, false /* showingHelp */); 121 } 122 updateState(int remainingSteps, int totalSteps, boolean showingHelp)123 private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) { 124 updateProgress(remainingSteps, totalSteps); 125 updateFillColor(showingHelp); 126 } 127 updateProgress(int remainingSteps, int totalSteps)128 private void updateProgress(int remainingSteps, int totalSteps) { 129 if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) { 130 return; 131 } 132 mRemainingSteps = remainingSteps; 133 mTotalSteps = totalSteps; 134 135 final int progressSteps = Math.max(0, totalSteps - remainingSteps); 136 137 // If needed, add 1 to progress and total steps to account for initial touch. 138 final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps; 139 final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps; 140 141 final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal); 142 143 if (mProgressAnimator != null && mProgressAnimator.isRunning()) { 144 mProgressAnimator.cancel(); 145 } 146 147 mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress); 148 mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS); 149 mProgressAnimator.addUpdateListener(mProgressUpdateListener); 150 mProgressAnimator.start(); 151 152 if (remainingSteps == 0) { 153 startCompletionAnimation(); 154 } else if (remainingSteps > 0) { 155 rollBackCompletionAnimation(); 156 } 157 } 158 updateFillColor(boolean showingHelp)159 private void updateFillColor(boolean showingHelp) { 160 if (mShowingHelp == showingHelp) { 161 return; 162 } 163 mShowingHelp = showingHelp; 164 165 if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) { 166 mFillColorAnimator.cancel(); 167 } 168 169 @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor; 170 mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor); 171 mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS); 172 mFillColorAnimator.addUpdateListener(mFillColorUpdateListener); 173 mFillColorAnimator.start(); 174 } 175 startCompletionAnimation()176 private void startCompletionAnimation() { 177 if (mComplete) { 178 return; 179 } 180 mComplete = true; 181 182 if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { 183 mCheckmarkAnimator.cancel(); 184 } 185 186 mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f); 187 mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS); 188 mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS); 189 mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator); 190 mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); 191 mCheckmarkAnimator.start(); 192 } 193 rollBackCompletionAnimation()194 private void rollBackCompletionAnimation() { 195 if (!mComplete) { 196 return; 197 } 198 mComplete = false; 199 200 // Adjust duration based on how much of the completion animation has played. 201 final float animatedFraction = mCheckmarkAnimator != null 202 ? mCheckmarkAnimator.getAnimatedFraction() 203 : 0f; 204 final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction); 205 206 if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { 207 mCheckmarkAnimator.cancel(); 208 } 209 210 mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f); 211 mCheckmarkAnimator.setDuration(durationMs); 212 mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); 213 mCheckmarkAnimator.start(); 214 } 215 216 @Override draw(@onNull Canvas canvas)217 public void draw(@NonNull Canvas canvas) { 218 canvas.save(); 219 220 // Progress starts from the top, instead of the right 221 canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY()); 222 223 final float halfPaddingPx = mStrokeWidthPx / 2f; 224 225 if (mProgress < 1f) { 226 // Draw the background color of the progress circle. 227 canvas.drawArc( 228 halfPaddingPx, 229 halfPaddingPx, 230 getBounds().right - halfPaddingPx, 231 getBounds().bottom - halfPaddingPx, 232 0f /* startAngle */, 233 360f /* sweepAngle */, 234 false /* useCenter */, 235 mBackgroundPaint); 236 } 237 238 if (mProgress > 0f) { 239 // Draw the filled portion of the progress circle. 240 canvas.drawArc( 241 halfPaddingPx, 242 halfPaddingPx, 243 getBounds().right - halfPaddingPx, 244 getBounds().bottom - halfPaddingPx, 245 0f /* startAngle */, 246 360f * mProgress /* sweepAngle */, 247 false /* useCenter */, 248 mFillPaint); 249 } 250 251 canvas.restore(); 252 253 if (mCheckmarkScale > 0f) { 254 final float offsetScale = (float) Math.sqrt(2) / 2f; 255 final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale; 256 final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale; 257 final float centerX = getBounds().centerX() + centerXOffset; 258 final float centerY = getBounds().centerY() + centerYOffset; 259 260 final float boundsXOffset = 261 mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale; 262 final float boundsYOffset = 263 mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale; 264 265 final int left = Math.round(centerX - boundsXOffset); 266 final int top = Math.round(centerY - boundsYOffset); 267 final int right = Math.round(centerX + boundsXOffset); 268 final int bottom = Math.round(centerY + boundsYOffset); 269 mCheckmarkDrawable.setBounds(left, top, right, bottom); 270 mCheckmarkDrawable.draw(canvas); 271 } 272 } 273 274 @Override setAlpha(int alpha)275 public void setAlpha(int alpha) { 276 } 277 278 @Override setColorFilter(@ullable ColorFilter colorFilter)279 public void setColorFilter(@Nullable ColorFilter colorFilter) { 280 } 281 282 @Override getOpacity()283 public int getOpacity() { 284 return 0; 285 } 286 } 287