Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 67d8eec6 authored by Curtis Belmonte's avatar Curtis Belmonte
Browse files

Combine UDFPS enroll segments into a single circle

Updates the UDFPS enrollment UI to present a single progress circle
instead of 4 segments (corresponding to the 4 underlying enrollment
stages). Visual progress is now continuous, based on the number of
remaining and total steps reported by FingerprintManager, rather than
being normalized across each stage.

As part of this change, the completion animation has also been changed
to display a checkmark rather than combining each of the segments into
a continuous circle at the end of enrollment.

Test: Manual

Fixes: 203134265
Change-Id: I1004c115a171deb14b579fa72ff00e951ca15f94
parent cec06948
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2021 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="54dp"
        android:height="54dp"
        android:viewportWidth="54"
        android:viewportHeight="54">
    <path
        android:pathData="M26.9999,3.9619C39.7029,3.9619 50.0369,14.2969 50.0369,26.9999C50.0369,39.7029 39.7029,50.0379 26.9999,50.0379C14.2969,50.0379 3.9629,39.7029 3.9629,26.9999C3.9629,14.2969 14.2969,3.9619 26.9999,3.9619Z"
        android:fillColor="?android:colorBackground"
        android:fillType="evenOdd"/>
    <path
        android:pathData="M27,0C12.088,0 0,12.088 0,27C0,41.912 12.088,54 27,54C41.912,54 54,41.912 54,27C54,12.088 41.912,0 27,0ZM27,3.962C39.703,3.962 50.037,14.297 50.037,27C50.037,39.703 39.703,50.038 27,50.038C14.297,50.038 3.963,39.703 3.963,27C3.963,14.297 14.297,3.962 27,3.962Z"
        android:fillColor="@color/udfps_enroll_progress"
        android:fillType="evenOdd"/>
    <path
        android:pathData="M23.0899,38.8534L10.4199,26.1824L13.2479,23.3544L23.0899,33.1974L41.2389,15.0474L44.0679,17.8754L23.0899,38.8534Z"
        android:fillColor="@color/udfps_enroll_progress"
        android:fillType="evenOdd"/>
</vector>
+206 −73
Original line number Diff line number Diff line
@@ -16,17 +16,22 @@

package com.android.systemui.biometrics;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.TypedValue;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import com.android.systemui.R;

