Use custom drawable for UDFPS enrollment progress bar

1) Removes UdfpsProgressBar and associated baggage
2) Creates entirely custom UdfpsEnrollProgressBarDrawable to support
   any required UX going forwards
3) UdfpsEnrollProgressBarDrawable is owned by UdfpsEnrollDrawable.
   UdfpsEnrollDrawable should be the source of truth for any
   UDFPS enrollment animations.

Note: This moves the handler up a few layers to UdfpsEnrollView, so
that child views/drawables receive callbacks on the right thread.

Bug: 187460696
Test: manual

Change-Id: If3362e340de8074e62cb5f69e739c1d687914fad
This commit is contained in:
Kevin Chyn
2021-05-20 16:20:01 -07:00
parent f6ff170d53
commit 7481c73582
12 changed files with 224 additions and 188 deletions

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape
android:innerRadiusRatio="2.2"
android:shape="ring"
android:thickness="@dimen/udfps_enroll_progress_thickness"
android:useLevel="false"
android:tint="?android:colorControlNormal">
<solid android:color="@*android:color/white_disabled_material" />
</shape>
</item>
<item android:id="@android:id/progress">
<rotate
android:fromDegrees="270"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="270">
<shape
android:innerRadiusRatio="2.2"
android:shape="ring"
android:thickness="@dimen/udfps_enroll_progress_thickness"
android:tint="?android:attr/colorControlActivated">
<solid android:color="@android:color/white" />
</shape>
</rotate>
</item>
</layer-list>

View File

@@ -20,17 +20,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Enrollment progress bar -->
<com.android.systemui.biometrics.UdfpsProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:max="100"
android:padding="@dimen/udfps_enroll_progress_thickness"
android:progress="0"
android:layout_gravity="center"
android:visibility="gone"/>
<!-- Fingerprint -->
<ImageView
android:id="@+id/udfps_enroll_animation_fp_view"

View File

@@ -188,6 +188,7 @@
<color name="udfps_enroll_icon">#000000</color> <!-- 100% black -->
<color name="udfps_moving_target_fill">#cc4285f4</color> <!-- 80% blue -->
<color name="udfps_moving_target_stroke">#ff669DF6</color> <!-- 100% blue -->
<color name="udfps_enroll_progress">#ff669DF6</color> <!-- 100% blue -->
<!-- Logout button -->
<color name="logout_button_bg_color">#ccffffff</color>

View File

@@ -630,6 +630,11 @@
58.0001 29.2229,56.9551 26.8945,55.195
</string>
<!-- The radius of the enrollment progress bar, in pixels -->
<integer name="config_udfpsEnrollProgressBar" translatable="false">
360
</integer>
<!-- package name of a built-in camera app to use to restrict implicit intent resolution
when the double-press power gesture is used. Ignored if empty. -->
<string translatable="false" name="config_cameraGesturePackage"></string>

View File

@@ -1188,9 +1188,6 @@
<!-- Y translation for credential contents when animating in -->
<dimen name="biometric_dialog_credential_translation_offset">60dp</dimen>
<!-- UDFPS enrollment progress bar thickness -->
<dimen name="udfps_enroll_progress_thickness">12dp</dimen>
<!-- Wireless Charging Animation values -->
<dimen name="wireless_charging_dots_radius_start">0dp</dimen>
<dimen name="wireless_charging_dots_radius_end">4dp</dimen>

View File

@@ -871,14 +871,6 @@
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="UdfpsProgressBarStyle"
parent="android:style/Widget.Material.ProgressBar.Horizontal">
<item name="android:indeterminate">false</item>
<item name="android:max">10000</item>
<item name="android:mirrorForRtl">false</item>
<item name="android:progressDrawable">@drawable/udfps_progress_bar</item>
</style>
<!-- Wallet styles -->
<style name="Wallet" />

View File

