7/n: Add enrollment animation

Fixes: 112005540

Test: Tested with ag/4749121

Change-Id: I7d51187f7b8b7a6c2c34c984740b76bc9fd89262
This commit is contained in:
Kevin Chyn
2018-08-09 16:31:09 -07:00
parent 6ec727914f
commit e9117d9b95
9 changed files with 506 additions and 20 deletions

View File

@@ -136,5 +136,11 @@
<color name="battery_maybe_color_dark">#fdd835</color> <!-- Material Yellow 600 --> <color name="battery_maybe_color_dark">#fdd835</color> <!-- Material Yellow 600 -->
<color name="battery_bad_color_dark">#f44336</color> <!-- Material Red 500 --> <color name="battery_bad_color_dark">#f44336</color> <!-- Material Red 500 -->
<!-- TODO: Figure out colors -->
<color name="face_anim_particle_color_1">#ff00bcd4</color> <!-- Material Cyan 500 -->
<color name="face_anim_particle_color_2">#ffef6c00</color> <!-- Material Orange 800 -->
<color name="face_anim_particle_color_3">#ff4caf50</color> <!-- Material Green 500 -->
<color name="face_anim_particle_color_4">#fffdd835</color> <!-- Material Yellow 600 -->
<color name="face_anim_particle_error">#ff9e9e9e</color> <!-- Material Gray 500 -->
</resources> </resources>

View File

@@ -37,7 +37,7 @@ import java.util.ArrayList;
public abstract class BiometricEnrollSidecar extends InstrumentedFragment { public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
public interface Listener { public interface Listener {
void onEnrollmentHelp(CharSequence helpString); void onEnrollmentHelp(int helpMsgId, CharSequence helpString);
void onEnrollmentError(int errMsgId, CharSequence errString); void onEnrollmentError(int errMsgId, CharSequence errString);
void onEnrollmentProgressChange(int steps, int remaining); void onEnrollmentProgressChange(int steps, int remaining);
} }
@@ -82,7 +82,7 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
@Override @Override
public void send(Listener listener) { public void send(Listener listener) {
listener.onEnrollmentHelp(helpString); listener.onEnrollmentHelp(helpMsgId, helpString);
} }
} }
@@ -174,7 +174,7 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (mListener != null) { if (mListener != null) {
mListener.onEnrollmentHelp(helpString); mListener.onEnrollmentHelp(helpMsgId, helpString);
} else { } else {
mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString)); mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
} }

View File

