/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.biometrics; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.view.animation.Interpolator; import android.view.animation.OvershootInterpolator; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.R; /** * UDFPS enrollment progress bar. */ public class UdfpsEnrollProgressBarDrawable extends Drawable { private static final String TAG = "UdfpsProgressBar"; private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L; private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L; private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L; private static final long PROGRESS_ANIMATION_DURATION_MS = 400L; private static final float STROKE_WIDTH_DP = 12f; private final float mStrokeWidthPx; @ColorInt private final int mProgressColor; @ColorInt private final int mHelpColor; @NonNull private final Drawable mCheckmarkDrawable; @NonNull private final Interpolator mCheckmarkInterpolator; @NonNull private final Paint mBackgroundPaint; @NonNull private final Paint mFillPaint; private boolean mAfterFirstTouch; private int mRemainingSteps = 0; private int mTotalSteps = 0; private float mProgress = 0f; @Nullable private ValueAnimator mProgressAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener; private boolean mShowingHelp = false; @Nullable private ValueAnimator mFillColorAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener; private boolean mComplete = false; private float mCheckmarkScale = 0f; @Nullable private ValueAnimator mCheckmarkAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener; public UdfpsEnrollProgressBarDrawable(@NonNull Context context) { mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP); mProgressColor = context.getColor(R.color.udfps_enroll_progress); mHelpColor = context.getColor(R.color.udfps_enroll_progress_help); mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark); mCheckmarkDrawable.mutate(); mCheckmarkInterpolator = new OvershootInterpolator(); mBackgroundPaint = new Paint(); mBackgroundPaint.setStrokeWidth(mStrokeWidthPx); mBackgroundPaint.setColor(context.getColor(R.color.udfps_moving_target_fill)); mBackgroundPaint.setAntiAlias(true); mBackgroundPaint.setStyle(Paint.Style.STROKE); mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); // Progress fill should *not* use the extracted system color. mFillPaint = new Paint(); mFillPaint.setStrokeWidth(mStrokeWidthPx); mFillPaint.setColor(mProgressColor); mFillPaint.setAntiAlias(true); mFillPaint.setStyle(Paint.Style.STROKE); mFillPaint.setStrokeCap(Paint.Cap.ROUND); mProgressUpdateListener = animation -> { mProgress = (float) animation.getAnimatedValue(); invalidateSelf(); }; mFillColorUpdateListener = animation -> { mFillPaint.setColor((int) animation.getAnimatedValue()); invalidateSelf(); }; mCheckmarkUpdateListener = animation -> { mCheckmarkScale = (float) animation.getAnimatedValue(); invalidateSelf(); }; } void onEnrollmentProgress(int remaining, int totalSteps) { mAfterFirstTouch = true; updateState(remaining, totalSteps, false /* showingHelp */); } void onEnrollmentHelp(int remaining, int totalSteps) { updateState(remaining, totalSteps, true /* showingHelp */); } void onLastStepAcquired() { updateState(0, mTotalSteps, false /* showingHelp */); } private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) { updateProgress(remainingSteps, totalSteps); updateFillColor(showingHelp); } private void updateProgress(int remainingSteps, int totalSteps) { if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) { return; } mRemainingSteps = remainingSteps; mTotalSteps = totalSteps; final int progressSteps = Math.max(0, totalSteps - remainingSteps); // If needed, add 1 to progress and total steps to account for initial touch. final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps; final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps; final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal); if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); } mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress); mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS); mProgressAnimator.addUpdateListener(mProgressUpdateListener); mProgressAnimator.start(); if (remainingSteps == 0) { startCompletionAnimation(); } else if (remainingSteps > 0) { rollBackCompletionAnimation(); } } private void updateFillColor(boolean showingHelp) { if (mShowingHelp == showingHelp) { return; } mShowingHelp = showingHelp; if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) { mFillColorAnimator.cancel(); } @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor; mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor); mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS); mFillColorAnimator.addUpdateListener(mFillColorUpdateListener); mFillColorAnimator.start(); } private void startCompletionAnimation() { if (mComplete) { return; } mComplete = true; if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { mCheckmarkAnimator.cancel(); } mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f); mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS); mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS); mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator); mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); mCheckmarkAnimator.start(); } private void rollBackCompletionAnimation() { if (!mComplete) { return; } mComplete = false; // Adjust duration based on how much of the completion animation has played. final float animatedFraction = mCheckmarkAnimator != null ? mCheckmarkAnimator.getAnimatedFraction() : 0f; final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction); if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { mCheckmarkAnimator.cancel(); } mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f); mCheckmarkAnimator.setDuration(durationMs); mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); mCheckmarkAnimator.start(); } @Override public void draw(@NonNull Canvas canvas) { canvas.save(); // Progress starts from the top, instead of the right canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY()); final float halfPaddingPx = mStrokeWidthPx / 2f; if (mProgress < 1f) { // Draw the background color of the progress circle. canvas.drawArc( halfPaddingPx, halfPaddingPx, getBounds().right - halfPaddingPx, getBounds().bottom - halfPaddingPx, 0f /* startAngle */, 360f /* sweepAngle */, false /* useCenter */, mBackgroundPaint); } if (mProgress > 0f) { // Draw the filled portion of the progress circle. canvas.drawArc( halfPaddingPx, halfPaddingPx, getBounds().right - halfPaddingPx, getBounds().bottom - halfPaddingPx, 0f /* startAngle */, 360f * mProgress /* sweepAngle */, false /* useCenter */, mFillPaint); } canvas.restore(); if (mCheckmarkScale > 0f) { final float offsetScale = (float) Math.sqrt(2) / 2f; final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale; final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale; final float centerX = getBounds().centerX() + centerXOffset; final float centerY = getBounds().centerY() + centerYOffset; final float boundsXOffset = mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale; final float boundsYOffset = mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale; final int left = Math.round(centerX - boundsXOffset); final int top = Math.round(centerY - boundsYOffset); final int right = Math.round(centerX + boundsXOffset); final int bottom = Math.round(centerY + boundsYOffset); mCheckmarkDrawable.setBounds(left, top, right, bottom); mCheckmarkDrawable.draw(canvas); } } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(@Nullable ColorFilter colorFilter) { } @Override public int getOpacity() { return 0; } }