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:
TreeHugger Robot
2021-09-22 07:48:00 +00:00
committed by Automerger Merge Worker
10 changed files with 845 additions and 216 deletions

View File

@@ -146,6 +146,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing
private CryptoObject mCryptoObject; private CryptoObject mCryptoObject;
@Nullable private RemoveTracker mRemoveTracker; @Nullable private RemoveTracker mRemoveTracker;
private Handler mHandler; private Handler mHandler;
@Nullable private float[] mEnrollStageThresholds;
/** /**
* Retrieves a list of properties for all fingerprint sensors on the device. * 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 * @hide
*/ */

View File

@@ -4601,6 +4601,13 @@
<!-- Indicates whether device has a power button fingerprint sensor. --> <!-- Indicates whether device has a power button fingerprint sensor. -->
<bool name="config_is_powerbutton_fps" translatable="false" >false</bool> <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 <!-- 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. 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 --> Entries are defined in android.hardware.biometrics.face@1.0 types.hal -->

View File

@@ -2613,6 +2613,7 @@
<java-symbol type="array" name="config_sfps_sensor_props" /> <java-symbol type="array" name="config_sfps_sensor_props" />
<java-symbol type="integer" name="config_udfps_illumination_transition_ms" /> <java-symbol type="integer" name="config_udfps_illumination_transition_ms" />
<java-symbol type="bool" name="config_is_powerbutton_fps" /> <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_enroll_ignorelist" />
<java-symbol type="array" name="config_face_acquire_vendor_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_title" />
<java-symbol type="string" name="view_and_control_notification_content" /> <java-symbol type="string" name="view_and_control_notification_content" />
<java-symbol type="array" name="config_accessibility_allowed_install_source" /> <java-symbol type="array" name="config_accessibility_allowed_install_source" />
<!-- Translation --> <!-- Translation -->
<java-symbol type="string" name="ui_translation_accessibility_translated_text" /> <java-symbol type="string" name="ui_translation_accessibility_translated_text" />
<java-symbol type="string" name="ui_translation_accessibility_translation_finished" /> <java-symbol type="string" name="ui_translation_accessibility_translation_finished" />

View File

@@ -244,7 +244,7 @@ public class UdfpsController implements DozeReceiver {
final UdfpsEnrollHelper enrollHelper; final UdfpsEnrollHelper enrollHelper;
if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
|| reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) { || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) {
enrollHelper = new UdfpsEnrollHelper(mContext, reason); enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason);
} else { } else {
enrollHelper = null; enrollHelper = null;
} }

View File

@@ -16,9 +16,11 @@
package com.android.systemui.biometrics; package com.android.systemui.biometrics;
import android.animation.Animator;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
@@ -26,11 +28,17 @@ import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.drawable.Drawable; 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.AccelerateDecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.android.internal.graphics.ColorUtils;
import com.android.systemui.R; import com.android.systemui.R;
/** /**
@@ -39,10 +47,20 @@ import com.android.systemui.R;
public class UdfpsEnrollDrawable extends UdfpsDrawable { public class UdfpsEnrollDrawable extends UdfpsDrawable {
private static final String TAG = "UdfpsAnimationEnroll"; 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 // 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 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 Drawable mMovingTargetFpIcon;
@NonNull private final Paint mSensorOutlinePaint; @NonNull private final Paint mSensorOutlinePaint;
@NonNull private final Paint mBlueFill; @NonNull private final Paint mBlueFill;
@@ -51,17 +69,41 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
@Nullable private UdfpsEnrollHelper mEnrollHelper; @Nullable private UdfpsEnrollHelper mEnrollHelper;
// Moving target animator set // Moving target animator set
@Nullable AnimatorSet mAnimatorSet; @Nullable AnimatorSet mTargetAnimatorSet;
// Moving target location // Moving target location
float mCurrentX; float mCurrentX;
float mCurrentY; float mCurrentY;
// Moving target size // Moving target size
float mCurrentScale = 1.f; 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) { UdfpsEnrollDrawable(@NonNull Context context) {
super(context); super(context);
mSensorOutlinePaint = new Paint(0 /* flags */); mSensorOutlinePaint = new Paint(0 /* flags */);
mSensorOutlinePaint.setAntiAlias(true); mSensorOutlinePaint.setAntiAlias(true);
mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon)); mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
@@ -78,6 +120,117 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
mMovingTargetFpIcon.mutate(); mMovingTargetFpIcon.mutate();
mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon)); 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) { void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
@@ -98,41 +251,154 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
} }
void onEnrollmentProgress(int remaining, int totalSteps) { void onEnrollmentProgress(int remaining, int totalSteps) {
if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper == null) {
if (mAnimatorSet != null && mAnimatorSet.isRunning()) { return;
mAnimatorSet.end(); }
if (!mEnrollHelper.isCenterEnrollmentStage()) {
if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
mTargetAnimatorSet.end();
} }
final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint(); 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); final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
x.addUpdateListener(animation -> { y.addUpdateListener(animation -> {
mCurrentX = (float) animation.getAnimatedValue(); mCurrentY = (float) animation.getAnimatedValue();
invalidateSelf(); invalidateSelf();
}); });
final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y); final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
y.addUpdateListener(animation -> { final long duration = isMovingToCenter
mCurrentY = (float) animation.getAnimatedValue(); ? TARGET_ANIM_DURATION_SHORT
invalidateSelf(); : TARGET_ANIM_DURATION_LONG;
});
final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI); final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
scale.setDuration(ANIM_DURATION); scale.setDuration(duration);
scale.addUpdateListener(animation -> { scale.addUpdateListener(animation -> {
// Grow then shrink // Grow then shrink
mCurrentScale = 1 + mCurrentScale = 1
SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
invalidateSelf(); invalidateSelf();
}); });
mAnimatorSet = new AnimatorSet(); mTargetAnimatorSet = new AnimatorSet();
mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
mAnimatorSet.setDuration(ANIM_DURATION); mTargetAnimatorSet.setDuration(duration);
mAnimatorSet.playTogether(x, y, scale); mTargetAnimatorSet.addListener(mTargetAnimListener);
mAnimatorSet.start(); 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 @Override
@@ -142,7 +408,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
} }
// Draw moving target // Draw moving target
if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
canvas.save(); canvas.save();
canvas.translate(mCurrentX, mCurrentY); canvas.translate(mCurrentX, mCurrentY);
@@ -162,6 +428,59 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
mFingerprintDrawable.setAlpha(mAlpha); mFingerprintDrawable.setAlpha(mAlpha);
mSensorOutlinePaint.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 @Override