@@ -26,8 +26,6 @@ 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.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.NonNull;
@@ -41,17 +39,15 @@ import com.android.systemui.R;
public class UdfpsEnrollDrawable extends UdfpsDrawable {
private static final String TAG = "UdfpsAnimationEnroll";
static final float PROGRESS_BAR_RADIUS = 360.f;
private static final long ANIM_DURATION = 800;
// 1 + SCALE_MAX is the maximum that the moving target will animate to
private static final float SCALE_MAX = 0.25f;
@NonNull private final UdfpsEnrollProgressBarDrawable mProgressDrawable;
@NonNull private final Drawable mMovingTargetFpIcon;
@NonNull private final Paint mSensorOutlinePaint;
@NonNull private final Paint mBlueFill;
@NonNull private final Paint mBlueStroke;
@NonNull private final Handler mHandler;
@Nullable private RectF mSensorRect;
@Nullable private UdfpsEnrollHelper mEnrollHelper;
@@ -64,11 +60,10 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
// Moving target size
float mCurrentScale = 1.f;
UdfpsEnrollDrawable(@NonNull Context context) {
super(context);
mHandler = new Handler(Looper.getMainLooper());
mProgressDrawable = new UdfpsEnrollProgressBarDrawable(context, this);
mSensorOutlinePaint = new Paint(0 /* flags */);
mSensorOutlinePaint.setAntiAlias(true);
@@ -112,47 +107,49 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
}
void onEnrollmentProgress(int remaining, int totalSteps) {
mProgressDrawable.setEnrollmentProgress(remaining, totalSteps);
if (mEnrollHelper.isCenterEnrollmentComplete()) {
mHandler.post(() -> {
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.end();
}
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.end();
}
final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
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 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();
});
mAnimatorSet = new AnimatorSet();
mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
mAnimatorSet.setDuration(ANIM_DURATION);
mAnimatorSet.playTogether(x, y, scale);
mAnimatorSet.start();
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 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();
});
mAnimatorSet = new AnimatorSet();
mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
mAnimatorSet.setDuration(ANIM_DURATION);
mAnimatorSet.playTogether(x, y, scale);
mAnimatorSet.start();
}
}
@Override
public void draw(@NonNull Canvas canvas) {
mProgressDrawable.draw(canvas);
if (isIlluminationShowing()) {
return;
}
@@ -181,6 +178,11 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
}
}
@Override
public void onBoundsChange(@NonNull Rect rect) {
mProgressDrawable.setBounds(rect);
}
@Override
public void setAlpha(int alpha) {
super.setAlpha(alpha);

View File

@@ -0,0 +1,150 @@
/*
* 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.ColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.R;
/**
* UDFPS enrollment progress bar.
*/
public class UdfpsEnrollProgressBarDrawable extends Drawable {
private static final String TAG = "UdfpsEnrollProgressBarDrawable";
private static final float PROGRESS_BAR_THICKNESS_DP = 12;
@NonNull private final Context mContext;
@NonNull private final UdfpsEnrollDrawable mParent;
@NonNull private final Paint mBackgroundCirclePaint;
@NonNull private final Paint mProgressPaint;
@Nullable private ValueAnimator mProgressAnimator;
private float mProgress;
public UdfpsEnrollProgressBarDrawable(@NonNull Context context,
@NonNull UdfpsEnrollDrawable parent) {
mContext = context;
mParent = parent;
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(context.getColor(R.color.udfps_enroll_progress));
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);
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
mProgressAnimator.cancel();
}
mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress);
mProgressAnimator.setDuration(150);
mProgressAnimator.addUpdateListener(animation -> {
mProgress = (float) animation.getAnimatedValue();
// Use the parent to invalidate, since it's the one that's attached as the view's
// drawable and has its callback set automatically. Invalidating via
// `this.invalidateSelf` actually does not invoke draw(), since this drawable's callback
// is not really set.
mParent.invalidateSelf();
});
mProgressAnimator.start();
}
@Override
public void draw(@NonNull Canvas canvas) {
canvas.save();
// Progress starts from the top, instead of the right
canvas.rotate(-90, 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
);
canvas.restore();
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return 0;
}
}

View File

@@ -17,6 +17,8 @@
package com.android.systemui.biometrics;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.widget.ImageView;
@@ -30,26 +32,25 @@ import com.android.systemui.R;
*/
public class UdfpsEnrollView extends UdfpsAnimationView {
@NonNull private final UdfpsEnrollDrawable mFingerprintDrawable;
@NonNull private final Handler mHandler;
@NonNull private ImageView mFingerprintView;
@NonNull private UdfpsProgressBar mProgressBar;
public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mFingerprintDrawable = new UdfpsEnrollDrawable(mContext);
mHandler = new Handler(Looper.getMainLooper());
}
@Override
protected void updateAlpha() {
super.updateAlpha();
mProgressBar.setAlpha(calculateAlpha());
mProgressBar.getProgressDrawable().setAlpha(calculateAlpha());
}
@Override
protected void onFinishInflate() {
mFingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view);
mFingerprintView.setImageDrawable(mFingerprintDrawable);
mProgressBar = findViewById(R.id.progress_bar);
}
@Override
@@ -62,6 +63,8 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
}
void onEnrollmentProgress(int remaining, int totalSteps) {
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
mHandler.post(() -> {
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
});
}
}

