Combine UDFPS enroll segments into a single circle
Updates the UDFPS enrollment UI to present a single progress circle instead of 4 segments (corresponding to the 4 underlying enrollment stages). Visual progress is now continuous, based on the number of remaining and total steps reported by FingerprintManager, rather than being normalized across each stage. As part of this change, the completion animation has also been changed to display a checkmark rather than combining each of the segments into a continuous circle at the end of enrollment. Test: Manual Fixes: 203134265 Change-Id: I1004c115a171deb14b579fa72ff00e951ca15f94
This commit is contained in:
35
packages/SystemUI/res/drawable/udfps_enroll_checkmark.xml
Normal file
35
packages/SystemUI/res/drawable/udfps_enroll_checkmark.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="54dp"
|
||||
android:height="54dp"
|
||||
android:viewportWidth="54"
|
||||
android:viewportHeight="54">
|
||||
<path
|
||||
android:pathData="M26.9999,3.9619C39.7029,3.9619 50.0369,14.2969 50.0369,26.9999C50.0369,39.7029 39.7029,50.0379 26.9999,50.0379C14.2969,50.0379 3.9629,39.7029 3.9629,26.9999C3.9629,14.2969 14.2969,3.9619 26.9999,3.9619Z"
|
||||
android:fillColor="?android:colorBackground"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M27,0C12.088,0 0,12.088 0,27C0,41.912 12.088,54 27,54C41.912,54 54,41.912 54,27C54,12.088 41.912,0 27,0ZM27,3.962C39.703,3.962 50.037,14.297 50.037,27C50.037,39.703 39.703,50.038 27,50.038C14.297,50.038 3.963,39.703 3.963,27C3.963,14.297 14.297,3.962 27,3.962Z"
|
||||
android:fillColor="@color/udfps_enroll_progress"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M23.0899,38.8534L10.4199,26.1824L13.2479,23.3544L23.0899,33.1974L41.2389,15.0474L44.0679,17.8754L23.0899,38.8534Z"
|
||||
android:fillColor="@color/udfps_enroll_progress"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -16,17 +16,22 @@
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* UDFPS enrollment progress bar.
|
||||
@@ -34,108 +39,193 @@ import java.util.List;
|
||||
public class UdfpsEnrollProgressBarDrawable extends Drawable {
|
||||
private static final String TAG = "UdfpsProgressBar";
|
||||
|
||||
private static final float SEGMENT_GAP_ANGLE = 12f;
|
||||
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;
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
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;
|
||||
|
||||
@Nullable private UdfpsEnrollHelper mEnrollHelper;
|
||||
@NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
|
||||
private boolean mAfterFirstTouch;
|
||||
|
||||
private int mRemainingSteps = 0;
|
||||
private int mTotalSteps = 0;
|
||||
private int mProgressSteps = 0;
|
||||
private boolean mIsShowingHelp = false;
|
||||
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) {
|
||||
mContext = 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();
|
||||
|
||||
void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
|
||||
mEnrollHelper = enrollHelper;
|
||||
if (enrollHelper != null) {
|
||||
final int stageCount = enrollHelper.getStageCount();
|
||||
mSegments = new ArrayList<>(stageCount);
|
||||
float startAngle = SEGMENT_GAP_ANGLE / 2f;
|
||||
final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
|
||||
final Runnable invalidateRunnable = this::invalidateSelf;
|
||||
for (int index = 0; index < stageCount; index++) {
|
||||
mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
|
||||
sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
|
||||
startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
|
||||
}
|
||||
invalidateSelf();
|
||||
mBackgroundPaint = new Paint();
|
||||
mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
|
||||
mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
|
||||
mBackgroundPaint.setAntiAlias(true);
|
||||
mBackgroundPaint.setStyle(Paint.Style.STROKE);
|
||||
mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
// Set background paint color and alpha.
|
||||
final int[] attrs = new int[] {android.R.attr.colorControlNormal};
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(attrs);
|
||||
try {
|
||||
@ColorInt final int tintColor = typedArray.getColor(0, mBackgroundPaint.getColor());
|
||||
mBackgroundPaint.setColor(tintColor);
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
TypedValue alpha = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
|
||||
mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
|
||||
|
||||
// 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) {
|
||||
mTotalSteps = totalSteps;
|
||||
|
||||
// Show some progress for the initial touch.
|
||||
updateState(Math.max(1, totalSteps - remaining), false /* isShowingHelp */);
|
||||
mAfterFirstTouch = true;
|
||||
updateState(remaining, totalSteps, false /* showingHelp */);
|
||||
}
|
||||
|
||||
void onEnrollmentHelp(int remaining, int totalSteps) {
|
||||
updateState(Math.max(0, totalSteps - remaining), true /* isShowingHelp */);
|
||||
updateState(remaining, totalSteps, true /* showingHelp */);
|
||||
}
|
||||
|
||||
void onLastStepAcquired() {
|
||||
updateState(mTotalSteps, false /* isShowingHelp */);
|
||||
updateState(0, mTotalSteps, false /* showingHelp */);
|
||||
}
|
||||
|
||||
private void updateState(int progressSteps, boolean isShowingHelp) {
|
||||
updateProgress(progressSteps);
|
||||
updateFillColor(isShowingHelp);
|
||||
private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
|
||||
updateProgress(remainingSteps, totalSteps);
|
||||
updateFillColor(showingHelp);
|
||||
}
|
||||
|
||||
private void updateProgress(int progressSteps) {
|
||||
if (mProgressSteps == progressSteps) {
|
||||
private void updateProgress(int remainingSteps, int totalSteps) {
|
||||
if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
|
||||
return;
|
||||
}
|
||||
mProgressSteps = progressSteps;
|
||||
mRemainingSteps = remainingSteps;
|
||||
mTotalSteps = totalSteps;
|
||||
|
||||
if (mEnrollHelper == null) {
|
||||
Log.e(TAG, "updateState: UDFPS enroll helper was null");
|
||||
return;
|
||||
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();
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
int prevThreshold = 0;
|
||||
while (index < mSegments.size()) {
|
||||
final UdfpsEnrollProgressBarSegment segment = mSegments.get(index);
|
||||
final int thresholdSteps = mEnrollHelper.getStageThresholdSteps(mTotalSteps, index);
|
||||
if (progressSteps >= thresholdSteps && segment.getProgress() < 1f) {
|
||||
segment.updateProgress(1f);
|
||||
break;
|
||||
} else if (progressSteps >= prevThreshold && progressSteps < thresholdSteps) {
|
||||
final int relativeSteps = progressSteps - prevThreshold;
|
||||
final int relativeThreshold = thresholdSteps - prevThreshold;
|
||||
final float segmentProgress = (float) relativeSteps / (float) relativeThreshold;
|
||||
segment.updateProgress(segmentProgress);
|
||||
break;
|
||||
}
|
||||
mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
|
||||
mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
|
||||
mProgressAnimator.addUpdateListener(mProgressUpdateListener);
|
||||
mProgressAnimator.start();
|
||||
|
||||
index++;
|
||||
prevThreshold = thresholdSteps;
|
||||
}
|
||||
|
||||
if (progressSteps >= mTotalSteps) {
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.startCompletionAnimation();
|
||||
}
|
||||
} else {
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.cancelCompletionAnimation();
|
||||
}
|
||||
if (remainingSteps == 0) {
|
||||
startCompletionAnimation();
|
||||
} else if (remainingSteps > 0) {
|
||||
rollBackCompletionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFillColor(boolean isShowingHelp) {
|
||||
if (mIsShowingHelp == isShowingHelp) {
|
||||
private void updateFillColor(boolean showingHelp) {
|
||||
if (mShowingHelp == showingHelp) {
|
||||
return;
|
||||
}
|
||||
mIsShowingHelp = isShowingHelp;
|
||||
mShowingHelp = showingHelp;
|
||||
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.updateFillColor(isShowingHelp);
|
||||
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
|
||||
@@ -145,12 +235,55 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
|
||||
// Progress starts from the top, instead of the right
|
||||
canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
|
||||
|
||||
// Draw each of the enroll segments.
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.draw(canvas);
|
||||
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
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* 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.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* A single segment of the UDFPS enrollment progress bar.
|
||||
*/
|
||||
public class UdfpsEnrollProgressBarSegment {
|
||||
private static final String TAG = "UdfpsProgressBarSegment";
|
||||
|
||||
private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
|
||||
private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
|
||||
private static final long OVER_SWEEP_ANIMATION_DELAY_MS = 200L;
|
||||
private static final long OVER_SWEEP_ANIMATION_DURATION_MS = 200L;
|
||||
|
||||
private static final float STROKE_WIDTH_DP = 12f;
|
||||
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@NonNull private final Rect mBounds;
|
||||
@NonNull private final Runnable mInvalidateRunnable;
|
||||
private final float mStartAngle;
|
||||
private final float mSweepAngle;
|
||||
private final float mMaxOverSweepAngle;
|
||||
private final float mStrokeWidthPx;
|
||||
@ColorInt private final int mProgressColor;
|
||||
@ColorInt private final int mHelpColor;
|
||||
|
||||
@NonNull private final Paint mBackgroundPaint;
|
||||
@NonNull private final Paint mProgressPaint;
|
||||
|
||||
private float mProgress = 0f;
|
||||
private float mAnimatedProgress = 0f;
|
||||
@Nullable private ValueAnimator mProgressAnimator;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
|
||||
|
||||
private boolean mIsShowingHelp = false;
|
||||
@Nullable private ValueAnimator mFillColorAnimator;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
|
||||
|
||||
private float mOverSweepAngle = 0f;
|
||||
@Nullable private ValueAnimator mOverSweepAnimator;
|
||||
@Nullable private ValueAnimator mOverSweepReverseAnimator;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mOverSweepUpdateListener;
|
||||
@NonNull private final Runnable mOverSweepAnimationRunnable;
|
||||
|
||||
public UdfpsEnrollProgressBarSegment(@NonNull Context context, @NonNull Rect bounds,
|
||||
float startAngle, float sweepAngle, float maxOverSweepAngle,
|
||||
@NonNull Runnable invalidateRunnable) {
|
||||
|
||||
mBounds = bounds;
|
||||
mInvalidateRunnable = invalidateRunnable;
|
||||
mStartAngle = startAngle;
|
||||
mSweepAngle = sweepAngle;
|
||||
mMaxOverSweepAngle = maxOverSweepAngle;
|
||||
mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
|
||||
mProgressColor = context.getColor(R.color.udfps_enroll_progress);
|
||||
mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
|
||||
|
||||
mBackgroundPaint = new Paint();
|
||||
mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
|
||||
mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
|
||||
mBackgroundPaint.setAntiAlias(true);
|
||||
mBackgroundPaint.setStyle(Paint.Style.STROKE);
|
||||
mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
// Background paint color + alpha
|
||||
final int[] attrs = new int[] {android.R.attr.colorControlNormal};
|
||||
final TypedArray ta = context.obtainStyledAttributes(attrs);
|
||||
@ColorInt final int tintColor = ta.getColor(0, mBackgroundPaint.getColor());
|
||||
mBackgroundPaint.setColor(tintColor);
|
||||
ta.recycle();
|
||||
TypedValue alpha = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
|
||||
mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
|
||||
|
||||
// Progress should not be color extracted
|
||||
mProgressPaint = new Paint();
|
||||
mProgressPaint.setStrokeWidth(mStrokeWidthPx);
|
||||
mProgressPaint.setColor(mProgressColor);
|
||||
mProgressPaint.setAntiAlias(true);
|
||||
mProgressPaint.setStyle(Paint.Style.STROKE);
|
||||
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
mProgressUpdateListener = animation -> {
|
||||
mAnimatedProgress = (float) animation.getAnimatedValue();
|
||||
mInvalidateRunnable.run();
|
||||
};
|
||||
|
||||
mFillColorUpdateListener = animation -> {
|
||||
mProgressPaint.setColor((int) animation.getAnimatedValue());
|
||||
mInvalidateRunnable.run();
|
||||
};
|
||||
|
||||
mOverSweepUpdateListener = animation -> {
|
||||
mOverSweepAngle = (float) animation.getAnimatedValue();
|
||||
mInvalidateRunnable.run();
|
||||
};
|
||||
mOverSweepAnimationRunnable = () -> {
|
||||
if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
|
||||
mOverSweepAnimator.cancel();
|
||||
}
|
||||
mOverSweepAnimator = ValueAnimator.ofFloat(mOverSweepAngle, mMaxOverSweepAngle);
|
||||
mOverSweepAnimator.setDuration(OVER_SWEEP_ANIMATION_DURATION_MS);
|
||||
mOverSweepAnimator.addUpdateListener(mOverSweepUpdateListener);
|
||||
mOverSweepAnimator.start();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws this segment to the given canvas.
|
||||
*/
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
final float halfPaddingPx = mStrokeWidthPx / 2f;
|
||||
|
||||
if (mAnimatedProgress < 1f) {
|
||||
// Draw the unfilled background color of the segment.
|
||||
canvas.drawArc(
|
||||
halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
mBounds.right - halfPaddingPx,
|
||||
mBounds.bottom - halfPaddingPx,
|
||||
mStartAngle,
|
||||
mSweepAngle,
|
||||
false /* useCenter */,
|
||||
mBackgroundPaint);
|
||||
}
|
||||
|
||||
if (mAnimatedProgress > 0f) {
|
||||
// Draw the filled progress portion of the segment.
|
||||
canvas.drawArc(
|
||||
halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
mBounds.right - halfPaddingPx,
|
||||
mBounds.bottom - halfPaddingPx,
|
||||
mStartAngle,
|
||||
mSweepAngle * mAnimatedProgress + mOverSweepAngle,
|
||||
false /* useCenter */,
|
||||
mProgressPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The fill progress of this segment, in the range [0, 1]. If fill progress is being
|
||||
* animated, returns the value it is animating to.
|
||||
*/
|
||||
public float getProgress() {
|
||||
return mProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fill progress of this segment, animating if necessary.
|
||||
*
|
||||
* @param progress The new fill progress, in the range [0, 1].
|
||||
*/
|
||||
public void updateProgress(float progress) {
|
||||
updateProgress(progress, PROGRESS_ANIMATION_DURATION_MS);
|
||||
}
|
||||
|
||||
private void updateProgress(float progress, long animationDurationMs) {
|
||||
if (mProgress == progress) {
|
||||
return;
|
||||
}
|
||||
mProgress = progress;
|
||||
|
||||
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
||||
mProgressAnimator.cancel();
|
||||
}
|
||||
|
||||
mProgressAnimator = ValueAnimator.ofFloat(mAnimatedProgress, progress);
|
||||
mProgressAnimator.setDuration(animationDurationMs);
|
||||
mProgressAnimator.addUpdateListener(mProgressUpdateListener);
|
||||
mProgressAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fill color of this segment, animating if necessary.
|
||||
*
|
||||
* @param isShowingHelp Whether fill color should indicate that a help message is being shown.
|
||||
*/
|
||||
public void updateFillColor(boolean isShowingHelp) {
|
||||
if (mIsShowingHelp == isShowingHelp) {
|
||||
return;
|
||||
}
|
||||
mIsShowingHelp = isShowingHelp;
|
||||
|
||||
if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
|
||||
mFillColorAnimator.cancel();
|
||||
}
|
||||
|
||||
@ColorInt final int targetColor = isShowingHelp ? mHelpColor : mProgressColor;
|
||||
mFillColorAnimator = ValueAnimator.ofArgb(mProgressPaint.getColor(), targetColor);
|
||||
mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
|
||||
mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
|
||||
mFillColorAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues and runs the completion animation for this segment.
|
||||
*/
|
||||
public void startCompletionAnimation() {
|
||||
final boolean hasCallback = mHandler.hasCallbacks(mOverSweepAnimationRunnable);
|
||||
if (hasCallback || mOverSweepAngle >= mMaxOverSweepAngle) {
|
||||
Log.d(TAG, "startCompletionAnimation skipped: hasCallback = " + hasCallback
|
||||
+ ", mOverSweepAngle = " + mOverSweepAngle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset sweep angle back to zero if the animation is being rolled back.
|
||||
if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
|
||||
mOverSweepReverseAnimator.cancel();
|
||||
mOverSweepAngle = 0f;
|
||||
}
|
||||
|
||||
// Clear help color and start filling the segment if it isn't already.
|
||||
if (mAnimatedProgress < 1f) {
|
||||
updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS);
|
||||
updateFillColor(false /* isShowingHelp */);
|
||||
}
|
||||
|
||||
// Queue the animation to run after fill completes.
|
||||
mHandler.postDelayed(mOverSweepAnimationRunnable, OVER_SWEEP_ANIMATION_DELAY_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels (and reverses, if necessary) a queued or running completion animation.
|
||||
*/
|
||||
public void cancelCompletionAnimation() {
|
||||
// Cancel the animation if it's queued or running.
|
||||
mHandler.removeCallbacks(mOverSweepAnimationRunnable);
|
||||
if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
|
||||
mOverSweepAnimator.cancel();
|
||||
}
|
||||
|
||||
// Roll back the animation if it has at least partially run.
|
||||
if (mOverSweepAngle > 0f) {
|
||||
if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
|
||||
mOverSweepReverseAnimator.cancel();
|
||||
}
|
||||
|
||||
final float completion = mOverSweepAngle / mMaxOverSweepAngle;
|
||||
final long proratedDuration = (long) (OVER_SWEEP_ANIMATION_DURATION_MS * completion);
|
||||
mOverSweepReverseAnimator = ValueAnimator.ofFloat(mOverSweepAngle, 0f);
|
||||
mOverSweepReverseAnimator.setDuration(proratedDuration);
|
||||
mOverSweepReverseAnimator.addUpdateListener(mOverSweepUpdateListener);
|
||||
mOverSweepReverseAnimator.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,6 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
|
||||
}
|
||||
|
||||
void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
|
||||
mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
|
||||
mFingerprintDrawable.setEnrollHelper(enrollHelper);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user