View File

@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.content.Context; import android.content.Context;
import android.graphics.PointF; import android.graphics.PointF;
import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.BiometricOverlayConstants;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build; import android.os.Build;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings; import android.provider.Settings;
@@ -44,16 +45,14 @@ public class UdfpsEnrollHelper {
private static final String NEW_COORDS_OVERRIDE = private static final String NEW_COORDS_OVERRIDE =
"com.android.systemui.biometrics.UdfpsNewCoords"; "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 { interface Listener {
void onEnrollmentProgress(int remaining, int totalSteps); void onEnrollmentProgress(int remaining, int totalSteps);
void onEnrollmentHelp(int remaining, int totalSteps);
void onLastStepAcquired(); void onLastStepAcquired();
void onEnrollmentHelp();
} }
@NonNull private final Context mContext; @NonNull private final Context mContext;
@NonNull private final FingerprintManager mFingerprintManager;
// IUdfpsOverlayController reason // IUdfpsOverlayController reason
private final int mEnrollReason; private final int mEnrollReason;
private final boolean mAccessibilityEnabled; private final boolean mAccessibilityEnabled;
@@ -66,10 +65,15 @@ public class UdfpsEnrollHelper {
// interface makes no promises about monotonically increasing by one each time. // interface makes no promises about monotonically increasing by one each time.
private int mLocationsEnrolled = 0; private int mLocationsEnrolled = 0;
private int mCenterTouchCount = 0;
@Nullable Listener mListener; @Nullable Listener mListener;
public UdfpsEnrollHelper(@NonNull Context context, int reason) { public UdfpsEnrollHelper(@NonNull Context context,
@NonNull FingerprintManager fingerprintManager, int reason) {
mContext = context; mContext = context;
mFingerprintManager = fingerprintManager;
mEnrollReason = reason; mEnrollReason = reason;
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); 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() { boolean shouldShowProgressBar() {
return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING; return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
} }
@@ -129,6 +141,9 @@ public class UdfpsEnrollHelper {
if (remaining != mRemainingSteps) { if (remaining != mRemainingSteps) {
mLocationsEnrolled++; mLocationsEnrolled++;
if (isCenterEnrollmentStage()) {
mCenterTouchCount++;
}
} }
mRemainingSteps = remaining; mRemainingSteps = remaining;
@@ -140,7 +155,7 @@ public class UdfpsEnrollHelper {
void onEnrollmentHelp() { void onEnrollmentHelp() {
if (mListener != null) { 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) { if (mTotalSteps == -1 || mRemainingSteps == -1) {
return false; return true;
} else if (mAccessibilityEnabled) { }
return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
}
boolean isGuidedEnrollmentStage() {
if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
return false; return false;
} }
final int stepsEnrolled = mTotalSteps - mRemainingSteps; final int progressSteps = mTotalSteps - mRemainingSteps;
return stepsEnrolled >= NUM_CENTER_TOUCHES; 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 @NonNull
PointF getNextGuidedEnrollmentPoint() { PointF getNextGuidedEnrollmentPoint() {
if (mAccessibilityEnabled) { if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
return new PointF(0f, 0f); return new PointF(0f, 0f);
} }
@@ -177,7 +214,7 @@ public class UdfpsEnrollHelper {
SCALE_OVERRIDE, SCALE, SCALE_OVERRIDE, SCALE,
UserHandle.USER_CURRENT); UserHandle.USER_CURRENT);
} }
final int index = mLocationsEnrolled - NUM_CENTER_TOUCHES; final int index = mLocationsEnrolled - mCenterTouchCount;
final PointF originalPoint = mGuidedEnrollmentPoints final PointF originalPoint = mGuidedEnrollmentPoints
.get(index % mGuidedEnrollmentPoints.size()); .get(index % mGuidedEnrollmentPoints.size());
return new PointF(originalPoint.x * scale, originalPoint.y * scale); return new PointF(originalPoint.x * scale, originalPoint.y * scale);

View File

@@ -16,163 +16,129 @@
package com.android.systemui.biometrics; package com.android.systemui.biometrics;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.Log; import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.android.systemui.R; import java.util.ArrayList;
import java.util.List;
/** /**
* UDFPS enrollment progress bar. * UDFPS enrollment progress bar.
*/ */
public class UdfpsEnrollProgressBarDrawable extends Drawable { public class UdfpsEnrollProgressBarDrawable extends Drawable {
private static final String TAG = "UdfpsProgressBar";
private static final String TAG = "UdfpsEnrollProgressBarDrawable"; private static final float SEGMENT_GAP_ANGLE = 12f;
private static final float PROGRESS_BAR_THICKNESS_DP = 12;
@NonNull private final Context mContext; @NonNull private final Context mContext;
@NonNull private final Paint mBackgroundCirclePaint;
@NonNull private final Paint mProgressPaint;
@Nullable private ValueAnimator mProgressAnimator; @Nullable private UdfpsEnrollHelper mEnrollHelper;
@Nullable private ValueAnimator mProgressShowingHelpAnimator; @NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
@Nullable private ValueAnimator mProgressHidingHelpAnimator; private int mTotalSteps = 1;
@ColorInt private final int mProgressColor; private int mProgressSteps = 0;
@ColorInt private final int mProgressHelpColor; private boolean mIsShowingHelp = false;
private final int mShortAnimationDuration;
private float mProgress;
private int mRotation; // After last step, rotate the progress bar once
private boolean mLastStepAcquired;
public UdfpsEnrollProgressBarDrawable(@NonNull Context context) { public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
mContext = 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) { void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
// Add one so that the first steps actually changes progress, but also so that the last mEnrollHelper = enrollHelper;
// step ends at 1.0 if (enrollHelper != null) {
final float progress = (totalSteps - remaining + 1) / (float) (totalSteps + 1); final int stageCount = enrollHelper.getStageCount();
setEnrollmentProgress(progress); mSegments = new ArrayList<>(stageCount);
} float startAngle = SEGMENT_GAP_ANGLE / 2f;
final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
private void setEnrollmentProgress(float progress) { final Runnable invalidateRunnable = this::invalidateSelf;
if (mLastStepAcquired) { for (int index = 0; index < stageCount; index++) {
return; mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
} sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
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();
invalidateSelf(); 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() { void onLastStepAcquired() {
setEnrollmentProgress(1.f); updateState(mTotalSteps, false /* isShowingHelp */);
mLastStepAcquired = true;
} }
void onEnrollmentHelp() { private static int getProgressSteps(int remaining, int totalSteps) {
if (mProgressShowingHelpAnimator != null || mProgressAnimator == null) { // Show some progress for the initial touch.
return; // already showing or at 0% (no progress bar visible) return Math.max(1, totalSteps - remaining);
}
if (mProgressHidingHelpAnimator != null && mProgressHidingHelpAnimator.isRunning()) {
mProgressHidingHelpAnimator.cancel();
}
mProgressHidingHelpAnimator = null;
mProgressShowingHelpAnimator = getProgressColorAnimator(
mProgressPaint.getColor(), mProgressHelpColor);
mProgressShowingHelpAnimator.start();
} }
private void hideEnrollmentHelp() { private void updateState(int progressSteps, boolean isShowingHelp) {
if (mProgressHidingHelpAnimator != null || mProgressShowingHelpAnimator == null) { updateProgress(progressSteps);
return; // already hidden or help never shown updateFillColor(isShowingHelp);
}
if (mProgressShowingHelpAnimator != null && mProgressShowingHelpAnimator.isRunning()) {
mProgressShowingHelpAnimator.cancel();
}
mProgressShowingHelpAnimator = null;
mProgressHidingHelpAnimator = getProgressColorAnimator(
mProgressPaint.getColor(), mProgressColor);
mProgressHidingHelpAnimator.start();
} }
private ValueAnimator getProgressColorAnimator(@ColorInt int from, @ColorInt int to) { private void updateProgress(int progressSteps) {
final ValueAnimator animator = ValueAnimator.ofObject( if (mProgressSteps == progressSteps) {
ArgbEvaluator.getInstance(), from, to); return;
animator.setDuration(mShortAnimationDuration); }
animator.addUpdateListener(animation -> { mProgressSteps = progressSteps;
mProgressPaint.setColor((int) animation.getAnimatedValue());
}); if (mEnrollHelper == null) {
return animator; 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 @Override
@@ -180,43 +146,22 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
canvas.save(); canvas.save();
// Progress starts from the top, instead of the right // 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" // Draw each of the enroll segments.
final float halfPaddingPx = Utils.dpToPixels(mContext, PROGRESS_BAR_THICKNESS_DP) / 2; for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
canvas.drawArc(halfPaddingPx, segment.draw(canvas);
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
);
canvas.restore(); canvas.restore();
} }
@Override @Override
public void setAlpha(int alpha) { public void setAlpha(int alpha) {
} }
@Override @Override
public void setColorFilter(@Nullable ColorFilter colorFilter) { public void setColorFilter(@Nullable ColorFilter colorFilter) {
} }
@Override @Override

View File

@@ -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();
}
}
}

View File

@@ -73,23 +73,22 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
} }
void setEnrollHelper(UdfpsEnrollHelper enrollHelper) { void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
mFingerprintDrawable.setEnrollHelper(enrollHelper); mFingerprintDrawable.setEnrollHelper(enrollHelper);
} }
void onEnrollmentProgress(int remaining, int totalSteps) { void onEnrollmentProgress(int remaining, int totalSteps) {
mHandler.post(() -> { mHandler.post(() -> {
mFingerprintProgressDrawable.setEnrollmentProgress(remaining, totalSteps); mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps); mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
}); });
} }
void onLastStepAcquired() { void onEnrollmentHelp(int remaining, int totalSteps) {
mHandler.post(() -> { mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps));
mFingerprintProgressDrawable.onLastStepAcquired();
});
} }
void onEnrollmentHelp() { void onLastStepAcquired() {
mHandler.post(mFingerprintProgressDrawable::onEnrollmentHelp); mHandler.post(mFingerprintProgressDrawable::onLastStepAcquired);
} }
} }

