• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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