Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +1 −0 Original line number Diff line number Diff line Loading @@ -210,6 +210,7 @@ public class UdfpsController implements DozeReceiver { } void onAcquiredGood() { Log.d(TAG, "onAcquiredGood"); if (mEnrollHelper != null) { mEnrollHelper.animateIfLastStep(); } Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java +31 −30 Original line number Diff line number Diff line Loading @@ -98,13 +98,13 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { } void onEnrollmentProgress(int remaining, int totalSteps) { if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { if (mAnimatorSet != null && mAnimatorSet.isRunning()) { mAnimatorSet.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(); Loading @@ -121,8 +121,8 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { scale.setDuration(ANIM_DURATION); scale.addUpdateListener(animation -> { // Grow then shrink mCurrentScale = 1 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); mCurrentScale = 1 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); invalidateSelf(); }); Loading @@ -134,6 +134,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { mAnimatorSet.start(); } } } @Override public void draw(@NonNull Canvas canvas) { Loading @@ -142,7 +143,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { } // Draw moving target if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { canvas.save(); canvas.translate(mCurrentX, mCurrentY); Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java +53 −12 Original line number Diff line number Diff line Loading @@ -44,8 +44,15 @@ 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; static final int ENROLL_STAGE_COUNT = 4; // TODO(b/198928407): Consolidate with FingerprintEnrollEnrolling private static final int[] STAGE_THRESHOLDS = new int[] { 2, // center 18, // guided 22, // fingertip 38, // edges }; interface Listener { void onEnrollmentProgress(int remaining, int totalSteps); Loading @@ -65,6 +72,8 @@ 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) { Loading Loading @@ -117,17 +126,43 @@ public class UdfpsEnrollHelper { } } static int getStageThreshold(int index) { return STAGE_THRESHOLDS[index]; } static int getLastStageThreshold() { return STAGE_THRESHOLDS[ENROLL_STAGE_COUNT - 1]; } boolean shouldShowProgressBar() { return mEnrollReason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING; } void onEnrollmentProgress(int remaining) { if (mTotalSteps == -1) { mTotalSteps = remaining; } Log.d(TAG, "onEnrollmentProgress: remaining = " + remaining + ", mRemainingSteps = " + mRemainingSteps + ", mTotalSteps = " + mTotalSteps + ", mLocationsEnrolled = " + mLocationsEnrolled + ", mCenterTouchCount = " + mCenterTouchCount); if (remaining != mRemainingSteps) { mLocationsEnrolled++; if (isCenterEnrollmentStage()) { mCenterTouchCount++; } } if (mTotalSteps == -1) { mTotalSteps = remaining; // Allocate (or subtract) any extra steps for the first enroll stage. final int extraSteps = mTotalSteps - getLastStageThreshold(); if (extraSteps != 0) { for (int stageIndex = 0; stageIndex < ENROLL_STAGE_COUNT; stageIndex++) { STAGE_THRESHOLDS[stageIndex] = Math.max(0, STAGE_THRESHOLDS[stageIndex] + extraSteps); } } } mRemainingSteps = remaining; Loading @@ -152,19 +187,24 @@ public class UdfpsEnrollHelper { } } boolean isCenterEnrollmentComplete() { boolean isCenterEnrollmentStage() { if (mTotalSteps == -1 || mRemainingSteps == -1) { return false; } else if (mAccessibilityEnabled) { return true; } return mTotalSteps - mRemainingSteps < STAGE_THRESHOLDS[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 >= STAGE_THRESHOLDS[0] && progressSteps < STAGE_THRESHOLDS[1]; } @NonNull PointF getNextGuidedEnrollmentPoint() { if (mAccessibilityEnabled) { if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) { return new PointF(0f, 0f); } Loading @@ -174,13 +214,14 @@ 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); } void animateIfLastStep() { Log.d(TAG, "animateIfLastStep: mRemainingSteps = " + mRemainingSteps); if (mListener == null) { Log.e(TAG, "animateIfLastStep, null listener"); return; Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java +65 −99 Original line number Diff line number Diff line Loading @@ -16,141 +16,107 @@ package com.android.systemui.biometrics; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.util.Log; import android.util.TypedValue; import 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 SEGMENT_GAP_ANGLE = 12f; private static final float PROGRESS_BAR_THICKNESS_DP = 12; @NonNull private final Context mContext; @NonNull private final Paint mBackgroundCirclePaint; @NonNull private final Paint mProgressPaint; @Nullable private ValueAnimator mProgressAnimator; private float mProgress; private int mRotation; // After last step, rotate the progress bar once private boolean mLastStepAcquired; @NonNull private final List<UdfpsEnrollProgressBarSegment> mSegments; public UdfpsEnrollProgressBarDrawable(@NonNull Context context) { mContext = context; 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); mSegments = new ArrayList<>(UdfpsEnrollHelper.ENROLL_STAGE_COUNT); float startAngle = SEGMENT_GAP_ANGLE / 2f; final float sweepAngle = (360f / UdfpsEnrollHelper.ENROLL_STAGE_COUNT) - SEGMENT_GAP_ANGLE; final Runnable invalidateRunnable = this::invalidateSelf; for (int index = 0; index < UdfpsEnrollHelper.ENROLL_STAGE_COUNT; index++) { mSegments.add(new UdfpsEnrollProgressBarSegment(context, getBounds(), startAngle, sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable)); startAngle += sweepAngle + SEGMENT_GAP_ANGLE; } } 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); if (remaining == totalSteps) { // Show some progress for the initial touch. setEnrollmentProgress(1); } else { setEnrollmentProgress(totalSteps - remaining); } private void setEnrollmentProgress(float progress) { if (mLastStepAcquired) { return; } long animationDuration = 150; 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(); private void setEnrollmentProgress(int progressSteps) { Log.d(TAG, "setEnrollmentProgress: progressSteps = " + progressSteps); int segmentIndex = 0; int prevThreshold = 0; while (segmentIndex < mSegments.size()) { final UdfpsEnrollProgressBarSegment segment = mSegments.get(segmentIndex); final int threshold = UdfpsEnrollHelper.getStageThreshold(segmentIndex); if (progressSteps >= threshold && !segment.isFilledOrFilling()) { Log.d(TAG, "setEnrollmentProgress: segment[" + segmentIndex + "] complete"); segment.updateProgress(1f); break; } else if (progressSteps >= prevThreshold && progressSteps < threshold) { final int relativeSteps = progressSteps - prevThreshold; final int relativeThreshold = threshold - prevThreshold; final float segmentProgress = (float) relativeSteps / (float) relativeThreshold; Log.d(TAG, "setEnrollmentProgress: segment[" + segmentIndex + "] progress = " + segmentProgress); segment.updateProgress(segmentProgress); break; } if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); segmentIndex++; prevThreshold = threshold; } mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress); mProgressAnimator.setDuration(animationDuration); mProgressAnimator.addUpdateListener(animation -> { mProgress = (float) animation.getAnimatedValue(); invalidateSelf(); }); mProgressAnimator.start(); if (progressSteps >= UdfpsEnrollHelper.getLastStageThreshold()) { Log.d(TAG, "setEnrollmentProgress: startCompletionAnimation"); for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.startCompletionAnimation(); } } else { Log.d(TAG, "setEnrollmentProgress: cancelCompletionAnimation"); for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.cancelCompletionAnimation(); } } } void onLastStepAcquired() { setEnrollmentProgress(1.f); mLastStepAcquired = true; Log.d(TAG, "setEnrollmentProgress: onLastStepAcquired"); setEnrollmentProgress(UdfpsEnrollHelper.getLastStageThreshold()); } @Override public void draw(@NonNull Canvas canvas) { Log.d(TAG, "setEnrollmentProgress: draw"); canvas.save(); // Progress starts from the top, instead of the right canvas.rotate(-90 + mRotation, 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.rotate(-90f, getBounds().centerX(), getBounds().centerY()); // Draw each of the enroll segments. for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.draw(canvas); } canvas.restore(); } Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java 0 → 100644 +256 −0 Original line number Diff line number Diff line /* * 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 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; @NonNull private final Paint mBackgroundPaint; @NonNull private final Paint mProgressPaint; private boolean mIsFilledOrFilling = false; private float mProgress = 0f; @Nullable private ValueAnimator mProgressAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener; 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); 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(context.getColor(R.color.udfps_enroll_progress)); mProgressPaint.setAntiAlias(true); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); mProgressUpdateListener = animation -> { mProgress = (float) 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) { Log.d(TAG, "draw: mProgress = " + mProgress); final float halfPaddingPx = mStrokeWidthPx / 2f; if (mProgress < 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 (mProgress > 0f) { // Draw the filled progress portion of the segment. canvas.drawArc( halfPaddingPx, halfPaddingPx, mBounds.right - halfPaddingPx, mBounds.bottom - halfPaddingPx, mStartAngle, mSweepAngle * mProgress + mOverSweepAngle, false /* useCenter */, mProgressPaint); } } /** * @return Whether this segment is filled or in the process of being filled. */ public boolean isFilledOrFilling() { return mIsFilledOrFilling; } /** * 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) { Log.d(TAG, "updateProgress: progress = " + progress + ", duration = " + animationDurationMs); if (mProgress == progress) { Log.d(TAG, "updateProgress skipped: progress == mProgress"); return; } mIsFilledOrFilling = progress >= 1f; if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); } mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress); mProgressAnimator.setDuration(animationDurationMs); mProgressAnimator.addUpdateListener(mProgressUpdateListener); mProgressAnimator.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; } Log.d(TAG, "startCompletionAnimation: mProgress = " + mProgress + ", mOverSweepAngle = " + mOverSweepAngle); // Reset sweep angle back to zero if the animation is being rolled back. if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) { mOverSweepReverseAnimator.cancel(); mOverSweepAngle = 0f; } // Start filling the segment if it isn't already. if (mProgress < 1f) { updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS); } // 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() { Log.d(TAG, "cancelCompletionAnimation: mProgress = " + mProgress + ", mOverSweepAngle = " + mOverSweepAngle); // 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(); } } } Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +1 −0 Original line number Diff line number Diff line Loading @@ -210,6 +210,7 @@ public class UdfpsController implements DozeReceiver { } void onAcquiredGood() { Log.d(TAG, "onAcquiredGood"); if (mEnrollHelper != null) { mEnrollHelper.animateIfLastStep(); } Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java +31 −30 Original line number Diff line number Diff line Loading @@ -98,13 +98,13 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { } void onEnrollmentProgress(int remaining, int totalSteps) { if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { if (mAnimatorSet != null && mAnimatorSet.isRunning()) { mAnimatorSet.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(); Loading @@ -121,8 +121,8 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { scale.setDuration(ANIM_DURATION); scale.addUpdateListener(animation -> { // Grow then shrink mCurrentScale = 1 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); mCurrentScale = 1 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); invalidateSelf(); }); Loading @@ -134,6 +134,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { mAnimatorSet.start(); } } } @Override public void draw(@NonNull Canvas canvas) { Loading @@ -142,7 +143,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { } // Draw moving target if (mEnrollHelper.isCenterEnrollmentComplete()) { if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { canvas.save(); canvas.translate(mCurrentX, mCurrentY); Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java +53 −12 Original line number Diff line number Diff line Loading @@ -44,8 +44,15 @@ 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; static final int ENROLL_STAGE_COUNT = 4; // TODO(b/198928407): Consolidate with FingerprintEnrollEnrolling private static final int[] STAGE_THRESHOLDS = new int[] { 2, // center 18, // guided 22, // fingertip 38, // edges }; interface Listener { void onEnrollmentProgress(int remaining, int totalSteps); Loading @@ -65,6 +72,8 @@ 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) { Loading Loading @@ -117,17 +126,43 @@ public class UdfpsEnrollHelper { } } static int getStageThreshold(int index) { return STAGE_THRESHOLDS[index]; } static int getLastStageThreshold() { return STAGE_THRESHOLDS[ENROLL_STAGE_COUNT - 1]; } boolean shouldShowProgressBar() { return mEnrollReason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING; } void onEnrollmentProgress(int remaining) { if (mTotalSteps == -1) { mTotalSteps = remaining; } Log.d(TAG, "onEnrollmentProgress: remaining = " + remaining + ", mRemainingSteps = " + mRemainingSteps + ", mTotalSteps = " + mTotalSteps + ", mLocationsEnrolled = " + mLocationsEnrolled + ", mCenterTouchCount = " + mCenterTouchCount); if (remaining != mRemainingSteps) { mLocationsEnrolled++; if (isCenterEnrollmentStage()) { mCenterTouchCount++; } } if (mTotalSteps == -1) { mTotalSteps = remaining; // Allocate (or subtract) any extra steps for the first enroll stage. final int extraSteps = mTotalSteps - getLastStageThreshold(); if (extraSteps != 0) { for (int stageIndex = 0; stageIndex < ENROLL_STAGE_COUNT; stageIndex++) { STAGE_THRESHOLDS[stageIndex] = Math.max(0, STAGE_THRESHOLDS[stageIndex] + extraSteps); } } } mRemainingSteps = remaining; Loading @@ -152,19 +187,24 @@ public class UdfpsEnrollHelper { } } boolean isCenterEnrollmentComplete() { boolean isCenterEnrollmentStage() { if (mTotalSteps == -1 || mRemainingSteps == -1) { return false; } else if (mAccessibilityEnabled) { return true; } return mTotalSteps - mRemainingSteps < STAGE_THRESHOLDS[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 >= STAGE_THRESHOLDS[0] && progressSteps < STAGE_THRESHOLDS[1]; } @NonNull PointF getNextGuidedEnrollmentPoint() { if (mAccessibilityEnabled) { if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) { return new PointF(0f, 0f); } Loading @@ -174,13 +214,14 @@ 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); } void animateIfLastStep() { Log.d(TAG, "animateIfLastStep: mRemainingSteps = " + mRemainingSteps); if (mListener == null) { Log.e(TAG, "animateIfLastStep, null listener"); return; Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java +65 −99 Original line number Diff line number Diff line Loading @@ -16,141 +16,107 @@ package com.android.systemui.biometrics; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.util.Log; import android.util.TypedValue; import 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 SEGMENT_GAP_ANGLE = 12f; private static final float PROGRESS_BAR_THICKNESS_DP = 12; @NonNull private final Context mContext; @NonNull private final Paint mBackgroundCirclePaint; @NonNull private final Paint mProgressPaint; @Nullable private ValueAnimator mProgressAnimator; private float mProgress; private int mRotation; // After last step, rotate the progress bar once private boolean mLastStepAcquired; @NonNull private final List<UdfpsEnrollProgressBarSegment> mSegments; public UdfpsEnrollProgressBarDrawable(@NonNull Context context) { mContext = context; 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); mSegments = new ArrayList<>(UdfpsEnrollHelper.ENROLL_STAGE_COUNT); float startAngle = SEGMENT_GAP_ANGLE / 2f; final float sweepAngle = (360f / UdfpsEnrollHelper.ENROLL_STAGE_COUNT) - SEGMENT_GAP_ANGLE; final Runnable invalidateRunnable = this::invalidateSelf; for (int index = 0; index < UdfpsEnrollHelper.ENROLL_STAGE_COUNT; index++) { mSegments.add(new UdfpsEnrollProgressBarSegment(context, getBounds(), startAngle, sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable)); startAngle += sweepAngle + SEGMENT_GAP_ANGLE; } } 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); if (remaining == totalSteps) { // Show some progress for the initial touch. setEnrollmentProgress(1); } else { setEnrollmentProgress(totalSteps - remaining); } private void setEnrollmentProgress(float progress) { if (mLastStepAcquired) { return; } long animationDuration = 150; 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(); private void setEnrollmentProgress(int progressSteps) { Log.d(TAG, "setEnrollmentProgress: progressSteps = " + progressSteps); int segmentIndex = 0; int prevThreshold = 0; while (segmentIndex < mSegments.size()) { final UdfpsEnrollProgressBarSegment segment = mSegments.get(segmentIndex); final int threshold = UdfpsEnrollHelper.getStageThreshold(segmentIndex); if (progressSteps >= threshold && !segment.isFilledOrFilling()) { Log.d(TAG, "setEnrollmentProgress: segment[" + segmentIndex + "] complete"); segment.updateProgress(1f); break; } else if (progressSteps >= prevThreshold && progressSteps < threshold) { final int relativeSteps = progressSteps - prevThreshold; final int relativeThreshold = threshold - prevThreshold; final float segmentProgress = (float) relativeSteps / (float) relativeThreshold; Log.d(TAG, "setEnrollmentProgress: segment[" + segmentIndex + "] progress = " + segmentProgress); segment.updateProgress(segmentProgress); break; } if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); segmentIndex++; prevThreshold = threshold; } mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress); mProgressAnimator.setDuration(animationDuration); mProgressAnimator.addUpdateListener(animation -> { mProgress = (float) animation.getAnimatedValue(); invalidateSelf(); }); mProgressAnimator.start(); if (progressSteps >= UdfpsEnrollHelper.getLastStageThreshold()) { Log.d(TAG, "setEnrollmentProgress: startCompletionAnimation"); for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.startCompletionAnimation(); } } else { Log.d(TAG, "setEnrollmentProgress: cancelCompletionAnimation"); for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.cancelCompletionAnimation(); } } } void onLastStepAcquired() { setEnrollmentProgress(1.f); mLastStepAcquired = true; Log.d(TAG, "setEnrollmentProgress: onLastStepAcquired"); setEnrollmentProgress(UdfpsEnrollHelper.getLastStageThreshold()); } @Override public void draw(@NonNull Canvas canvas) { Log.d(TAG, "setEnrollmentProgress: draw"); canvas.save(); // Progress starts from the top, instead of the right canvas.rotate(-90 + mRotation, 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.rotate(-90f, getBounds().centerX(), getBounds().centerY()); // Draw each of the enroll segments. for (final UdfpsEnrollProgressBarSegment segment : mSegments) { segment.draw(canvas); } canvas.restore(); } Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java 0 → 100644 +256 −0 Original line number Diff line number Diff line /* * 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 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; @NonNull private final Paint mBackgroundPaint; @NonNull private final Paint mProgressPaint; private boolean mIsFilledOrFilling = false; private float mProgress = 0f; @Nullable private ValueAnimator mProgressAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener; 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); 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(context.getColor(R.color.udfps_enroll_progress)); mProgressPaint.setAntiAlias(true); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); mProgressUpdateListener = animation -> { mProgress = (float) 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) { Log.d(TAG, "draw: mProgress = " + mProgress); final float halfPaddingPx = mStrokeWidthPx / 2f; if (mProgress < 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 (mProgress > 0f) { // Draw the filled progress portion of the segment. canvas.drawArc( halfPaddingPx, halfPaddingPx, mBounds.right - halfPaddingPx, mBounds.bottom - halfPaddingPx, mStartAngle, mSweepAngle * mProgress + mOverSweepAngle, false /* useCenter */, mProgressPaint); } } /** * @return Whether this segment is filled or in the process of being filled. */ public boolean isFilledOrFilling() { return mIsFilledOrFilling; } /** * 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) { Log.d(TAG, "updateProgress: progress = " + progress + ", duration = " + animationDurationMs); if (mProgress == progress) { Log.d(TAG, "updateProgress skipped: progress == mProgress"); return; } mIsFilledOrFilling = progress >= 1f; if (mProgressAnimator != null && mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); } mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress); mProgressAnimator.setDuration(animationDurationMs); mProgressAnimator.addUpdateListener(mProgressUpdateListener); mProgressAnimator.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; } Log.d(TAG, "startCompletionAnimation: mProgress = " + mProgress + ", mOverSweepAngle = " + mOverSweepAngle); // Reset sweep angle back to zero if the animation is being rolled back. if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) { mOverSweepReverseAnimator.cancel(); mOverSweepAngle = 0f; } // Start filling the segment if it isn't already. if (mProgress < 1f) { updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS); } // 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() { Log.d(TAG, "cancelCompletionAnimation: mProgress = " + mProgress + ", mOverSweepAngle = " + mOverSweepAngle); // 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(); } } }