Merge "Support configurable multi-stage UDFPS enrollment" into sc-v2-dev am: 8f8783ca1b
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15876070 Change-Id: I2a05114500ec35d26f02ddd0039ff47f0f76710f
This commit is contained in:
@@ -146,6 +146,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing
|
||||
private CryptoObject mCryptoObject;
|
||||
@Nullable private RemoveTracker mRemoveTracker;
|
||||
private Handler mHandler;
|
||||
@Nullable private float[] mEnrollStageThresholds;
|
||||
|
||||
/**
|
||||
* Retrieves a list of properties for all fingerprint sensors on the device.
|
||||
@@ -1326,6 +1327,46 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public int getEnrollStageCount() {
|
||||
if (mEnrollStageThresholds == null) {
|
||||
mEnrollStageThresholds = createEnrollStageThresholds(mContext);
|
||||
}
|
||||
return mEnrollStageThresholds.length + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public float getEnrollStageThreshold(int index) {
|
||||
if (mEnrollStageThresholds == null) {
|
||||
mEnrollStageThresholds = createEnrollStageThresholds(mContext);
|
||||
}
|
||||
|
||||
if (index < 0 || index > mEnrollStageThresholds.length) {
|
||||
Slog.w(TAG, "Unsupported enroll stage index: " + index);
|
||||
return index < 0 ? 0f : 1f;
|
||||
}
|
||||
|
||||
// The implicit threshold for the final stage is always 1.
|
||||
return index == mEnrollStageThresholds.length ? 1f : mEnrollStageThresholds[index];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static float[] createEnrollStageThresholds(@NonNull Context context) {
|
||||
// TODO(b/200604947): Fetch this value from FingerprintService, rather than internal config
|
||||
final String[] enrollStageThresholdStrings = context.getResources().getStringArray(
|
||||
com.android.internal.R.array.config_udfps_enroll_stage_thresholds);
|
||||
|
||||
final float[] enrollStageThresholds = new float[enrollStageThresholdStrings.length];
|
||||
for (int i = 0; i < enrollStageThresholds.length; i++) {
|
||||
enrollStageThresholds[i] = Float.parseFloat(enrollStageThresholdStrings[i]);
|
||||
}
|
||||
return enrollStageThresholds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
|
||||
@@ -4601,6 +4601,13 @@
|
||||
<!-- Indicates whether device has a power button fingerprint sensor. -->
|
||||
<bool name="config_is_powerbutton_fps" translatable="false" >false</bool>
|
||||
|
||||
<!-- When each intermediate UDFPS enroll stage ends, as a fraction of total progress. -->
|
||||
<string-array name="config_udfps_enroll_stage_thresholds" translatable="false">
|
||||
<item>0.25</item>
|
||||
<item>0.5</item>
|
||||
<item>0.75</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Messages that should not be shown to the user during face auth enrollment. This should be
|
||||
used to hide messages that may be too chatty or messages that the user can't do much about.
|
||||
Entries are defined in android.hardware.biometrics.face@1.0 types.hal -->
|
||||
|
||||
@@ -2613,6 +2613,7 @@
|
||||
<java-symbol type="array" name="config_sfps_sensor_props" />
|
||||
<java-symbol type="integer" name="config_udfps_illumination_transition_ms" />
|
||||
<java-symbol type="bool" name="config_is_powerbutton_fps" />
|
||||
<java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
|
||||
|
||||
<java-symbol type="array" name="config_face_acquire_enroll_ignorelist" />
|
||||
<java-symbol type="array" name="config_face_acquire_vendor_enroll_ignorelist" />
|
||||
@@ -4418,7 +4419,7 @@
|
||||
<java-symbol type="string" name="view_and_control_notification_title" />
|
||||
<java-symbol type="string" name="view_and_control_notification_content" />
|
||||
<java-symbol type="array" name="config_accessibility_allowed_install_source" />
|
||||
|
||||
|
||||
<!-- Translation -->
|
||||
<java-symbol type="string" name="ui_translation_accessibility_translated_text" />
|
||||
<java-symbol type="string" name="ui_translation_accessibility_translation_finished" />
|
||||
|
||||
@@ -244,7 +244,7 @@ public class UdfpsController implements DozeReceiver {
|
||||
final UdfpsEnrollHelper enrollHelper;
|
||||
if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
|
||||
|| reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) {
|
||||
enrollHelper = new UdfpsEnrollHelper(mContext, reason);
|
||||
enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason);
|
||||
} else {
|
||||
enrollHelper = null;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
@@ -26,11 +28,17 @@ import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.TypedValue;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
@@ -39,10 +47,20 @@ import com.android.systemui.R;
|
||||
public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
private static final String TAG = "UdfpsAnimationEnroll";
|
||||
|
||||
private static final long ANIM_DURATION = 800;
|
||||
private static final long HINT_COLOR_ANIM_DELAY_MS = 233L;
|
||||
private static final long HINT_COLOR_ANIM_DURATION_MS = 517L;
|
||||
private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L;
|
||||
private static final long TARGET_ANIM_DURATION_LONG = 800L;
|
||||
private static final long TARGET_ANIM_DURATION_SHORT = 600L;
|
||||
// 1 + SCALE_MAX is the maximum that the moving target will animate to
|
||||
private static final float SCALE_MAX = 0.25f;
|
||||
|
||||
private static final float HINT_PADDING_DP = 10f;
|
||||
private static final float HINT_MAX_WIDTH_DP = 6f;
|
||||
private static final float HINT_ANGLE = 40f;
|
||||
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@NonNull private final Drawable mMovingTargetFpIcon;
|
||||
@NonNull private final Paint mSensorOutlinePaint;
|
||||
@NonNull private final Paint mBlueFill;
|
||||
@@ -51,17 +69,41 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
@Nullable private UdfpsEnrollHelper mEnrollHelper;
|
||||
|
||||
// Moving target animator set
|
||||
@Nullable AnimatorSet mAnimatorSet;
|
||||
@Nullable AnimatorSet mTargetAnimatorSet;
|
||||
// Moving target location
|
||||
float mCurrentX;
|
||||
float mCurrentY;
|
||||
// Moving target size
|
||||
float mCurrentScale = 1.f;
|
||||
|
||||
@ColorInt private final int mHintColorFaded;
|
||||
@ColorInt private final int mHintColorHighlight;
|
||||
private final float mHintMaxWidthPx;
|
||||
private final float mHintPaddingPx;
|
||||
|
||||
@NonNull private final Animator.AnimatorListener mTargetAnimListener;
|
||||
|
||||
private boolean mShouldShowTipHint = false;
|
||||
@NonNull private final Paint mTipHintPaint;
|
||||
@Nullable private AnimatorSet mTipHintAnimatorSet;
|
||||
@Nullable private ValueAnimator mTipHintColorAnimator;
|
||||
@Nullable private ValueAnimator mTipHintWidthAnimator;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener;
|
||||
@NonNull private final Animator.AnimatorListener mTipHintPulseListener;
|
||||
|
||||
private boolean mShouldShowEdgeHint = false;
|
||||
@NonNull private final Paint mEdgeHintPaint;
|
||||
@Nullable private AnimatorSet mEdgeHintAnimatorSet;
|
||||
@Nullable private ValueAnimator mEdgeHintColorAnimator;
|
||||
@Nullable private ValueAnimator mEdgeHintWidthAnimator;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener;
|
||||
@NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener;
|
||||
@NonNull private final Animator.AnimatorListener mEdgeHintPulseListener;
|
||||
|
||||
UdfpsEnrollDrawable(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
|
||||
mSensorOutlinePaint = new Paint(0 /* flags */);
|
||||
mSensorOutlinePaint.setAntiAlias(true);
|
||||
mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
|
||||
@@ -78,6 +120,117 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
mMovingTargetFpIcon.mutate();
|
||||
|
||||
mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
|
||||
|
||||
mHintColorFaded = getHintColorFaded(context);
|
||||
mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
|
||||
mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
|
||||
mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
|
||||
|
||||
mTargetAnimListener = new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
};
|
||||
|
||||
mTipHintPaint = new Paint(0 /* flags */);
|
||||
mTipHintPaint.setAntiAlias(true);
|
||||
mTipHintPaint.setColor(mHintColorFaded);
|
||||
mTipHintPaint.setStyle(Paint.Style.STROKE);
|
||||
mTipHintPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
mTipHintPaint.setStrokeWidth(0f);
|
||||
mTipHintColorUpdateListener = animation -> {
|
||||
mTipHintPaint.setColor((int) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
mTipHintWidthUpdateListener = animation -> {
|
||||
mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
mTipHintPulseListener = new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mHandler.postDelayed(() -> {
|
||||
mTipHintColorAnimator =
|
||||
ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded);
|
||||
mTipHintColorAnimator.setInterpolator(new LinearInterpolator());
|
||||
mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
|
||||
mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
|
||||
mTipHintColorAnimator.start();
|
||||
}, HINT_COLOR_ANIM_DELAY_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
};
|
||||
|
||||
mEdgeHintPaint = new Paint(0 /* flags */);
|
||||
mEdgeHintPaint.setAntiAlias(true);
|
||||
mEdgeHintPaint.setColor(mHintColorFaded);
|
||||
mEdgeHintPaint.setStyle(Paint.Style.STROKE);
|
||||
mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
mEdgeHintPaint.setStrokeWidth(0f);
|
||||
mEdgeHintColorUpdateListener = animation -> {
|
||||
mEdgeHintPaint.setColor((int) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
mEdgeHintWidthUpdateListener = animation -> {
|
||||
mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
mEdgeHintPulseListener = new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mHandler.postDelayed(() -> {
|
||||
mEdgeHintColorAnimator =
|
||||
ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded);
|
||||
mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator());
|
||||
mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
|
||||
mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
|
||||
mEdgeHintColorAnimator.start();
|
||||
}, HINT_COLOR_ANIM_DELAY_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
};
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private static int getHintColorFaded(@NonNull Context context) {
|
||||
final TypedValue tv = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
|
||||
final int alpha = (int) (tv.getFloat() * 255f);
|
||||
|
||||
final int[] attrs = new int[] {android.R.attr.colorControlNormal};
|
||||
final TypedArray ta = context.obtainStyledAttributes(attrs);
|
||||
try {
|
||||
@ColorInt final int color = ta.getColor(0, context.getColor(R.color.white_disabled));
|
||||
return ColorUtils.setAlphaComponent(color, alpha);
|
||||
} finally {
|
||||
ta.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
|
||||
@@ -98,41 +251,154 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
}
|
||||
|
||||
void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
if (mEnrollHelper.isCenterEnrollmentComplete()) {
|
||||
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
|
||||
mAnimatorSet.end();
|
||||
if (mEnrollHelper == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mEnrollHelper.isCenterEnrollmentStage()) {
|
||||
if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
|
||||
mTargetAnimatorSet.end();
|
||||
}
|
||||
|
||||
final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
|
||||
if (mCurrentX != point.x || mCurrentY != point.y) {
|
||||
final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
|
||||
x.addUpdateListener(animation -> {
|
||||
mCurrentX = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
|
||||
x.addUpdateListener(animation -> {
|
||||
mCurrentX = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
|
||||
y.addUpdateListener(animation -> {
|
||||
mCurrentY = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
|
||||
y.addUpdateListener(animation -> {
|
||||
mCurrentY = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
|
||||
final long duration = isMovingToCenter
|
||||
? TARGET_ANIM_DURATION_SHORT
|
||||
: TARGET_ANIM_DURATION_LONG;
|
||||
|
||||
final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
|
||||
scale.setDuration(ANIM_DURATION);
|
||||
scale.addUpdateListener(animation -> {
|
||||
// Grow then shrink
|
||||
mCurrentScale = 1 +
|
||||
SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
});
|
||||
final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
|
||||
scale.setDuration(duration);
|
||||
scale.addUpdateListener(animation -> {
|
||||
// Grow then shrink
|
||||
mCurrentScale = 1
|
||||
+ SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
mAnimatorSet = new AnimatorSet();
|
||||
mTargetAnimatorSet = new AnimatorSet();
|
||||
|
||||
mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
mAnimatorSet.setDuration(ANIM_DURATION);
|
||||
mAnimatorSet.playTogether(x, y, scale);
|
||||
mAnimatorSet.start();
|
||||
mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
mTargetAnimatorSet.setDuration(duration);
|
||||
mTargetAnimatorSet.addListener(mTargetAnimListener);
|
||||
mTargetAnimatorSet.playTogether(x, y, scale);
|
||||
mTargetAnimatorSet.start();
|
||||
} else {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
} else {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
|
||||
updateEdgeHintVisibility();
|
||||
}
|
||||
|
||||
private void updateTipHintVisibility() {
|
||||
final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
|
||||
if (mShouldShowTipHint == shouldShow) {
|
||||
return;
|
||||
}
|
||||
mShouldShowTipHint = shouldShow;
|
||||
|
||||
if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) {
|
||||
mTipHintWidthAnimator.cancel();
|
||||
}
|
||||
|
||||
final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
|
||||
mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth);
|
||||
mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
|
||||
mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener);
|
||||
|
||||
if (shouldShow) {
|
||||
startTipHintPulseAnimation();
|
||||
} else {
|
||||
mTipHintWidthAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEdgeHintVisibility() {
|
||||
final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
|
||||
if (mShouldShowEdgeHint == shouldShow) {
|
||||
return;
|
||||
}
|
||||
mShouldShowEdgeHint = shouldShow;
|
||||
|
||||
if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) {
|
||||
mEdgeHintWidthAnimator.cancel();
|
||||
}
|
||||
|
||||
final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
|
||||
mEdgeHintWidthAnimator =
|
||||
ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth);
|
||||
mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
|
||||
mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener);
|
||||
|
||||
if (shouldShow) {
|
||||
startEdgeHintPulseAnimation();
|
||||
} else {
|
||||
mEdgeHintWidthAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void startTipHintPulseAnimation() {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) {
|
||||
mTipHintAnimatorSet.cancel();
|
||||
}
|
||||
if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) {
|
||||
mTipHintColorAnimator.cancel();
|
||||
}
|
||||
|
||||
mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight);
|
||||
mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
|
||||
mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
|
||||
mTipHintColorAnimator.addListener(mTipHintPulseListener);
|
||||
|
||||
mTipHintAnimatorSet = new AnimatorSet();
|
||||
mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator);
|
||||
mTipHintAnimatorSet.start();
|
||||
}
|
||||
|
||||
private void startEdgeHintPulseAnimation() {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) {
|
||||
mEdgeHintAnimatorSet.cancel();
|
||||
}
|
||||
if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) {
|
||||
mEdgeHintColorAnimator.cancel();
|
||||
}
|
||||
|
||||
mEdgeHintColorAnimator =
|
||||
ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight);
|
||||
mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
|
||||
mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
|
||||
mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener);
|
||||
|
||||
mEdgeHintAnimatorSet = new AnimatorSet();
|
||||
mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator);
|
||||
mEdgeHintAnimatorSet.start();
|
||||
}
|
||||
|
||||
private boolean isTipHintVisible() {
|
||||
return mTipHintPaint.getStrokeWidth() > 0f;
|
||||
}
|
||||
|
||||
private boolean isEdgeHintVisible() {
|
||||
return mEdgeHintPaint.getStrokeWidth() > 0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,7 +408,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
}
|
||||
|
||||
// Draw moving target
|
||||
if (mEnrollHelper.isCenterEnrollmentComplete()) {
|
||||
if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
|
||||
canvas.save();
|
||||
canvas.translate(mCurrentX, mCurrentY);
|
||||
|
||||
@@ -162,6 +428,59 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
|
||||
mFingerprintDrawable.setAlpha(mAlpha);
|
||||
mSensorOutlinePaint.setAlpha(mAlpha);
|
||||
}
|
||||
|
||||
// Draw the finger tip or edges hint.
|
||||
if (isTipHintVisible() || isEdgeHintVisible()) {
|
||||
canvas.save();
|
||||
|
||||
// Make arcs start from the top, rather than the right.
|
||||
canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
|
||||
|
||||
final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f;
|
||||
final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f;
|
||||
final float hintXOffset = halfSensorWidth + mHintPaddingPx;
|
||||
final float hintYOffset = halfSensorHeight + mHintPaddingPx;
|
||||
|
||||
if (isTipHintVisible()) {
|
||||
canvas.drawArc(
|
||||
mSensorRect.centerX() - hintXOffset,
|
||||
mSensorRect.centerY() - hintYOffset,
|
||||
mSensorRect.centerX() + hintXOffset,
|
||||
mSensorRect.centerY() + hintYOffset,
|
||||
-HINT_ANGLE / 2f,
|
||||
HINT_ANGLE,
|
||||
false /* useCenter */,
|
||||
mTipHintPaint);
|
||||
}
|
||||
|
||||
if (isEdgeHintVisible()) {
|
||||
// Draw right edge hint.
|
||||
canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
|
||||
canvas.drawArc(
|
||||
mSensorRect.centerX() - hintXOffset,
|
||||
mSensorRect.centerY() - hintYOffset,
|
||||
mSensorRect.centerX() + hintXOffset,
|
||||
mSensorRect.centerY() + hintYOffset,
|
||||
-HINT_ANGLE / 2f,
|
||||
HINT_ANGLE,
|
||||
false /* useCenter */,
|
||||
mEdgeHintPaint);
|
||||
|
||||
// Draw left edge hint.
|
||||
canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY());
|
||||
canvas.drawArc(
|
||||
mSensorRect.centerX() - hintXOffset,
|
||||
mSensorRect.centerY() - hintYOffset,
|
||||
mSensorRect.centerX() + hintXOffset,
|
||||
mSensorRect.centerY() + hintYOffset,
|
||||
-HINT_ANGLE / 2f,
|
||||
HINT_ANGLE,
|
||||
false /* useCenter */,
|
||||
mEdgeHintPaint);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.hardware.biometrics.BiometricOverlayConstants;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.Settings;
|
||||
@@ -44,16 +45,14 @@ public class UdfpsEnrollHelper {
|
||||
private static final String NEW_COORDS_OVERRIDE =
|
||||
"com.android.systemui.biometrics.UdfpsNewCoords";
|
||||
|
||||
// Enroll with two center touches before going to guided enrollment
|
||||
private static final int NUM_CENTER_TOUCHES = 2;
|
||||
|
||||
interface Listener {
|
||||
void onEnrollmentProgress(int remaining, int totalSteps);
|
||||
void onEnrollmentHelp(int remaining, int totalSteps);
|
||||
void onLastStepAcquired();
|
||||
void onEnrollmentHelp();
|
||||
}
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
@NonNull private final FingerprintManager mFingerprintManager;
|
||||
// IUdfpsOverlayController reason
|
||||
private final int mEnrollReason;
|
||||
private final boolean mAccessibilityEnabled;
|
||||
@@ -66,10 +65,15 @@ public class UdfpsEnrollHelper {
|
||||
// interface makes no promises about monotonically increasing by one each time.
|
||||
private int mLocationsEnrolled = 0;
|
||||
|
||||
private int mCenterTouchCount = 0;
|
||||
|
||||
@Nullable Listener mListener;
|
||||
|
||||
public UdfpsEnrollHelper(@NonNull Context context, int reason) {
|
||||
public UdfpsEnrollHelper(@NonNull Context context,
|
||||
@NonNull FingerprintManager fingerprintManager, int reason) {
|
||||
|
||||
mContext = context;
|
||||
mFingerprintManager = fingerprintManager;
|
||||
mEnrollReason = reason;
|
||||
|
||||
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
|
||||
@@ -118,6 +122,14 @@ public class UdfpsEnrollHelper {
|
||||
}
|
||||
}
|
||||
|
||||
int getStageCount() {
|
||||
return mFingerprintManager.getEnrollStageCount();
|
||||
}
|
||||
|
||||
int getStageThresholdSteps(int totalSteps, int stageIndex) {
|
||||
return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
|
||||
}
|
||||
|
||||
boolean shouldShowProgressBar() {
|
||||
return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
|
||||
}
|
||||
@@ -129,6 +141,9 @@ public class UdfpsEnrollHelper {
|
||||
|
||||
if (remaining != mRemainingSteps) {
|
||||
mLocationsEnrolled++;
|
||||
if (isCenterEnrollmentStage()) {
|
||||
mCenterTouchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
mRemainingSteps = remaining;
|
||||
@@ -140,7 +155,7 @@ public class UdfpsEnrollHelper {
|
||||
|
||||
void onEnrollmentHelp() {
|
||||
if (mListener != null) {
|
||||
mListener.onEnrollmentHelp();
|
||||
mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,19 +170,41 @@ public class UdfpsEnrollHelper {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isCenterEnrollmentComplete() {
|
||||
boolean isCenterEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
} else if (mAccessibilityEnabled) {
|
||||
return true;
|
||||
}
|
||||
return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
|
||||
}
|
||||
|
||||
boolean isGuidedEnrollmentStage() {
|
||||
if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
final int stepsEnrolled = mTotalSteps - mRemainingSteps;
|
||||
return stepsEnrolled >= NUM_CENTER_TOUCHES;
|
||||
final int progressSteps = mTotalSteps - mRemainingSteps;
|
||||
return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
|
||||
&& progressSteps < getStageThresholdSteps(mTotalSteps, 1);
|
||||
}
|
||||
|
||||
boolean isTipEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
final int progressSteps = mTotalSteps - mRemainingSteps;
|
||||
return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
|
||||
&& progressSteps < getStageThresholdSteps(mTotalSteps, 2);
|
||||
}
|
||||
|
||||
boolean isEdgeEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
PointF getNextGuidedEnrollmentPoint() {
|
||||
if (mAccessibilityEnabled) {
|
||||
if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
|
||||
return new PointF(0f, 0f);
|
||||
}
|
||||
|
||||
@@ -177,7 +214,7 @@ public class UdfpsEnrollHelper {
|
||||
SCALE_OVERRIDE, SCALE,
|
||||
UserHandle.USER_CURRENT);
|
||||
}
|
||||
final int index = mLocationsEnrolled - NUM_CENTER_TOUCHES;
|
||||
final int index = mLocationsEnrolled - mCenterTouchCount;
|
||||
final PointF originalPoint = mGuidedEnrollmentPoints
|
||||
.get(index % mGuidedEnrollmentPoints.size());
|
||||
return new PointF(originalPoint.x * scale, originalPoint.y * scale);
|
||||
|
||||
@@ -16,163 +16,129 @@
|
||||
|
||||
package com.android.systemui.biometrics;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.ColorInt;
|
||||
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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.systemui.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UDFPS enrollment progress bar.
|
||||
*/
|
||||
public class UdfpsEnrollProgressBarDrawable extends Drawable {
|
||||
private static final String TAG = "UdfpsProgressBar";
|
||||
|
||||
private static final String TAG = "UdfpsEnrollProgressBarDrawable";
|
||||
|
||||
private static final float PROGRESS_BAR_THICKNESS_DP = 12;
|
||||
private static final float SEGMENT_GAP_ANGLE = 12f;
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
@NonNull private final Paint mBackgroundCirclePaint;
|
||||
@NonNull private final Paint mProgressPaint;
|
||||
|
||||
@Nullable private ValueAnimator mProgressAnimator;
|
||||
@Nullable private ValueAnimator mProgressShowingHelpAnimator;
|
||||
@Nullable private ValueAnimator mProgressHidingHelpAnimator;
|
||||
@ColorInt private final int mProgressColor;
|
||||
@ColorInt private final int mProgressHelpColor;
|
||||
private final int mShortAnimationDuration;
|
||||
private float mProgress;
|
||||
private int mRotation; // After last step, rotate the progress bar once
|
||||
private boolean mLastStepAcquired;
|
||||
@Nullable private UdfpsEnrollHelper mEnrollHelper;
|
||||
@NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
|
||||
private int mTotalSteps = 1;
|
||||
private int mProgressSteps = 0;
|
||||
private boolean mIsShowingHelp = false;
|
||||
|
||||
public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
|
||||
mContext = context;
|
||||
|
||||
mShortAnimationDuration = context.getResources()
|
||||
.getInteger(com.android.internal.R.integer.config_shortAnimTime);
|
||||
mProgressColor = context.getColor(R.color.udfps_enroll_progress);
|
||||
mProgressHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
|
||||
|
||||
mBackgroundCirclePaint = new Paint();
|
||||
mBackgroundCirclePaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
|
||||
mBackgroundCirclePaint.setColor(context.getColor(R.color.white_disabled));
|
||||
mBackgroundCirclePaint.setAntiAlias(true);
|
||||
mBackgroundCirclePaint.setStyle(Paint.Style.STROKE);
|
||||
|
||||
// Background circle color + alpha
|
||||
TypedArray tc = context.obtainStyledAttributes(
|
||||
new int[] {android.R.attr.colorControlNormal});
|
||||
int tintColor = tc.getColor(0, mBackgroundCirclePaint.getColor());
|
||||
mBackgroundCirclePaint.setColor(tintColor);
|
||||
tc.recycle();
|
||||
TypedValue alpha = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
|
||||
mBackgroundCirclePaint.setAlpha((int) (alpha.getFloat() * 255));
|
||||
|
||||
// Progress should not be color extracted
|
||||
mProgressPaint = new Paint();
|
||||
mProgressPaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
|
||||
mProgressPaint.setColor(mProgressColor);
|
||||
mProgressPaint.setAntiAlias(true);
|
||||
mProgressPaint.setStyle(Paint.Style.STROKE);
|
||||
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
void setEnrollmentProgress(int remaining, int totalSteps) {
|
||||
// Add one so that the first steps actually changes progress, but also so that the last
|
||||
// step ends at 1.0
|
||||
final float progress = (totalSteps - remaining + 1) / (float) (totalSteps + 1);
|
||||
setEnrollmentProgress(progress);
|
||||
}
|
||||
|
||||
private void setEnrollmentProgress(float progress) {
|
||||
if (mLastStepAcquired) {
|
||||
return;
|
||||
}
|
||||
|
||||
long animationDuration = mShortAnimationDuration;
|
||||
|
||||
hideEnrollmentHelp();
|
||||
|
||||
if (progress == 1.f) {
|
||||
animationDuration = 400;
|
||||
final ValueAnimator rotationAnimator = ValueAnimator.ofInt(0, 400);
|
||||
rotationAnimator.setDuration(animationDuration);
|
||||
rotationAnimator.addUpdateListener(animation -> {
|
||||
Log.d(TAG, "Rotation: " + mRotation);
|
||||
mRotation = (int) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
rotationAnimator.start();
|
||||
}
|
||||
|
||||
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
||||
mProgressAnimator.cancel();
|
||||
}
|
||||
|
||||
mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress);
|
||||
mProgressAnimator.setDuration(animationDuration);
|
||||
mProgressAnimator.addUpdateListener(animation -> {
|
||||
mProgress = (float) animation.getAnimatedValue();
|
||||
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();
|
||||
});
|
||||
mProgressAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
mTotalSteps = totalSteps;
|
||||
updateState(getProgressSteps(remaining, totalSteps), false /* isShowingHelp */);
|
||||
}
|
||||
|
||||
void onEnrollmentHelp(int remaining, int totalSteps) {
|
||||
updateState(getProgressSteps(remaining, totalSteps), true /* isShowingHelp */);
|
||||
}
|
||||
|
||||
void onLastStepAcquired() {
|
||||
setEnrollmentProgress(1.f);
|
||||
mLastStepAcquired = true;
|
||||
updateState(mTotalSteps, false /* isShowingHelp */);
|
||||
}
|
||||
|
||||
void onEnrollmentHelp() {
|
||||
if (mProgressShowingHelpAnimator != null || mProgressAnimator == null) {
|
||||
return; // already showing or at 0% (no progress bar visible)
|
||||
}
|
||||
|
||||
if (mProgressHidingHelpAnimator != null && mProgressHidingHelpAnimator.isRunning()) {
|
||||
mProgressHidingHelpAnimator.cancel();
|
||||
}
|
||||
mProgressHidingHelpAnimator = null;
|
||||
|
||||
mProgressShowingHelpAnimator = getProgressColorAnimator(
|
||||
mProgressPaint.getColor(), mProgressHelpColor);
|
||||
mProgressShowingHelpAnimator.start();
|
||||
private static int getProgressSteps(int remaining, int totalSteps) {
|
||||
// Show some progress for the initial touch.
|
||||
return Math.max(1, totalSteps - remaining);
|
||||
}
|
||||
|
||||
private void hideEnrollmentHelp() {
|
||||
if (mProgressHidingHelpAnimator != null || mProgressShowingHelpAnimator == null) {
|
||||
return; // already hidden or help never shown
|
||||
}
|
||||
|
||||
if (mProgressShowingHelpAnimator != null && mProgressShowingHelpAnimator.isRunning()) {
|
||||
mProgressShowingHelpAnimator.cancel();
|
||||
}
|
||||
mProgressShowingHelpAnimator = null;
|
||||
|
||||
mProgressHidingHelpAnimator = getProgressColorAnimator(
|
||||
mProgressPaint.getColor(), mProgressColor);
|
||||
mProgressHidingHelpAnimator.start();
|
||||
private void updateState(int progressSteps, boolean isShowingHelp) {
|
||||
updateProgress(progressSteps);
|
||||
updateFillColor(isShowingHelp);
|
||||
}
|
||||
|
||||
private ValueAnimator getProgressColorAnimator(@ColorInt int from, @ColorInt int to) {
|
||||
final ValueAnimator animator = ValueAnimator.ofObject(
|
||||
ArgbEvaluator.getInstance(), from, to);
|
||||
animator.setDuration(mShortAnimationDuration);
|
||||
animator.addUpdateListener(animation -> {
|
||||
mProgressPaint.setColor((int) animation.getAnimatedValue());
|
||||
});
|
||||
return animator;
|
||||
private void updateProgress(int progressSteps) {
|
||||
if (mProgressSteps == progressSteps) {
|
||||
return;
|
||||
}
|
||||
mProgressSteps = progressSteps;
|
||||
|
||||
if (mEnrollHelper == null) {
|
||||
Log.e(TAG, "updateState: UDFPS enroll helper was null");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
index++;
|
||||
prevThreshold = thresholdSteps;
|
||||
}
|
||||
|
||||
if (progressSteps >= mTotalSteps) {
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.startCompletionAnimation();
|
||||
}
|
||||
} else {
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.cancelCompletionAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFillColor(boolean isShowingHelp) {
|
||||
if (mIsShowingHelp == isShowingHelp) {
|
||||
return;
|
||||
}
|
||||
mIsShowingHelp = isShowingHelp;
|
||||
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.updateFillColor(isShowingHelp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -180,43 +146,22 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
|
||||
canvas.save();
|
||||
|
||||
// Progress starts from the top, instead of the right
|
||||
canvas.rotate(-90 + mRotation, getBounds().centerX(), getBounds().centerY());
|
||||
canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
|
||||
|
||||
// Progress bar "background track"
|
||||
final float halfPaddingPx = Utils.dpToPixels(mContext, PROGRESS_BAR_THICKNESS_DP) / 2;
|
||||
canvas.drawArc(halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
getBounds().right - halfPaddingPx,
|
||||
getBounds().bottom - halfPaddingPx,
|
||||
0,
|
||||
360,
|
||||
false,
|
||||
mBackgroundCirclePaint
|
||||
);
|
||||
|
||||
final float progress = 360.f * mProgress;
|
||||
// Progress
|
||||
canvas.drawArc(halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
getBounds().right - halfPaddingPx,
|
||||
getBounds().bottom - halfPaddingPx,
|
||||
0,
|
||||
progress,
|
||||
false,
|
||||
mProgressPaint
|
||||
);
|
||||
// Draw each of the enroll segments.
|
||||
for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
|
||||
segment.draw(canvas);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* 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,23 +73,22 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
|
||||
}
|
||||
|
||||
void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
|
||||
mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
|
||||
mFingerprintDrawable.setEnrollHelper(enrollHelper);
|
||||
}
|
||||
|
||||
void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
mHandler.post(() -> {
|
||||
mFingerprintProgressDrawable.setEnrollmentProgress(remaining, totalSteps);
|
||||
mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
|
||||
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
|
||||
});
|
||||
}
|
||||
|
||||
void onLastStepAcquired() {
|
||||
mHandler.post(() -> {
|
||||
mFingerprintProgressDrawable.onLastStepAcquired();
|
||||
});
|
||||
void onEnrollmentHelp(int remaining, int totalSteps) {
|
||||
mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps));
|
||||
}
|
||||
|
||||
void onEnrollmentHelp() {
|
||||
mHandler.post(mFingerprintProgressDrawable::onEnrollmentHelp);
|
||||
void onLastStepAcquired() {
|
||||
mHandler.post(mFingerprintProgressDrawable::onLastStepAcquired);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,21 +35,21 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
|
||||
@NonNull private final UdfpsEnrollHelper mEnrollHelper;
|
||||
@NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
|
||||
new UdfpsEnrollHelper.Listener() {
|
||||
@Override
|
||||
public void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
mView.onEnrollmentProgress(remaining, totalSteps);
|
||||
}
|
||||
@Override
|
||||
public void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
mView.onEnrollmentProgress(remaining, totalSteps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLastStepAcquired() {
|
||||
mView.onLastStepAcquired();
|
||||
}
|
||||
@Override
|
||||
public void onEnrollmentHelp(int remaining, int totalSteps) {
|
||||
mView.onEnrollmentHelp(remaining, totalSteps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentHelp() {
|
||||
mView.onEnrollmentHelp();
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onLastStepAcquired() {
|
||||
mView.onLastStepAcquired();
|
||||
}
|
||||
};
|
||||
|
||||
protected UdfpsEnrollViewController(
|
||||
@NonNull UdfpsEnrollView view,
|
||||
@@ -81,7 +81,7 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
|
||||
@NonNull
|
||||
@Override
|
||||
public PointF getTouchTranslation() {
|
||||
if (!mEnrollHelper.isCenterEnrollmentComplete()) {
|
||||
if (!mEnrollHelper.isGuidedEnrollmentStage()) {
|
||||
return new PointF(0, 0);
|
||||
} else {
|
||||
return mEnrollHelper.getNextGuidedEnrollmentPoint();
|
||||
|
||||
Reference in New Issue
Block a user