/* * 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.Animator; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; 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 android.view.animation.LinearInterpolator; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.R; /** * UDFPS fingerprint drawable that is shown when enrolling */ public class UdfpsEnrollDrawable extends UdfpsDrawable { private static final String TAG = "UdfpsAnimationEnroll"; private static final long HINT_COLOR_ANIM_DELAY_MS = 233L; private static final long HINT_COLOR_ANIM_DURATION_MS = 517L; private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L; private static final long TARGET_ANIM_DURATION_LONG = 800L; private static final long TARGET_ANIM_DURATION_SHORT = 600L; // 1 + SCALE_MAX is the maximum that the moving target will animate to private static final float SCALE_MAX = 0.25f; private static final float HINT_PADDING_DP = 10f; private static final float HINT_MAX_WIDTH_DP = 6f; private static final float HINT_ANGLE = 40f; private final Handler mHandler = new Handler(Looper.getMainLooper()); @NonNull private final Drawable mMovingTargetFpIcon; @NonNull private final Paint mSensorOutlinePaint; @NonNull private final Paint mBlueFill; @Nullable private RectF mSensorRect; @Nullable private UdfpsEnrollHelper mEnrollHelper; // Moving target animator set @Nullable AnimatorSet mTargetAnimatorSet; // Moving target location float mCurrentX; float mCurrentY; // Moving target size float mCurrentScale = 1.f; @ColorInt private final int mHintColorFaded; @ColorInt private final int mHintColorHighlight; private final float mHintMaxWidthPx; private final float mHintPaddingPx; @NonNull private final Animator.AnimatorListener mTargetAnimListener; private boolean mShouldShowTipHint = false; @NonNull private final Paint mTipHintPaint; @Nullable private AnimatorSet mTipHintAnimatorSet; @Nullable private ValueAnimator mTipHintColorAnimator; @Nullable private ValueAnimator mTipHintWidthAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener; @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener; @NonNull private final Animator.AnimatorListener mTipHintPulseListener; private boolean mShouldShowEdgeHint = false; @NonNull private final Paint mEdgeHintPaint; @Nullable private AnimatorSet mEdgeHintAnimatorSet; @Nullable private ValueAnimator mEdgeHintColorAnimator; @Nullable private ValueAnimator mEdgeHintWidthAnimator; @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener; @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener; @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener; private boolean mShowingNewUdfpsEnroll = false; UdfpsEnrollDrawable(@NonNull Context context) { super(context); mSensorOutlinePaint = new Paint(0 /* flags */); mSensorOutlinePaint.setAntiAlias(true); mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_moving_target_fill)); mSensorOutlinePaint.setStyle(Paint.Style.FILL); mBlueFill = new Paint(0 /* flags */); mBlueFill.setAntiAlias(true); mBlueFill.setColor(context.getColor(R.color.udfps_moving_target_fill)); mBlueFill.setStyle(Paint.Style.FILL); mMovingTargetFpIcon = context.getResources() .getDrawable(R.drawable.ic_kg_fingerprint, null); mMovingTargetFpIcon.setTint(mContext.getColor(R.color.udfps_enroll_icon)); mMovingTargetFpIcon.mutate(); mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon)); mHintColorFaded = context.getColor(R.color.udfps_moving_target_fill); mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress); mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP); mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP); mTargetAnimListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { updateTipHintVisibility(); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }; mTipHintPaint = new Paint(0 /* flags */); mTipHintPaint.setAntiAlias(true); mTipHintPaint.setColor(mHintColorFaded); mTipHintPaint.setStyle(Paint.Style.STROKE); mTipHintPaint.setStrokeCap(Paint.Cap.ROUND); mTipHintPaint.setStrokeWidth(0f); mTipHintColorUpdateListener = animation -> { mTipHintPaint.setColor((int) animation.getAnimatedValue()); invalidateSelf(); }; mTipHintWidthUpdateListener = animation -> { mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue()); invalidateSelf(); }; mTipHintPulseListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { mHandler.postDelayed(() -> { mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded); mTipHintColorAnimator.setInterpolator(new LinearInterpolator()); mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS); mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener); mTipHintColorAnimator.start(); }, HINT_COLOR_ANIM_DELAY_MS); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }; mEdgeHintPaint = new Paint(0 /* flags */); mEdgeHintPaint.setAntiAlias(true); mEdgeHintPaint.setColor(mHintColorFaded); mEdgeHintPaint.setStyle(Paint.Style.STROKE); mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND); mEdgeHintPaint.setStrokeWidth(0f); mEdgeHintColorUpdateListener = animation -> { mEdgeHintPaint.setColor((int) animation.getAnimatedValue()); invalidateSelf(); }; mEdgeHintWidthUpdateListener = animation -> { mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue()); invalidateSelf(); }; mEdgeHintPulseListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { mHandler.postDelayed(() -> { mEdgeHintColorAnimator = ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded); mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator()); mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS); mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener); mEdgeHintColorAnimator.start(); }, HINT_COLOR_ANIM_DELAY_MS); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }; mShowingNewUdfpsEnroll = context.getResources().getBoolean( com.android.internal.R.bool.config_udfpsSupportsNewUi); } void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) { mEnrollHelper = helper; } @Override public void onSensorRectUpdated(@NonNull RectF sensorRect) { super.onSensorRectUpdated(sensorRect); mSensorRect = sensorRect; } @Override protected void updateFingerprintIconBounds(@NonNull Rect bounds) { super.updateFingerprintIconBounds(bounds); mMovingTargetFpIcon.setBounds(bounds); invalidateSelf(); } void onEnrollmentProgress(int remaining, int totalSteps) { if (mEnrollHelper == null) { return; } if (!mEnrollHelper.isCenterEnrollmentStage()) { if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) { mTargetAnimatorSet.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(); invalidateSelf(); }); final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y); y.addUpdateListener(animation -> { mCurrentY = (float) animation.getAnimatedValue(); invalidateSelf(); }); final boolean isMovingToCenter = point.x == 0f && point.y == 0f; final long duration = isMovingToCenter ? TARGET_ANIM_DURATION_SHORT : TARGET_ANIM_DURATION_LONG; final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI); scale.setDuration(duration); scale.addUpdateListener(animation -> { // Grow then shrink mCurrentScale = 1 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); invalidateSelf(); }); mTargetAnimatorSet = new AnimatorSet(); mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); mTargetAnimatorSet.setDuration(duration); mTargetAnimatorSet.addListener(mTargetAnimListener); mTargetAnimatorSet.playTogether(x, y, scale); mTargetAnimatorSet.start(); } else { updateTipHintVisibility(); } } else { updateTipHintVisibility(); } updateEdgeHintVisibility(); } private void updateTipHintVisibility() { final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage(); if (mShouldShowTipHint == shouldShow) { return; } mShouldShowTipHint = shouldShow; if (mShowingNewUdfpsEnroll) { return; } if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) { mTipHintWidthAnimator.cancel(); } final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f; mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth); mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener); if (shouldShow) { startTipHintPulseAnimation(); } else { mTipHintWidthAnimator.start(); } } private void updateEdgeHintVisibility() { final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage(); if (mShouldShowEdgeHint == shouldShow) { return; } mShouldShowEdgeHint = shouldShow; if (mShowingNewUdfpsEnroll) { return; } if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) { mEdgeHintWidthAnimator.cancel(); } final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f; mEdgeHintWidthAnimator = ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth); mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener); if (shouldShow) { startEdgeHintPulseAnimation(); } else { mEdgeHintWidthAnimator.start(); } } private void startTipHintPulseAnimation() { if (mShowingNewUdfpsEnroll) { return; } mHandler.removeCallbacksAndMessages(null); if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) { mTipHintAnimatorSet.cancel(); } if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) { mTipHintColorAnimator.cancel(); } mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight); mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener); mTipHintColorAnimator.addListener(mTipHintPulseListener); mTipHintAnimatorSet = new AnimatorSet(); mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator); mTipHintAnimatorSet.start(); } private void startEdgeHintPulseAnimation() { if (mShowingNewUdfpsEnroll) { return; } mHandler.removeCallbacksAndMessages(null); if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) { mEdgeHintAnimatorSet.cancel(); } if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) { mEdgeHintColorAnimator.cancel(); } mEdgeHintColorAnimator = ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight); mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS); mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener); mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener); mEdgeHintAnimatorSet = new AnimatorSet(); mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator); mEdgeHintAnimatorSet.start(); } private boolean isTipHintVisible() { return mTipHintPaint.getStrokeWidth() > 0f; } private boolean isEdgeHintVisible() { return mEdgeHintPaint.getStrokeWidth() > 0f; } @Override public void draw(@NonNull Canvas canvas) { if (isIlluminationShowing()) { return; } // Draw moving target if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { canvas.save(); canvas.translate(mCurrentX, mCurrentY); if (mSensorRect != null) { canvas.scale(mCurrentScale, mCurrentScale, mSensorRect.centerX(), mSensorRect.centerY()); canvas.drawOval(mSensorRect, mBlueFill); } mMovingTargetFpIcon.draw(canvas); canvas.restore(); } else { if (mSensorRect != null) { canvas.drawOval(mSensorRect, mSensorOutlinePaint); } mFingerprintDrawable.draw(canvas); mFingerprintDrawable.setAlpha(mAlpha); mSensorOutlinePaint.setAlpha(mAlpha); } if (mShowingNewUdfpsEnroll) { return; } // Draw the finger tip or edges hint. if (isTipHintVisible() || isEdgeHintVisible()) { canvas.save(); // Make arcs start from the top, rather than the right. canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY()); final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f; final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f; final float hintXOffset = halfSensorWidth + mHintPaddingPx; final float hintYOffset = halfSensorHeight + mHintPaddingPx; if (isTipHintVisible()) { canvas.drawArc( mSensorRect.centerX() - hintXOffset, mSensorRect.centerY() - hintYOffset, mSensorRect.centerX() + hintXOffset, mSensorRect.centerY() + hintYOffset, -HINT_ANGLE / 2f, HINT_ANGLE, false /* useCenter */, mTipHintPaint); } if (isEdgeHintVisible()) { // Draw right edge hint. canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY()); canvas.drawArc( mSensorRect.centerX() - hintXOffset, mSensorRect.centerY() - hintYOffset, mSensorRect.centerX() + hintXOffset, mSensorRect.centerY() + hintYOffset, -HINT_ANGLE / 2f, HINT_ANGLE, false /* useCenter */, mEdgeHintPaint); // Draw left edge hint. canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY()); canvas.drawArc( mSensorRect.centerX() - hintXOffset, mSensorRect.centerY() - hintYOffset, mSensorRect.centerX() + hintXOffset, mSensorRect.centerY() + hintYOffset, -HINT_ANGLE / 2f, HINT_ANGLE, false /* useCenter */, mEdgeHintPaint); } canvas.restore(); } } @Override public void setAlpha(int alpha) { super.setAlpha(alpha); mSensorOutlinePaint.setAlpha(alpha); mBlueFill.setAlpha(alpha); mMovingTargetFpIcon.setAlpha(alpha); invalidateSelf(); } }