1 /* 2 * Copyright (C) 2018 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.face; 18 19 import android.animation.ArgbEvaluator; 20 import android.content.Context; 21 import android.graphics.Canvas; 22 import android.graphics.Paint; 23 import android.graphics.Path; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.util.Log; 27 28 import com.android.settings.R; 29 30 import java.util.List; 31 32 /** 33 * Class containing the state for an individual feedback dot / path. The dots are assigned colors 34 * based on their index. 35 */ 36 public class AnimationParticle { 37 38 private static final String TAG = "AnimationParticle"; 39 40 private static final int MIN_STROKE_WIDTH = 10; 41 private static final int MAX_STROKE_WIDTH = 20; // Be careful that this doesn't get clipped 42 private static final int FINAL_RING_STROKE_WIDTH = 15; 43 44 private static final float ROTATION_SPEED_NORMAL = 0.8f; // radians per second, 1 = ~57 degrees 45 private static final float ROTATION_ACCELERATION_SPEED = 2.0f; 46 private static final float PULSE_SPEED_NORMAL = 1 * 2 * (float) Math.PI; // 1 cycle per second 47 private static final float RING_SWEEP_GROW_RATE_PRIMARY = 480; // degrees per second 48 private static final float RING_SWEEP_GROW_RATE_SECONDARY = 240; // degrees per second 49 private static final float RING_SIZE_FINALIZATION_TIME = 0.1f; // seconds 50 51 private final Rect mBounds; // bounds for the canvas 52 private final int mBorderWidth; // amount of padding from the edges 53 private final ArgbEvaluator mEvaluator; 54 private final int mErrorColor; 55 private final int mIndex; 56 private final Listener mListener; 57 58 private final Paint mPaint; 59 private final int mAssignedColor; 60 private final float mOffsetTimeSec; // stagger particle size to make a wave effect 61 62 private int mLastAnimationState; 63 private int mAnimationState; 64 private float mCurrentSize = MIN_STROKE_WIDTH; 65 private float mCurrentAngle; // 0 is to the right, in radians 66 private float mRotationSpeed = ROTATION_SPEED_NORMAL; // speed of dot rotation 67 private float mSweepAngle = 0; // ring sweep, degrees per second 68 private float mSweepRate = RING_SWEEP_GROW_RATE_SECONDARY; // acceleration 69 private float mRingAdjustRate; // rate at which ring should grow/shrink to final size 70 private float mRingCompletionTime; // time at which ring should be completed 71 72 public interface Listener { onRingCompleted(int index)73 void onRingCompleted(int index); 74 } 75 AnimationParticle(Context context, Listener listener, Rect bounds, int borderWidth, int index, int totalParticles, List<Integer> colors)76 public AnimationParticle(Context context, Listener listener, Rect bounds, int borderWidth, 77 int index, int totalParticles, List<Integer> colors) { 78 mBounds = bounds; 79 mBorderWidth = borderWidth; 80 mEvaluator = new ArgbEvaluator(); 81 mErrorColor = context.getResources() 82 .getColor(R.color.face_anim_particle_error, context.getTheme()); 83 mIndex = index; 84 mListener = listener; 85 86 mCurrentAngle = (float) index / totalParticles * 2 * (float) Math.PI; 87 mOffsetTimeSec = (float) index / totalParticles 88 * (1 / ROTATION_SPEED_NORMAL) * 2 * (float) Math.PI; 89 90 mPaint = new Paint(); 91 mAssignedColor = colors.get(index % colors.size()); 92 mPaint.setColor(mAssignedColor); 93 mPaint.setAntiAlias(true); 94 mPaint.setStrokeWidth(mCurrentSize); 95 mPaint.setStyle(Paint.Style.FILL); 96 mPaint.setStrokeCap(Paint.Cap.ROUND); 97 } 98 updateState(int animationState)99 public void updateState(int animationState) { 100 if (mAnimationState == animationState) { 101 Log.w(TAG, "Already in state " + animationState); 102 return; 103 } 104 if (animationState == ParticleCollection.STATE_COMPLETE) { 105 mPaint.setStyle(Paint.Style.STROKE); 106 } 107 mLastAnimationState = mAnimationState; 108 mAnimationState = animationState; 109 } 110 111 // There are two types of particles, secondary and primary. Primary particles accelerate faster 112 // during the "completed" animation. Particles are secondary by default. setAsPrimary()113 public void setAsPrimary() { 114 mSweepRate = RING_SWEEP_GROW_RATE_PRIMARY; 115 } 116 update(long t, long dt)117 public void update(long t, long dt) { 118 if (mAnimationState != ParticleCollection.STATE_COMPLETE) { 119 updateDot(t, dt); 120 } else { 121 updateRing(t, dt); 122 } 123 } 124 updateDot(long t, long dt)125 private void updateDot(long t, long dt) { 126 final float dtSec = 0.001f * dt; 127 final float tSec = 0.001f * t; 128 129 final float multiplier = mRotationSpeed / ROTATION_SPEED_NORMAL; 130 131 // Calculate rotation speed / angle 132 if ((mAnimationState == ParticleCollection.STATE_STOPPED_COLORFUL 133 || mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) 134 && mRotationSpeed > 0) { 135 // Linear slow down for now 136 mRotationSpeed = Math.max(mRotationSpeed - ROTATION_ACCELERATION_SPEED * dtSec, 0); 137 } else if (mAnimationState == ParticleCollection.STATE_STARTED 138 && mRotationSpeed < ROTATION_SPEED_NORMAL) { 139 // Linear speed up for now 140 mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; 141 } 142 143 mCurrentAngle += dtSec * mRotationSpeed; 144 145 // Calculate dot / ring size; linearly proportional with rotation speed 146 mCurrentSize = 147 (MAX_STROKE_WIDTH - MIN_STROKE_WIDTH) / 2 148 * (float) Math.sin(tSec * PULSE_SPEED_NORMAL + mOffsetTimeSec) 149 + (MAX_STROKE_WIDTH + MIN_STROKE_WIDTH) / 2; 150 mCurrentSize = (mCurrentSize - MIN_STROKE_WIDTH) * multiplier + MIN_STROKE_WIDTH; 151 152 // Calculate paint color; linearly proportional to rotation speed 153 int color = mAssignedColor; 154 if (mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { 155 color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); 156 } else if (mLastAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { 157 color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); 158 } 159 160 mPaint.setColor(color); 161 mPaint.setStrokeWidth(mCurrentSize); 162 } 163 updateRing(long t, long dt)164 private void updateRing(long t, long dt) { 165 final float dtSec = 0.001f * dt; 166 final float tSec = 0.001f * t; 167 168 // Store the start time, since we need to guarantee all rings reach final size at same time 169 // independent of current size. The magic 0 check is safe. 170 if (mRingAdjustRate == 0) { 171 mRingAdjustRate = 172 (FINAL_RING_STROKE_WIDTH - mCurrentSize) / RING_SIZE_FINALIZATION_TIME; 173 if (mRingCompletionTime == 0) { 174 mRingCompletionTime = tSec + RING_SIZE_FINALIZATION_TIME; 175 } 176 } 177 178 // Accelerate to attack speed.. jk, back to normal speed 179 if (mRotationSpeed < ROTATION_SPEED_NORMAL) { 180 mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; 181 } 182 183 // For arcs, this is the "start" 184 mCurrentAngle += dtSec * mRotationSpeed; 185 186 // Update the sweep angle until it fills entire circle 187 if (mSweepAngle < 360) { 188 final float sweepGrowth = mSweepRate * dtSec; 189 mSweepAngle = mSweepAngle + sweepGrowth; 190 mSweepRate = mSweepRate + sweepGrowth; 191 } 192 if (mSweepAngle > 360) { 193 mSweepAngle = 360; 194 mListener.onRingCompleted(mIndex); 195 } 196 197 // Animate stroke width to final size. 198 if (tSec < RING_SIZE_FINALIZATION_TIME) { 199 mCurrentSize = mCurrentSize + mRingAdjustRate * dtSec; 200 mPaint.setStrokeWidth(mCurrentSize); 201 } else { 202 // There should be small to no discontinuity in this if/else 203 mCurrentSize = FINAL_RING_STROKE_WIDTH; 204 mPaint.setStrokeWidth(mCurrentSize); 205 } 206 207 } 208 draw(Canvas canvas)209 public void draw(Canvas canvas) { 210 if (mAnimationState != ParticleCollection.STATE_COMPLETE) { 211 drawDot(canvas); 212 } else { 213 drawRing(canvas); 214 } 215 } 216 217 // Draws a dot at the current position on the circumference of the path. drawDot(Canvas canvas)218 private void drawDot(Canvas canvas) { 219 final float w = mBounds.right - mBounds.exactCenterX() - mBorderWidth; 220 final float h = mBounds.bottom - mBounds.exactCenterY() - mBorderWidth; 221 canvas.drawCircle( 222 mBounds.exactCenterX() + w * (float) Math.cos(mCurrentAngle), 223 mBounds.exactCenterY() + h * (float) Math.sin(mCurrentAngle), 224 mCurrentSize, 225 mPaint); 226 } 227 drawRing(Canvas canvas)228 private void drawRing(Canvas canvas) { 229 RectF arc = new RectF( 230 mBorderWidth, mBorderWidth, 231 mBounds.width() - mBorderWidth, mBounds.height() - mBorderWidth); 232 Path path = new Path(); 233 path.arcTo(arc, (float) Math.toDegrees(mCurrentAngle), mSweepAngle); 234 canvas.drawPath(path, mPaint); 235 } 236 } 237