View File

@@ -35,21 +35,21 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
@NonNull private final UdfpsEnrollHelper mEnrollHelper; @NonNull private final UdfpsEnrollHelper mEnrollHelper;
@NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener = @NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
new UdfpsEnrollHelper.Listener() { new UdfpsEnrollHelper.Listener() {
@Override @Override
public void onEnrollmentProgress(int remaining, int totalSteps) { public void onEnrollmentProgress(int remaining, int totalSteps) {
mView.onEnrollmentProgress(remaining, totalSteps); mView.onEnrollmentProgress(remaining, totalSteps);
} }
@Override @Override
public void onLastStepAcquired() { public void onEnrollmentHelp(int remaining, int totalSteps) {
mView.onLastStepAcquired(); mView.onEnrollmentHelp(remaining, totalSteps);
} }
@Override @Override
public void onEnrollmentHelp() { public void onLastStepAcquired() {
mView.onEnrollmentHelp(); mView.onLastStepAcquired();
} }
}; };
protected UdfpsEnrollViewController( protected UdfpsEnrollViewController(
@NonNull UdfpsEnrollView view, @NonNull UdfpsEnrollView view,
@@ -81,7 +81,7 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
@NonNull @NonNull
@Override @Override
public PointF getTouchTranslation() { public PointF getTouchTranslation() {
if (!mEnrollHelper.isCenterEnrollmentComplete()) { if (!mEnrollHelper.isGuidedEnrollmentStage()) {
return new PointF(0, 0); return new PointF(0, 0);
} else { } else {
return mEnrollHelper.getNextGuidedEnrollmentPoint(); return mEnrollHelper.getNextGuidedEnrollmentPoint();