/**
 * UDFPS enrollment progress bar.
@@ -34,108 +39,193 @@ import java.util.List;
public class UdfpsEnrollProgressBarDrawable extends Drawable {
    private static final String TAG = "UdfpsProgressBar";

    private static final float SEGMENT_GAP_ANGLE = 12f;
    private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
    private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
    private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
    private static final float STROKE_WIDTH_DP = 12f;

    @NonNull private final Context mContext;
    private final float mStrokeWidthPx;
    @ColorInt private final int mProgressColor;
    @ColorInt private final int mHelpColor;
    @NonNull private final Drawable mCheckmarkDrawable;
    @NonNull private final Interpolator mCheckmarkInterpolator;
    @NonNull private final Paint mBackgroundPaint;
    @NonNull private final Paint mFillPaint;

    @Nullable private UdfpsEnrollHelper mEnrollHelper;
    @NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
    private boolean mAfterFirstTouch;

    private int mRemainingSteps = 0;
    private int mTotalSteps = 0;
    private int mProgressSteps = 0;
    private boolean mIsShowingHelp = false;
    private float mProgress = 0f;
    @Nullable private ValueAnimator mProgressAnimator;
    @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;

    private boolean mShowingHelp = false;
    @Nullable private ValueAnimator mFillColorAnimator;
    @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;

    private boolean mComplete = false;
    private float mCheckmarkScale = 0f;
    @Nullable private ValueAnimator mCheckmarkAnimator;
    @NonNull private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener;

    public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
        mContext = context;
    }

    void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
        mEnrollHelper = enrollHelper;
        if (enrollHelper != null) {
            final int stageCount = enrollHelper.getStageCount();
            mSegments = new ArrayList<>(stageCount);
            float startAngle = SEGMENT_GAP_ANGLE / 2f;
            final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
            final Runnable invalidateRunnable = this::invalidateSelf;
            for (int index = 0; index < stageCount; index++) {
                mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
                        sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
                startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
        mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
        mProgressColor = context.getColor(R.color.udfps_enroll_progress);
        mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
        mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
        mCheckmarkDrawable.mutate();
        mCheckmarkInterpolator = new OvershootInterpolator();

        mBackgroundPaint = new Paint();
        mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
        mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
        mBackgroundPaint.setAntiAlias(true);
        mBackgroundPaint.setStyle(Paint.Style.STROKE);
        mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);

        // Set background paint color and alpha.
        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
        final TypedArray typedArray = context.obtainStyledAttributes(attrs);
        try {
            @ColorInt final int tintColor = typedArray.getColor(0, mBackgroundPaint.getColor());
            mBackgroundPaint.setColor(tintColor);
        } finally {
            typedArray.recycle();
        }
        TypedValue alpha = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
        mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));

        // Progress fill should *not* use the extracted system color.
        mFillPaint = new Paint();
        mFillPaint.setStrokeWidth(mStrokeWidthPx);
        mFillPaint.setColor(mProgressColor);
        mFillPaint.setAntiAlias(true);
        mFillPaint.setStyle(Paint.Style.STROKE);
        mFillPaint.setStrokeCap(Paint.Cap.ROUND);

        mProgressUpdateListener = animation -> {
            mProgress = (float) animation.getAnimatedValue();
            invalidateSelf();
        }
        };

        mFillColorUpdateListener = animation -> {
            mFillPaint.setColor((int) animation.getAnimatedValue());
            invalidateSelf();
        };

        mCheckmarkUpdateListener = animation -> {
            mCheckmarkScale = (float) animation.getAnimatedValue();
            invalidateSelf();
        };
    }

    void onEnrollmentProgress(int remaining, int totalSteps) {
        mTotalSteps = totalSteps;

        // Show some progress for the initial touch.
        updateState(Math.max(1, totalSteps - remaining), false /* isShowingHelp */);
        mAfterFirstTouch = true;
        updateState(remaining, totalSteps, false /* showingHelp */);
    }

    void onEnrollmentHelp(int remaining, int totalSteps) {
        updateState(Math.max(0, totalSteps - remaining), true /* isShowingHelp */);
        updateState(remaining, totalSteps, true /* showingHelp */);
    }

    void onLastStepAcquired() {
        updateState(mTotalSteps, false /* isShowingHelp */);
        updateState(0, mTotalSteps, false /* showingHelp */);
    }

    private void updateState(int progressSteps, boolean isShowingHelp) {
        updateProgress(progressSteps);
        updateFillColor(isShowingHelp);
    private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
        updateProgress(remainingSteps, totalSteps);
        updateFillColor(showingHelp);
    }

    private void updateProgress(int progressSteps) {
        if (mProgressSteps == progressSteps) {
    private void updateProgress(int remainingSteps, int totalSteps) {
        if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
            return;
        }
        mProgressSteps = progressSteps;
        mRemainingSteps = remainingSteps;
        mTotalSteps = totalSteps;

        if (mEnrollHelper == null) {
            Log.e(TAG, "updateState: UDFPS enroll helper was null");
            return;
        final int progressSteps = Math.max(0, totalSteps - remainingSteps);

        // If needed, add 1 to progress and total steps to account for initial touch.
        final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps;
        final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps;

        final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal);

        if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
            mProgressAnimator.cancel();
        }

        int index = 0;
        int prevThreshold = 0;
        while (index < mSegments.size()) {
            final UdfpsEnrollProgressBarSegment segment = mSegments.get(index);
            final int thresholdSteps = mEnrollHelper.getStageThresholdSteps(mTotalSteps, index);
            if (progressSteps >= thresholdSteps && segment.getProgress() < 1f) {
                segment.updateProgress(1f);
                break;
            } else if (progressSteps >= prevThreshold && progressSteps < thresholdSteps) {
                final int relativeSteps = progressSteps - prevThreshold;
                final int relativeThreshold = thresholdSteps - prevThreshold;
                final float segmentProgress = (float) relativeSteps / (float) relativeThreshold;
                segment.updateProgress(segmentProgress);
                break;
        mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
        mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
        mProgressAnimator.addUpdateListener(mProgressUpdateListener);
        mProgressAnimator.start();

        if (remainingSteps == 0) {
            startCompletionAnimation();
        } else if (remainingSteps > 0) {
            rollBackCompletionAnimation();
        }
    }

            index++;
            prevThreshold = thresholdSteps;
    private void updateFillColor(boolean showingHelp) {
        if (mShowingHelp == showingHelp) {
            return;
        }
        mShowingHelp = showingHelp;

        if (progressSteps >= mTotalSteps) {
            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
                segment.startCompletionAnimation();
        if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
            mFillColorAnimator.cancel();
        }
        } else {
            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
                segment.cancelCompletionAnimation();

        @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
        mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
        mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
        mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
        mFillColorAnimator.start();
    }

    private void startCompletionAnimation() {
        if (mComplete) {
            return;
        }
        mComplete = true;

        if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
            mCheckmarkAnimator.cancel();
        }

        mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f);
        mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS);
        mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS);
        mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator);
        mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
        mCheckmarkAnimator.start();
    }

    private void updateFillColor(boolean isShowingHelp) {
        if (mIsShowingHelp == isShowingHelp) {
    private void rollBackCompletionAnimation() {
        if (!mComplete) {
            return;
        }
        mIsShowingHelp = isShowingHelp;
        mComplete = false;

        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
            segment.updateFillColor(isShowingHelp);
        // Adjust duration based on how much of the completion animation has played.
        final float animatedFraction = mCheckmarkAnimator != null
                ? mCheckmarkAnimator.getAnimatedFraction()
                : 0f;
        final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction);

        if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
            mCheckmarkAnimator.cancel();
        }

        mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f);
        mCheckmarkAnimator.setDuration(durationMs);
        mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
        mCheckmarkAnimator.start();
    }

    @Override