@@ -0,0 +1,236 @@
/*
* 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);
}
}

View File

@@ -16,6 +16,8 @@
package com.android.settings.biometrics.face; package com.android.settings.biometrics.face;
import android.animation.TimeAnimator;
import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.ColorFilter; import android.graphics.ColorFilter;
@@ -26,16 +28,43 @@ import android.graphics.PorterDuffXfermode;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
/** import com.android.settings.biometrics.BiometricEnrollSidecar;
* A drawable containing the circle cutout.
*/
public class FaceEnrollAnimationDrawable extends Drawable {
/**
* A drawable containing the circle cutout as well as the animations.
*/
public class FaceEnrollAnimationDrawable extends Drawable
implements BiometricEnrollSidecar.Listener {
// Tune this parameter so the UI looks nice - and so that we don't have to draw the animations
// outside our bounds. A fraction of each rotating dot should be overlapping the camera preview.
private static final int BORDER_BOUNDS = 20;
private final Context mContext;
private final ParticleCollection.Listener mListener;
private Rect mBounds; private Rect mBounds;
private final Paint mSquarePaint; private final Paint mSquarePaint;
private final Paint mCircleCutoutPaint; private final Paint mCircleCutoutPaint;
public FaceEnrollAnimationDrawable() { private ParticleCollection mParticleCollection;
private TimeAnimator mTimeAnimator;
private final ParticleCollection.Listener mAnimationListener
= new ParticleCollection.Listener() {
@Override
public void onEnrolled() {
if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
mTimeAnimator.end();
mListener.onEnrolled();
}
}
};
public FaceEnrollAnimationDrawable(Context context, ParticleCollection.Listener listener) {
mContext = context;
mListener = listener;
mSquarePaint = new Paint(); mSquarePaint = new Paint();
mSquarePaint.setColor(Color.WHITE); mSquarePaint.setColor(Color.WHITE);
mSquarePaint.setAntiAlias(true); mSquarePaint.setAntiAlias(true);
@@ -46,9 +75,35 @@ public class FaceEnrollAnimationDrawable extends Drawable {
mCircleCutoutPaint.setAntiAlias(true); mCircleCutoutPaint.setAntiAlias(true);
} }
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
mParticleCollection.onEnrollmentHelp(helpMsgId, helpString);
}
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
mParticleCollection.onEnrollmentError(errMsgId, errString);
}
@Override
public void onEnrollmentProgressChange(int steps, int remaining) {
mParticleCollection.onEnrollmentProgressChange(steps, remaining);
}
@Override @Override
protected void onBoundsChange(Rect bounds) { protected void onBoundsChange(Rect bounds) {
mBounds = bounds; mBounds = bounds;
mParticleCollection =
new ParticleCollection(mContext, mAnimationListener, bounds, BORDER_BOUNDS);
if (mTimeAnimator == null) {
mTimeAnimator = new TimeAnimator();
mTimeAnimator.setTimeListener((animation, totalTimeMs, deltaTimeMs) -> {
mParticleCollection.update(totalTimeMs, deltaTimeMs);
FaceEnrollAnimationDrawable.this.invalidateSelf();
});
mTimeAnimator.start();
}
} }
@Override @Override
@@ -63,7 +118,10 @@ public class FaceEnrollAnimationDrawable extends Drawable {
// Clear a circle in the middle for the camera preview // Clear a circle in the middle for the camera preview
canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(), canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(),
mBounds.height() / 2, mCircleCutoutPaint); mBounds.height() / 2 - BORDER_BOUNDS, mCircleCutoutPaint);
// Draw the animation
mParticleCollection.draw(canvas);
canvas.restore(); canvas.restore();
} }

View File

@@ -45,7 +45,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
private TextView mErrorText; private TextView mErrorText;
private Interpolator mLinearOutSlowInInterpolator; private Interpolator mLinearOutSlowInInterpolator;
private boolean mShouldFinishOnStop = true; private boolean mShouldFinishOnStop = true;
private FaceEnrollPreviewFragment mFaceCameraPreview; private FaceEnrollPreviewFragment mPreviewFragment;
private ParticleCollection.Listener mListener = new ParticleCollection.Listener() {
@Override
public void onEnrolled() {
FaceEnrollEnrolling.this.launchFinish(mToken);
}
};
public static class FaceErrorDialog extends BiometricErrorDialog { public static class FaceErrorDialog extends BiometricErrorDialog {
static FaceErrorDialog newInstance(CharSequence msg, int msgId) { static FaceErrorDialog newInstance(CharSequence msg, int msgId) {
@@ -87,7 +94,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
if (shouldLaunchConfirmLock()) { if (shouldLaunchConfirmLock()) {
launchConfirmLock(R.string.security_settings_face_preference_title, launchConfirmLock(R.string.security_settings_face_preference_title,
Utils.getFaceManagerOrNull(this).preEnroll()); Utils.getFingerprintManagerOrNull(this).preEnroll());
mShouldFinishOnStop = false; mShouldFinishOnStop = false;
} else { } else {
startEnrollment(); startEnrollment();
@@ -97,13 +104,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
@Override @Override
public void startEnrollment() { public void startEnrollment() {
super.startEnrollment(); super.startEnrollment();
mFaceCameraPreview = (FaceEnrollPreviewFragment) getSupportFragmentManager() mPreviewFragment = (FaceEnrollPreviewFragment) getSupportFragmentManager()
.findFragmentByTag(TAG_FACE_PREVIEW); .findFragmentByTag(TAG_FACE_PREVIEW);
if (mFaceCameraPreview == null) { if (mPreviewFragment == null) {
mFaceCameraPreview = new FaceEnrollPreviewFragment(); mPreviewFragment = new FaceEnrollPreviewFragment();
getSupportFragmentManager().beginTransaction().add(mFaceCameraPreview, TAG_FACE_PREVIEW) getSupportFragmentManager().beginTransaction().add(mPreviewFragment, TAG_FACE_PREVIEW)
.commitAllowingStateLoss(); .commitAllowingStateLoss();
} }
mPreviewFragment.setListener(mListener);
} }
@Override @Override
@@ -132,10 +140,11 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
} }
@Override @Override
public void onEnrollmentHelp(CharSequence helpString) { public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) { if (!TextUtils.isEmpty(helpString)) {
showError(helpString); showError(helpString);
} }
mPreviewFragment.onEnrollmentHelp(helpMsgId, helpString);
} }
@Override @Override
@@ -149,6 +158,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
msgId = R.string.security_settings_face_enroll_error_generic_dialog_message; msgId = R.string.security_settings_face_enroll_error_generic_dialog_message;
break; break;
} }
mPreviewFragment.onEnrollmentError(errMsgId, errString);
showErrorDialog(getText(msgId), errMsgId); showErrorDialog(getText(msgId), errMsgId);
} }
@@ -157,6 +167,8 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
if (DEBUG) { if (DEBUG) {
Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining); Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining);
} }
mPreviewFragment.onEnrollmentProgressChange(steps, remaining);
// TODO: Update the actual animation // TODO: Update the actual animation
showError("Steps: " + steps + " Remaining: " + remaining); showError("Steps: " + steps + " Remaining: " + remaining);
} }

