1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.biometrics.fingerprint; 18 19 20 import android.animation.Animator; 21 import android.animation.AnimatorSet; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.ColorFilter; 27 import android.graphics.Paint; 28 import android.graphics.PointF; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.ShapeDrawable; 33 import android.graphics.drawable.shapes.PathShape; 34 import android.util.AttributeSet; 35 import android.util.PathParser; 36 import android.view.animation.AccelerateDecelerateInterpolator; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 41 import com.android.settings.R; 42 43 /** 44 * UDFPS fingerprint drawable that is shown when enrolling 45 */ 46 public class UdfpsEnrollDrawable extends Drawable { 47 private static final String TAG = "UdfpsAnimationEnroll"; 48 49 private static final long TARGET_ANIM_DURATION_LONG = 800L; 50 private static final long TARGET_ANIM_DURATION_SHORT = 600L; 51 // 1 + SCALE_MAX is the maximum that the moving target will animate to 52 private static final float SCALE_MAX = 0.25f; 53 private static final float DEFAULT_STROKE_WIDTH = 3f; 54 55 @NonNull 56 private final Drawable mMovingTargetFpIcon; 57 @NonNull 58 private final Paint mSensorOutlinePaint; 59 @NonNull 60 private final Paint mBlueFill; 61 @NonNull 62 private final ShapeDrawable mFingerprintDrawable; 63 64 private int mAlpha; 65 private boolean mSkipDraw = false; 66 67 @Nullable 68 private RectF mSensorRect; 69 @Nullable 70 private UdfpsEnrollHelper mEnrollHelper; 71 72 // Moving target animator set 73 @Nullable 74 AnimatorSet mTargetAnimatorSet; 75 // Moving target location 76 float mCurrentX; 77 float mCurrentY; 78 // Moving target size 79 float mCurrentScale = 1.f; 80 81 @NonNull 82 private final Animator.AnimatorListener mTargetAnimListener; 83 84 private boolean mShouldShowTipHint = false; 85 private boolean mShouldShowEdgeHint = false; 86 87 private int mEnrollIcon; 88 private int mMovingTargetFill; 89 UdfpsEnrollDrawable(@onNull Context context, @Nullable AttributeSet attrs)90 UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) { 91 mFingerprintDrawable = defaultFactory(context); 92 93 loadResources(context, attrs); 94 mSensorOutlinePaint = new Paint(0 /* flags */); 95 mSensorOutlinePaint.setAntiAlias(true); 96 mSensorOutlinePaint.setColor(mMovingTargetFill); 97 mSensorOutlinePaint.setStyle(Paint.Style.FILL); 98 99 mBlueFill = new Paint(0 /* flags */); 100 mBlueFill.setAntiAlias(true); 101 mBlueFill.setColor(mMovingTargetFill); 102 mBlueFill.setStyle(Paint.Style.FILL); 103 104 mMovingTargetFpIcon = context.getResources() 105 .getDrawable(R.drawable.ic_enrollment_fingerprint, null); 106 mMovingTargetFpIcon.setTint(mEnrollIcon); 107 mMovingTargetFpIcon.mutate(); 108 109 mFingerprintDrawable.setTint(mEnrollIcon); 110 111 setAlpha(255); 112 mTargetAnimListener = new Animator.AnimatorListener() { 113 @Override 114 public void onAnimationStart(Animator animation) { 115 } 116 117 @Override 118 public void onAnimationEnd(Animator animation) { 119 updateTipHintVisibility(); 120 } 121 122 @Override 123 public void onAnimationCancel(Animator animation) { 124 } 125 126 @Override 127 public void onAnimationRepeat(Animator animation) { 128 } 129 }; 130 } 131 132 /** The [sensorRect] coordinates for the sensor area. */ onSensorRectUpdated(@onNull RectF sensorRect)133 void onSensorRectUpdated(@NonNull RectF sensorRect) { 134 int margin = ((int) sensorRect.height()) / 8; 135 Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin, 136 (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin); 137 updateFingerprintIconBounds(bounds); 138 mSensorRect = sensorRect; 139 } 140 setEnrollHelper(@onNull UdfpsEnrollHelper helper)141 void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) { 142 mEnrollHelper = helper; 143 } 144 setShouldSkipDraw(boolean skipDraw)145 void setShouldSkipDraw(boolean skipDraw) { 146 if (mSkipDraw == skipDraw) { 147 return; 148 } 149 mSkipDraw = skipDraw; 150 invalidateSelf(); 151 } 152 updateFingerprintIconBounds(@onNull Rect bounds)153 void updateFingerprintIconBounds(@NonNull Rect bounds) { 154 mFingerprintDrawable.setBounds(bounds); 155 invalidateSelf(); 156 mMovingTargetFpIcon.setBounds(bounds); 157 invalidateSelf(); 158 } 159 onEnrollmentProgress(int remaining, int totalSteps)160 void onEnrollmentProgress(int remaining, int totalSteps) { 161 if (mEnrollHelper == null) { 162 return; 163 } 164 165 if (!mEnrollHelper.isCenterEnrollmentStage()) { 166 if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) { 167 mTargetAnimatorSet.end(); 168 } 169 170 final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint(); 171 if (mCurrentX != point.x || mCurrentY != point.y) { 172 final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x); 173 x.addUpdateListener(animation -> { 174 mCurrentX = (float) animation.getAnimatedValue(); 175 invalidateSelf(); 176 }); 177 178 final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y); 179 y.addUpdateListener(animation -> { 180 mCurrentY = (float) animation.getAnimatedValue(); 181 invalidateSelf(); 182 }); 183 184 final boolean isMovingToCenter = point.x == 0f && point.y == 0f; 185 final long duration = isMovingToCenter 186 ? TARGET_ANIM_DURATION_SHORT 187 : TARGET_ANIM_DURATION_LONG; 188 189 final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI); 190 scale.setDuration(duration); 191 scale.addUpdateListener(animation -> { 192 // Grow then shrink 193 mCurrentScale = 1 194 + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); 195 invalidateSelf(); 196 }); 197 198 mTargetAnimatorSet = new AnimatorSet(); 199 200 mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 201 mTargetAnimatorSet.setDuration(duration); 202 mTargetAnimatorSet.addListener(mTargetAnimListener); 203 mTargetAnimatorSet.playTogether(x, y, scale); 204 mTargetAnimatorSet.start(); 205 } else { 206 updateTipHintVisibility(); 207 } 208 } else { 209 updateTipHintVisibility(); 210 } 211 212 updateEdgeHintVisibility(); 213 } 214 215 @Override draw(@onNull Canvas canvas)216 public void draw(@NonNull Canvas canvas) { 217 if (mSkipDraw) { 218 return; 219 } 220 221 // Draw moving target 222 if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { 223 canvas.save(); 224 canvas.translate(mCurrentX, mCurrentY); 225 226 if (mSensorRect != null) { 227 canvas.scale(mCurrentScale, mCurrentScale, 228 mSensorRect.centerX(), mSensorRect.centerY()); 229 canvas.drawOval(mSensorRect, mBlueFill); 230 } 231 232 mMovingTargetFpIcon.draw(canvas); 233 canvas.restore(); 234 } else { 235 if (mSensorRect != null) { 236 canvas.drawOval(mSensorRect, mSensorOutlinePaint); 237 } 238 mFingerprintDrawable.draw(canvas); 239 mFingerprintDrawable.setAlpha(getAlpha()); 240 mSensorOutlinePaint.setAlpha(getAlpha()); 241 } 242 243 } 244 245 @Override setAlpha(int alpha)246 public void setAlpha(int alpha) { 247 mAlpha = alpha; 248 mFingerprintDrawable.setAlpha(alpha); 249 mSensorOutlinePaint.setAlpha(alpha); 250 mBlueFill.setAlpha(alpha); 251 mMovingTargetFpIcon.setAlpha(alpha); 252 invalidateSelf(); 253 } 254 255 @Override getAlpha()256 public int getAlpha() { 257 return mAlpha; 258 } 259 260 @Override setColorFilter(@ullable ColorFilter colorFilter)261 public void setColorFilter(@Nullable ColorFilter colorFilter) { 262 } 263 264 @Override getOpacity()265 public int getOpacity() { 266 return 0; 267 } 268 updateTipHintVisibility()269 private void updateTipHintVisibility() { 270 final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage(); 271 // With the new update, we will git rid of most of this code, and instead 272 // we will change the fingerprint icon. 273 if (mShouldShowTipHint == shouldShow) { 274 return; 275 } 276 mShouldShowTipHint = shouldShow; 277 } 278 updateEdgeHintVisibility()279 private void updateEdgeHintVisibility() { 280 final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage(); 281 if (mShouldShowEdgeHint == shouldShow) { 282 return; 283 } 284 mShouldShowEdgeHint = shouldShow; 285 } 286 defaultFactory(Context context)287 private ShapeDrawable defaultFactory(Context context) { 288 String fpPath = context.getResources().getString(R.string.config_udfpsIcon); 289 ShapeDrawable drawable = new ShapeDrawable( 290 new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f) 291 ); 292 drawable.mutate(); 293 drawable.getPaint().setStyle(Paint.Style.STROKE); 294 drawable.getPaint().setStrokeCap(Paint.Cap.ROUND); 295 drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH); 296 return drawable; 297 } 298 loadResources(Context context, @Nullable AttributeSet attrs)299 private void loadResources(Context context, @Nullable AttributeSet attrs) { 300 final TypedArray ta = context.obtainStyledAttributes(attrs, 301 R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle, 302 R.style.BiometricsEnrollStyle); 303 mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0); 304 mMovingTargetFill = ta.getColor( 305 R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0); 306 ta.recycle(); 307 } 308 309 } 310