@@ -145,12 +235,55 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
        // Progress starts from the top, instead of the right
        canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());

        // Draw each of the enroll segments.
        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
            segment.draw(canvas);
        final float halfPaddingPx = mStrokeWidthPx / 2f;

        if (mProgress < 1f) {
            // Draw the background color of the progress circle.
            canvas.drawArc(
                    halfPaddingPx,
                    halfPaddingPx,
                    getBounds().right - halfPaddingPx,
                    getBounds().bottom - halfPaddingPx,
                    0f /* startAngle */,
                    360f /* sweepAngle */,
                    false /* useCenter */,
                    mBackgroundPaint);
        }

        if (mProgress > 0f) {
            // Draw the filled portion of the progress circle.
            canvas.drawArc(
                    halfPaddingPx,
                    halfPaddingPx,
                    getBounds().right - halfPaddingPx,
                    getBounds().bottom - halfPaddingPx,
                    0f /* startAngle */,
                    360f * mProgress /* sweepAngle */,
                    false /* useCenter */,
                    mFillPaint);
        }

        canvas.restore();

        if (mCheckmarkScale > 0f) {
            final float offsetScale = (float) Math.sqrt(2) / 2f;
            final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale;
            final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale;
            final float centerX = getBounds().centerX() + centerXOffset;
            final float centerY = getBounds().centerY() + centerYOffset;

            final float boundsXOffset =
                    mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale;
            final float boundsYOffset =
                    mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale;

            final int left = Math.round(centerX - boundsXOffset);
            final int top = Math.round(centerY - boundsYOffset);
            final int right = Math.round(centerX + boundsXOffset);
            final int bottom = Math.round(centerY + boundsYOffset);
            mCheckmarkDrawable.setBounds(left, top, right, bottom);
            mCheckmarkDrawable.draw(canvas);
        }
    }

    @Override
+0 −280

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −1
Original line number Diff line number Diff line
@@ -73,7 +73,6 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
    }

    void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
        mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
        mFingerprintDrawable.setEnrollHelper(enrollHelper);
    }