View File

@@ -18,7 +18,6 @@ package com.android.systemui.biometrics;
import android.annotation.NonNull;
import android.graphics.PointF;
import android.view.View;
import com.android.systemui.R;
import com.android.systemui.dump.DumpManager;
@@ -29,8 +28,11 @@ import com.android.systemui.statusbar.phone.StatusBar;
* Class that coordinates non-HBM animations during enrollment.
*/
public class UdfpsEnrollViewController extends UdfpsAnimationViewController<UdfpsEnrollView> {
@NonNull private final UdfpsProgressBar mProgressBar;
private final int mEnrollProgressBarRadius;
@NonNull private final UdfpsEnrollHelper mEnrollHelper;
@NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
mView::onEnrollmentProgress;
protected UdfpsEnrollViewController(
@NonNull UdfpsEnrollView view,
@@ -39,8 +41,9 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
@NonNull StatusBar statusBar,
@NonNull DumpManager dumpManager) {
super(view, statusBarStateController, statusBar, dumpManager);
mEnrollProgressBarRadius = getContext().getResources()
.getInteger(R.integer.config_udfpsEnrollProgressBar);
mEnrollHelper = enrollHelper;
mProgressBar = mView.findViewById(R.id.progress_bar);
mView.setEnrollHelper(mEnrollHelper);
}
@@ -53,8 +56,6 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
protected void onViewAttached() {
super.onViewAttached();
if (mEnrollHelper.shouldShowProgressBar()) {
mProgressBar.setVisibility(View.VISIBLE);
// Only need enrollment updates if the progress bar is showing :)
mEnrollHelper.setListener(mEnrollHelperListener);
}
@@ -78,23 +79,11 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
@Override
public int getPaddingX() {
return (int) Math.ceil(UdfpsEnrollDrawable.PROGRESS_BAR_RADIUS);
return mEnrollProgressBarRadius;
}
@Override
public int getPaddingY() {
return (int) Math.ceil(UdfpsEnrollDrawable.PROGRESS_BAR_RADIUS);
return mEnrollProgressBarRadius;
}
private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
new UdfpsEnrollHelper.Listener() {
@Override
public void onEnrollmentProgress(int remaining, int totalSteps) {
final int interpolatedProgress = mProgressBar.getMax()
* Math.max(0, totalSteps + 1 - remaining) / (totalSteps + 1);
mProgressBar.setProgress(interpolatedProgress, true);
mView.onEnrollmentProgress(remaining, totalSteps);
}
};
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ProgressBar;
import com.android.systemui.R;
/**
* A (determinate) progress bar in the form of a ring. The progress bar goes clockwise starting
* from the 12 o'clock position. This view maintain equal width and height using a strategy similar
* to "centerInside" for ImageView.
*/
public class UdfpsProgressBar extends ProgressBar {
public UdfpsProgressBar(Context context) {
this(context, null);
}
public UdfpsProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public UdfpsProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, R.style.UdfpsProgressBarStyle);
}
public UdfpsProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int measuredHeight = getMeasuredHeight();
final int measuredWidth = getMeasuredWidth();
final int length = Math.min(measuredHeight, measuredWidth);
setMeasuredDimension(length, length);
}
}

View File

@@ -29,6 +29,7 @@ import android.content.pm.PackageManager;
import android.hardware.biometrics.PromptInfo;
import android.hardware.biometrics.SensorPropertiesInternal;
import android.os.UserManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -49,6 +50,16 @@ public class Utils {
@IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD})
@interface CredentialType {}
static float dpToPixels(Context context, float dp) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
static float pixelsToDp(Context context, float pixels) {
return pixels / ((float) context.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
static void notifyAccessibilityContentChanged(AccessibilityManager am, ViewGroup view) {
if (!am.isEnabled()) {
return;