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

Commit b4a1d572 authored by Curtis Belmonte's avatar Curtis Belmonte Committed by Android (Google) Code Review
Browse files

Merge "RESTRICT AUTOMERGE Support UDFPS multi-stage enrollment" into sc-dev

parents c94785d2 aee9eaa9
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -210,6 +210,7 @@ public class UdfpsController implements DozeReceiver {
        }

        void onAcquiredGood() {
            Log.d(TAG, "onAcquiredGood");
            if (mEnrollHelper != null) {
                mEnrollHelper.animateIfLastStep();
            }
+31 −30
Original line number Diff line number Diff line
@@ -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();
@@ -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();
                });

@@ -134,6 +134,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable {
                mAnimatorSet.start();
            }
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
@@ -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);

+53 −12
Original line number Diff line number Diff line
@@ -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);
@@ -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) {
@@ -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;
@@ -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);
        }

@@ -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;
+65 −99
Original line number Diff line number Diff line
@@ -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();
    }
+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