| /* |
| * Copyright (C) 2018 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.settings.biometrics.face; |
| |
| import android.animation.ArgbEvaluator; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.util.Log; |
| |
| import com.android.settings.R; |
| |
| import java.util.List; |
| |
| /** |
| * Class containing the state for an individual feedback dot / path. The dots are assigned colors |
| * based on their index. |
| */ |
| public class AnimationParticle { |
| |
| private static final String TAG = "AnimationParticle"; |
| |
| private static final int MIN_STROKE_WIDTH = 10; |
| private static final int MAX_STROKE_WIDTH = 20; // Be careful that this doesn't get clipped |
| private static final int FINAL_RING_STROKE_WIDTH = 15; |
| |
| private static final float ROTATION_SPEED_NORMAL = 0.8f; // radians per second, 1 = ~57 degrees |
| private static final float ROTATION_ACCELERATION_SPEED = 2.0f; |
| private static final float PULSE_SPEED_NORMAL = 1 * 2 * (float) Math.PI; // 1 cycle per second |
| private static final float RING_SWEEP_GROW_RATE_PRIMARY = 480; // degrees per second |
| private static final float RING_SWEEP_GROW_RATE_SECONDARY = 240; // degrees per second |
| private static final float RING_SIZE_FINALIZATION_TIME = 0.1f; // seconds |
| |
| private final Rect mBounds; // bounds for the canvas |
| private final int mBorderWidth; // amount of padding from the edges |
| private final ArgbEvaluator mEvaluator; |
| private final int mErrorColor; |
| private final int mIndex; |
| private final Listener mListener; |
| |
| private final Paint mPaint; |
| private final int mAssignedColor; |
| private final float mOffsetTimeSec; // stagger particle size to make a wave effect |
| |
| private int mLastAnimationState; |
| private int mAnimationState; |
| private float mCurrentSize = MIN_STROKE_WIDTH; |
| private float mCurrentAngle; // 0 is to the right, in radians |
| private float mRotationSpeed = ROTATION_SPEED_NORMAL; // speed of dot rotation |
| private float mSweepAngle = 0; // ring sweep, degrees per second |
| private float mSweepRate = RING_SWEEP_GROW_RATE_SECONDARY; // acceleration |
| private float mRingAdjustRate; // rate at which ring should grow/shrink to final size |
| private float mRingCompletionTime; // time at which ring should be completed |
| |
| public interface Listener { |
| void onRingCompleted(int index); |
| } |
| |
| public AnimationParticle(Context context, Listener listener, Rect bounds, int borderWidth, |
| int index, int totalParticles, List<Integer> colors) { |
| mBounds = bounds; |
| mBorderWidth = borderWidth; |
| mEvaluator = new ArgbEvaluator(); |
| mErrorColor = context.getResources() |
| .getColor(R.color.face_anim_particle_error, context.getTheme()); |
| mIndex = index; |
| mListener = listener; |
| |
| mCurrentAngle = (float) index / totalParticles * 2 * (float) Math.PI; |
| mOffsetTimeSec = (float) index / totalParticles |
| * (1 / ROTATION_SPEED_NORMAL) * 2 * (float) Math.PI; |
| |
| mPaint = new Paint(); |
| mAssignedColor = colors.get(index % colors.size()); |
| mPaint.setColor(mAssignedColor); |
| mPaint.setAntiAlias(true); |
| mPaint.setStrokeWidth(mCurrentSize); |
| mPaint.setStyle(Paint.Style.FILL); |
| mPaint.setStrokeCap(Paint.Cap.ROUND); |
| } |
| |
| public void updateState(int animationState) { |
| if (mAnimationState == animationState) { |
| Log.w(TAG, "Already in state " + animationState); |
| return; |
| } |
| if (animationState == ParticleCollection.STATE_COMPLETE) { |
| mPaint.setStyle(Paint.Style.STROKE); |
| } |
| mLastAnimationState = mAnimationState; |
| mAnimationState = animationState; |
| } |
| |
| // There are two types of particles, secondary and primary. Primary particles accelerate faster |
| // during the "completed" animation. Particles are secondary by default. |
| public void setAsPrimary() { |
| mSweepRate = RING_SWEEP_GROW_RATE_PRIMARY; |
| } |
| |
| public void update(long t, long dt) { |
| if (mAnimationState != ParticleCollection.STATE_COMPLETE) { |
| updateDot(t, dt); |
| } else { |
| updateRing(t, dt); |
| } |
| } |
| |
| private void updateDot(long t, long dt) { |
| final float dtSec = 0.001f * dt; |
| final float tSec = 0.001f * t; |
| |
| final float multiplier = mRotationSpeed / ROTATION_SPEED_NORMAL; |
| |
| // Calculate rotation speed / angle |
| if ((mAnimationState == ParticleCollection.STATE_STOPPED_COLORFUL |
| || mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) |
| && mRotationSpeed > 0) { |
| // Linear slow down for now |
| mRotationSpeed = Math.max(mRotationSpeed - ROTATION_ACCELERATION_SPEED * dtSec, 0); |
| } else if (mAnimationState == ParticleCollection.STATE_STARTED |
| && mRotationSpeed < ROTATION_SPEED_NORMAL) { |
| // Linear speed up for now |
| mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; |
| } |
| |
| mCurrentAngle += dtSec * mRotationSpeed; |
| |
| // Calculate dot / ring size; linearly proportional with rotation speed |
| mCurrentSize = |
| (MAX_STROKE_WIDTH - MIN_STROKE_WIDTH) / 2 |
| * (float) Math.sin(tSec * PULSE_SPEED_NORMAL + mOffsetTimeSec) |
| + (MAX_STROKE_WIDTH + MIN_STROKE_WIDTH) / 2; |
| mCurrentSize = (mCurrentSize - MIN_STROKE_WIDTH) * multiplier + MIN_STROKE_WIDTH; |
| |
| // Calculate paint color; linearly proportional to rotation speed |
| int color = mAssignedColor; |
| if (mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { |
| color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); |
| } else if (mLastAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { |
| color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); |
| } |
| |
| mPaint.setColor(color); |
| mPaint.setStrokeWidth(mCurrentSize); |
| } |
| |
| private void updateRing(long t, long dt) { |
| final float dtSec = 0.001f * dt; |
| final float tSec = 0.001f * t; |
| |
| // Store the start time, since we need to guarantee all rings reach final size at same time |
| // independent of current size. The magic 0 check is safe. |
| if (mRingAdjustRate == 0) { |
| mRingAdjustRate = |
| (FINAL_RING_STROKE_WIDTH - mCurrentSize) / RING_SIZE_FINALIZATION_TIME; |
| if (mRingCompletionTime == 0) { |
| mRingCompletionTime = tSec + RING_SIZE_FINALIZATION_TIME; |
| } |
| } |
| |
| // Accelerate to attack speed.. jk, back to normal speed |
| if (mRotationSpeed < ROTATION_SPEED_NORMAL) { |
| mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; |
| } |
| |
| // For arcs, this is the "start" |
| mCurrentAngle += dtSec * mRotationSpeed; |
| |
| // Update the sweep angle until it fills entire circle |
| if (mSweepAngle < 360) { |
| final float sweepGrowth = mSweepRate * dtSec; |
| mSweepAngle = mSweepAngle + sweepGrowth; |
| mSweepRate = mSweepRate + sweepGrowth; |
| } |
| if (mSweepAngle > 360) { |
| mSweepAngle = 360; |
| mListener.onRingCompleted(mIndex); |
| } |
| |
| // Animate stroke width to final size. |
| if (tSec < RING_SIZE_FINALIZATION_TIME) { |
| mCurrentSize = mCurrentSize + mRingAdjustRate * dtSec; |
| mPaint.setStrokeWidth(mCurrentSize); |
| } else { |
| // There should be small to no discontinuity in this if/else |
| mCurrentSize = FINAL_RING_STROKE_WIDTH; |
| mPaint.setStrokeWidth(mCurrentSize); |
| } |
| |
| } |
| |
| public void draw(Canvas canvas) { |
| if (mAnimationState != ParticleCollection.STATE_COMPLETE) { |
| drawDot(canvas); |
| } else { |
| drawRing(canvas); |
| } |
| } |
| |
| // Draws a dot at the current position on the circumference of the path. |
| private void drawDot(Canvas canvas) { |
| final float w = mBounds.right - mBounds.exactCenterX() - mBorderWidth; |
| final float h = mBounds.bottom - mBounds.exactCenterY() - mBorderWidth; |
| canvas.drawCircle( |
| mBounds.exactCenterX() + w * (float) Math.cos(mCurrentAngle), |
| mBounds.exactCenterY() + h * (float) Math.sin(mCurrentAngle), |
| mCurrentSize, |
| mPaint); |
| } |
| |
| private void drawRing(Canvas canvas) { |
| RectF arc = new RectF( |
| mBorderWidth, mBorderWidth, |
| mBounds.width() - mBorderWidth, mBounds.height() - mBorderWidth); |
| Path path = new Path(); |
| path.arcTo(arc, (float) Math.toDegrees(mCurrentAngle), mSweepAngle); |
| canvas.drawPath(path, mPaint); |
| } |
| } |