Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9117d9b authored by Kevin Chyn's avatar Kevin Chyn
Browse files

7/n: Add enrollment animation

Fixes: 112005540

Test: Tested with ag/4749121

Change-Id: I7d51187f7b8b7a6c2c34c984740b76bc9fd89262
parent 6ec72791
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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>
+3 −3
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+62 −4
Original line number Diff line number Diff line
@@ -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;

import com.android.settings.biometrics.BiometricEnrollSidecar;

/**
 * A drawable containing the circle cutout.
 * A drawable containing the circle cutout as well as the animations.
 */
public class FaceEnrollAnimationDrawable extends Drawable {
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();
    }
+19 −7
Original line number Diff line number Diff line
@@ -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);
    }
Loading