View File

@@ -38,6 +38,7 @@ import android.widget.ImageView;
import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.biometrics.BiometricEnrollSidecar;
import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.InstrumentedPreferenceFragment;
import java.util.ArrayList; import java.util.ArrayList;
@@ -50,7 +51,8 @@ import java.util.List;
* Fragment that contains the logic for showing and controlling the camera preview, circular * Fragment that contains the logic for showing and controlling the camera preview, circular
* overlay, as well as the enrollment animations. * overlay, as well as the enrollment animations.
*/ */
public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment { public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment
implements BiometricEnrollSidecar.Listener {
private static final String TAG = "FaceEnrollPreviewFragment"; private static final String TAG = "FaceEnrollPreviewFragment";
@@ -65,6 +67,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
private CameraCaptureSession mCaptureSession; private CameraCaptureSession mCaptureSession;
private CaptureRequest mPreviewRequest; private CaptureRequest mPreviewRequest;
private Size mPreviewSize; private Size mPreviewSize;
private ParticleCollection.Listener mListener;
// View used to contain the circular cutout and enrollment animation drawable // View used to contain the circular cutout and enrollment animation drawable
private ImageView mCircleView; private ImageView mCircleView;
@@ -75,6 +78,15 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
// Texture used for showing the camera preview // Texture used for showing the camera preview
private FaceSquareTextureView mTextureView; private FaceSquareTextureView mTextureView;
// Listener sent to the animation drawable
private final ParticleCollection.Listener mAnimationListener
= new ParticleCollection.Listener() {
@Override
public void onEnrolled() {
mListener.onEnrolled();
}
};
private final TextureView.SurfaceTextureListener mSurfaceTextureListener = private final TextureView.SurfaceTextureListener mSurfaceTextureListener =
new TextureView.SurfaceTextureListener() { new TextureView.SurfaceTextureListener() {
@@ -185,7 +197,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
// Must disable hardware acceleration for this view, otherwise transparency breaks // Must disable hardware acceleration for this view, otherwise transparency breaks
mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mAnimationDrawable = new FaceEnrollAnimationDrawable(); mAnimationDrawable = new FaceEnrollAnimationDrawable(getContext(), mAnimationListener);
mCircleView.setImageDrawable(mAnimationDrawable); mCircleView.setImageDrawable(mAnimationDrawable);
mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE);
@@ -212,6 +224,25 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
closeCamera(); closeCamera();
} }
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
mAnimationDrawable.onEnrollmentError(errMsgId, errString);
}
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
mAnimationDrawable.onEnrollmentHelp(helpMsgId, helpString);
}
@Override
public void onEnrollmentProgressChange(int steps, int remaining) {
mAnimationDrawable.onEnrollmentProgressChange(steps, remaining);
}
public void setListener(ParticleCollection.Listener listener) {
mListener = listener;
}
/** /**
* Sets up member variables related to camera. * Sets up member variables related to camera.
* *

View File

@@ -0,0 +1,143 @@
/*
* 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.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import com.android.settings.R;
import com.android.settings.biometrics.BiometricEnrollSidecar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Class that's used to create, maintain, and update the state of each animation particle. Particles
* should have their colors assigned based on their index. Particles are split into primary and
* secondary types - primary types animate twice as fast during the completion effect. The particles
* are updated/drawn in a special order so that the overlap is correct during the final completion
* effect.
*/
public class ParticleCollection implements BiometricEnrollSidecar.Listener {
private static final String TAG = "AnimationController";
private static final int NUM_PARTICLES = 12;
public static final int STATE_STARTED = 1; // dots are rotating
public static final int STATE_STOPPED_COLORFUL = 2; // dots are not rotating but colorful
public static final int STATE_STOPPED_GRAY = 3; // dots are not rotating and also gray (error)
public static final int STATE_COMPLETE = 4; // face is enrolled
private final List<AnimationParticle> mParticleList;
private final List<Integer> mPrimariesInProgress; // primary particles not done animating yet
private int mState;
private Listener mListener;
public interface Listener {
void onEnrolled();
}
private final AnimationParticle.Listener mParticleListener = new AnimationParticle.Listener() {
@Override
public void onRingCompleted(int index) {
final boolean wasEmpty = mPrimariesInProgress.isEmpty();
// We can stop the time animator once the three primary particles have finished
for (int i = 0; i < mPrimariesInProgress.size(); i++) {
if (mPrimariesInProgress.get(i).intValue() == index) {
mPrimariesInProgress.remove(i);
break;
}
}
if (mPrimariesInProgress.isEmpty() && !wasEmpty) {
mListener.onEnrolled();
}
}
};
public ParticleCollection(Context context, Listener listener, Rect bounds, int borderWidth) {
mParticleList = new ArrayList<>();
mListener = listener;
final List<Integer> colors = new ArrayList<>();
final Resources.Theme theme = context.getTheme();
final Resources resources = context.getResources();
colors.add(resources.getColor(R.color.face_anim_particle_color_1, theme));
colors.add(resources.getColor(R.color.face_anim_particle_color_2, theme));
colors.add(resources.getColor(R.color.face_anim_particle_color_3, theme));
colors.add(resources.getColor(R.color.face_anim_particle_color_4, theme));
// Primary particles expand faster during the completion animation
mPrimariesInProgress = new ArrayList<>(Arrays.asList(0, 4, 8));
// Order in which to draw the particles. This is so the final "completion" animation has
// the correct behavior.
final int[] order = {3, 7, 11, 2, 6, 10, 1, 5, 9, 0, 4, 8};
for (int i = 0; i < NUM_PARTICLES; i++) {
AnimationParticle particle = new AnimationParticle(context, mParticleListener, bounds,
borderWidth, order[i], NUM_PARTICLES, colors);
if (mPrimariesInProgress.contains(order[i])) {
particle.setAsPrimary();
}
mParticleList.add(particle);
}
updateState(STATE_STARTED);
}
public void update(long t, long dt) {
for (int i = 0; i < mParticleList.size(); i++) {
mParticleList.get(i).update(t, dt);
}
}
public void draw(Canvas canvas) {
for (int i = 0; i < mParticleList.size(); i++) {
mParticleList.get(i).draw(canvas);
}
}
private void updateState(int state) {
if (mState != state) {
for (int i = 0; i < mParticleList.size(); i++) {
mParticleList.get(i).updateState(state);
}
mState = state;
}
}
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
}
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
}
@Override
public void onEnrollmentProgressChange(int steps, int remaining) {
if (remaining == 0) {
updateState(STATE_COMPLETE);
}
}
}

View File

@@ -245,7 +245,7 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
} }
@Override @Override
public void onEnrollmentHelp(CharSequence helpString) { public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) { if (!TextUtils.isEmpty(helpString)) {
mErrorText.removeCallbacks(mTouchAgainRunnable); mErrorText.removeCallbacks(mTouchAgainRunnable);
showError(helpString); showError(helpString);

View File

@@ -94,7 +94,7 @@ public class FingerprintEnrollFindSensor extends BiometricEnrollBase {
} }
@Override @Override
public void onEnrollmentHelp(CharSequence helpString) { public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
} }
@Override @Override