7/n: Add enrollment animation
Fixes: 112005540 Test: Tested with ag/4749121 Change-Id: I7d51187f7b8b7a6c2c34c984740b76bc9fd89262
This commit is contained in:
@@ -136,5 +136,11 @@
|
||||
<color name="battery_maybe_color_dark">#fdd835</color> <!-- Material Yellow 600 -->
|
||||
<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>
|
||||
|
||||
|
@@ -37,7 +37,7 @@ import java.util.ArrayList;
|
||||
public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
|
||||
|
||||
public interface Listener {
|
||||
void onEnrollmentHelp(CharSequence helpString);
|
||||
void onEnrollmentHelp(int helpMsgId, CharSequence helpString);
|
||||
void onEnrollmentError(int errMsgId, CharSequence errString);
|
||||
void onEnrollmentProgressChange(int steps, int remaining);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
|
||||
|
||||
@Override
|
||||
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) {
|
||||
if (mListener != null) {
|
||||
mListener.onEnrollmentHelp(helpString);
|
||||
mListener.onEnrollmentHelp(helpMsgId, helpString);
|
||||
} else {
|
||||
mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
|
||||
}
|
||||
|
236
src/com/android/settings/biometrics/face/AnimationParticle.java
Normal file
236
src/com/android/settings/biometrics/face/AnimationParticle.java
Normal 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);
|
||||
}
|
||||
}
|
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.settings.biometrics.face;
|
||||
|
||||
import android.animation.TimeAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
@@ -26,16 +28,43 @@ import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
/**
|
||||
* A drawable containing the circle cutout.
|
||||
*/
|
||||
public class FaceEnrollAnimationDrawable extends Drawable {
|
||||
import com.android.settings.biometrics.BiometricEnrollSidecar;
|
||||
|
||||
/**
|
||||
* 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 final Paint mSquarePaint;
|
||||
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.setColor(Color.WHITE);
|
||||
mSquarePaint.setAntiAlias(true);
|
||||
@@ -46,9 +75,35 @@ public class FaceEnrollAnimationDrawable extends Drawable {
|
||||
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
|
||||
protected void onBoundsChange(Rect 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
|
||||
@@ -63,7 +118,10 @@ public class FaceEnrollAnimationDrawable extends Drawable {
|
||||
|
||||
// Clear a circle in the middle for the camera preview
|
||||
canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(),
|
||||
mBounds.height() / 2, mCircleCutoutPaint);
|
||||
mBounds.height() / 2 - BORDER_BOUNDS, mCircleCutoutPaint);
|
||||
|
||||
// Draw the animation
|
||||
mParticleCollection.draw(canvas);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
@@ -45,7 +45,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
private TextView mErrorText;
|
||||
private Interpolator mLinearOutSlowInInterpolator;
|
||||
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 {
|
||||
static FaceErrorDialog newInstance(CharSequence msg, int msgId) {
|
||||
@@ -87,7 +94,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
|
||||
if (shouldLaunchConfirmLock()) {
|
||||
launchConfirmLock(R.string.security_settings_face_preference_title,
|
||||
Utils.getFaceManagerOrNull(this).preEnroll());
|
||||
Utils.getFingerprintManagerOrNull(this).preEnroll());
|
||||
mShouldFinishOnStop = false;
|
||||
} else {
|
||||
startEnrollment();
|
||||
@@ -97,13 +104,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
@Override
|
||||
public void startEnrollment() {
|
||||
super.startEnrollment();
|
||||
mFaceCameraPreview = (FaceEnrollPreviewFragment) getSupportFragmentManager()
|
||||
mPreviewFragment = (FaceEnrollPreviewFragment) getSupportFragmentManager()
|
||||
.findFragmentByTag(TAG_FACE_PREVIEW);
|
||||
if (mFaceCameraPreview == null) {
|
||||
mFaceCameraPreview = new FaceEnrollPreviewFragment();
|
||||
getSupportFragmentManager().beginTransaction().add(mFaceCameraPreview, TAG_FACE_PREVIEW)
|
||||
if (mPreviewFragment == null) {
|
||||
mPreviewFragment = new FaceEnrollPreviewFragment();
|
||||
getSupportFragmentManager().beginTransaction().add(mPreviewFragment, TAG_FACE_PREVIEW)
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
mPreviewFragment.setListener(mListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,10 +140,11 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentHelp(CharSequence helpString) {
|
||||
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
|
||||
if (!TextUtils.isEmpty(helpString)) {
|
||||
showError(helpString);
|
||||
}
|
||||
mPreviewFragment.onEnrollmentHelp(helpMsgId, helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,6 +158,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
msgId = R.string.security_settings_face_enroll_error_generic_dialog_message;
|
||||
break;
|
||||
}
|
||||
mPreviewFragment.onEnrollmentError(errMsgId, errString);
|
||||
showErrorDialog(getText(msgId), errMsgId);
|
||||
}
|
||||
|
||||
@@ -157,6 +167,8 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining);
|
||||
}
|
||||
mPreviewFragment.onEnrollmentProgressChange(steps, remaining);
|
||||
|
||||
// TODO: Update the actual animation
|
||||
showError("Steps: " + steps + " Remaining: " + remaining);
|
||||
}
|
||||
|
@@ -38,6 +38,7 @@ import android.widget.ImageView;
|
||||
|
||||
import com.android.internal.logging.nano.MetricsProto;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.biometrics.BiometricEnrollSidecar;
|
||||
import com.android.settings.core.InstrumentedPreferenceFragment;
|
||||
|
||||
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
|
||||
* 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";
|
||||
|
||||
@@ -65,6 +67,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
|
||||
private CameraCaptureSession mCaptureSession;
|
||||
private CaptureRequest mPreviewRequest;
|
||||
private Size mPreviewSize;
|
||||
private ParticleCollection.Listener mListener;
|
||||
|
||||
// View used to contain the circular cutout and enrollment animation drawable
|
||||
private ImageView mCircleView;
|
||||
@@ -75,6 +78,15 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
|
||||
// Texture used for showing the camera preview
|
||||
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 =
|
||||
new TextureView.SurfaceTextureListener() {
|
||||
|
||||
@@ -185,7 +197,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
|
||||
// Must disable hardware acceleration for this view, otherwise transparency breaks
|
||||
mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
|
||||
mAnimationDrawable = new FaceEnrollAnimationDrawable();
|
||||
mAnimationDrawable = new FaceEnrollAnimationDrawable(getContext(), mAnimationListener);
|
||||
mCircleView.setImageDrawable(mAnimationDrawable);
|
||||
|
||||
mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE);
|
||||
@@ -212,6 +224,25 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
|
||||
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.
|
||||
*
|
||||
|
143
src/com/android/settings/biometrics/face/ParticleCollection.java
Normal file
143
src/com/android/settings/biometrics/face/ParticleCollection.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -245,7 +245,7 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentHelp(CharSequence helpString) {
|
||||
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
|
||||
if (!TextUtils.isEmpty(helpString)) {
|
||||
mErrorText.removeCallbacks(mTouchAgainRunnable);
|
||||
showError(helpString);
|
||||
|
@@ -94,7 +94,7 @@ public class FingerprintEnrollFindSensor extends BiometricEnrollBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentHelp(CharSequence helpString) {
|
||||
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Reference in New Issue
Block a user