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