Update udfps icon on lockscreen and AOD

Use lottie to render udfps icons on AOD and lockscreen. On AOD, this
allows us to step through a dashed pattern to decrease burn-in.

On lockscreen, we use the lottie to animate the stroke width of the
icon.

Test: manual, atest SystemUITest
Bug: 178418596

Change-Id: Ie74c460c23b4283b9c6029b29b17ec9d0dd9b09f
This commit is contained in:
Beverly
2021-06-22 16:29:52 -04:00
parent d6910efffd
commit 46817e1227
10 changed files with 3648 additions and 176 deletions

View File

@@ -94,6 +94,7 @@ android_library {
"SystemUI-proto",
"dagger2",
"jsr330",
"lottie",
],
manifest: "AndroidManifest.xml",

View File

@@ -16,6 +16,7 @@
-->
<com.android.systemui.biometrics.UdfpsKeyguardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/udfps_animation_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -29,8 +30,28 @@
android:visibility="gone"/>
<!-- Fingerprint -->
<ImageView
android:id="@+id/udfps_keyguard_animation_fp_view"
<!-- AOD dashed fingerprint icon with moving dashes -->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/udfps_aod_fp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
app:lottie_autoPlay="false"
android:padding="16dp"
app:lottie_loop="true"
app:lottie_rawRes="@raw/udfps_aod_fp"/>
<!-- LockScreen fingerprint icon from 0 stroke width to full width -->
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/udfps_lockscreen_fp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
app:lottie_autoPlay="false"
app:lottie_loop="false"
android:padding="16dp"
app:lottie_rawRes="@raw/udfps_lockscreen_fp"/>
</com.android.systemui.biometrics.UdfpsKeyguardView>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,10 @@ abstract class UdfpsAnimationView extends FrameLayout {
return false;
}
protected void updateAlpha() {
/**
* @return current alpha
*/
protected int updateAlpha() {
int alpha = calculateAlpha();
getDrawable().setAlpha(alpha);
@@ -84,6 +87,8 @@ abstract class UdfpsAnimationView extends FrameLayout {
} else {
((ViewGroup) getParent()).setVisibility(View.VISIBLE);
}
return alpha;
}
int calculateAlpha() {

View File

@@ -42,11 +42,6 @@ public class UdfpsEnrollView extends UdfpsAnimationView {
mHandler = new Handler(Looper.getMainLooper());
}
@Override
protected void updateAlpha() {
super.updateAlpha();
}
@Override
protected void onFinishInflate() {
mFingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view);

View File

@@ -1,127 +0,0 @@
/*
* Copyright (C) 2021 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.systemui.biometrics;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.MathUtils;
import androidx.annotation.NonNull;
import com.android.internal.graphics.ColorUtils;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.doze.DozeReceiver;
/**
* UDFPS animations that should be shown when authenticating on keyguard.
*/
public class UdfpsKeyguardDrawable extends UdfpsDrawable implements DozeReceiver {
private static final String TAG = "UdfpsAnimationKeyguard";
private final int mAmbientDisplayColor;
static final float DEFAULT_AOD_STROKE_WIDTH = 1f;
@NonNull private final Context mContext;
private int mLockScreenColor;
// AOD anti-burn-in offsets
private final int mMaxBurnInOffsetX;
private final int mMaxBurnInOffsetY;
private float mInterpolatedDarkAmount;
private float mBurnInOffsetX;
private float mBurnInOffsetY;
private final ValueAnimator mHintAnimator = ValueAnimator.ofFloat(
UdfpsKeyguardDrawable.DEFAULT_STROKE_WIDTH,
.5f,
UdfpsKeyguardDrawable.DEFAULT_STROKE_WIDTH);
UdfpsKeyguardDrawable(@NonNull Context context) {
super(context);
mContext = context;
mMaxBurnInOffsetX = context.getResources()
.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
mMaxBurnInOffsetY = context.getResources()
.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
mHintAnimator.setDuration(2000);
mHintAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mHintAnimator.addUpdateListener(anim -> setStrokeWidth((float) anim.getAnimatedValue()));
mLockScreenColor = Utils.getColorAttrDefaultColor(mContext,
R.attr.wallpaperTextColorAccent);
mAmbientDisplayColor = Color.WHITE;
updateIcon();
}
private void updateIcon() {
mBurnInOffsetX = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
- mMaxBurnInOffsetX,
mInterpolatedDarkAmount);
mBurnInOffsetY = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
- mMaxBurnInOffsetY,
mInterpolatedDarkAmount);
mFingerprintDrawable.setTint(ColorUtils.blendARGB(mLockScreenColor,
mAmbientDisplayColor, mInterpolatedDarkAmount));
setStrokeWidth(MathUtils.lerp(DEFAULT_STROKE_WIDTH, DEFAULT_AOD_STROKE_WIDTH,
mInterpolatedDarkAmount));
invalidateSelf();
}
@Override
public void dozeTimeTick() {
updateIcon();
}
@Override
public void draw(@NonNull Canvas canvas) {
if (isIlluminationShowing()) {
return;
}
canvas.save();
canvas.translate(mBurnInOffsetX, mBurnInOffsetY);
mFingerprintDrawable.draw(canvas);
canvas.restore();
}
void animateHint() {
mHintAnimator.start();
}
void onDozeAmountChanged(float linear, float eased) {
mHintAnimator.cancel();
mInterpolatedDarkAmount = eased;
updateIcon();
}
void setLockScreenColor(int color) {
if (mLockScreenColor == color) return;
mLockScreenColor = color;
updateIcon();
}
}

View File

@@ -16,6 +16,9 @@
package com.android.systemui.biometrics;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -23,7 +26,10 @@ import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.util.AttributeSet;
import android.util.MathUtils;
import android.view.View;
import android.widget.ImageView;
@@ -34,12 +40,17 @@ import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.statusbar.StatusBarState;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
/**
* View corresponding with udfps_keyguard_view.xml
*/
public class UdfpsKeyguardView extends UdfpsAnimationView {
private final UdfpsKeyguardDrawable mFingerprintDrawable;
private ImageView mFingerprintView;
private UdfpsDrawable mFingerprintDrawable; // placeholder
private LottieAnimationView mAodFp;
private LottieAnimationView mLockScreenFp;
private int mUdfpsBouncerColor;
private int mWallpaperTextColor;
private int mStatusBarState;
@@ -52,16 +63,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
private AnimatorSet mAnimatorSet;
private int mAlpha; // 0-255
// AOD anti-burn-in offsets
private final int mMaxBurnInOffsetX;
private final int mMaxBurnInOffsetY;
private float mBurnInOffsetX;
private float mBurnInOffsetY;
private float mBurnInProgress;
private float mInterpolatedDarkAmount;
private ValueAnimator mHintAnimator;
public UdfpsKeyguardView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mFingerprintDrawable = new UdfpsKeyguardDrawable(mContext);
mFingerprintDrawable = new UdfpsFpDrawable(context);
mMaxBurnInOffsetX = context.getResources()
.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
mMaxBurnInOffsetY = context.getResources()
.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFingerprintView = findViewById(R.id.udfps_keyguard_animation_fp_view);
mFingerprintView.setForeground(mFingerprintDrawable);
mAodFp = findViewById(R.id.udfps_aod_fp);
mLockScreenFp = findViewById(R.id.udfps_lockscreen_fp);
mBgProtection = findViewById(R.id.udfps_keyguard_fp_bg);
@@ -69,7 +95,16 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
R.attr.wallpaperTextColorAccent);
mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext,
android.R.attr.textColorPrimary);
// requires call to invalidate to update the color (see #updateColor)
mLockScreenFp.addValueCallback(
new KeyPath("**"), LottieProperty.COLOR_FILTER,
frameInfo -> new PorterDuffColorFilter(getColor(), PorterDuff.Mode.SRC_ATOP)
);
mUdfpsRequested = false;
mHintAnimator = ObjectAnimator.ofFloat(mLockScreenFp, "progress", 1f, 0f, 1f);
mHintAnimator.setDuration(4000);
}
@Override
@@ -89,10 +124,27 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
@Override
public boolean dozeTimeTick() {
mFingerprintDrawable.dozeTimeTick();
updateBurnInOffsets();
return true;
}
private void updateBurnInOffsets() {
mBurnInOffsetX = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
- mMaxBurnInOffsetX, mInterpolatedDarkAmount);
mBurnInOffsetY = MathUtils.lerp(0f,
getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
- mMaxBurnInOffsetY, mInterpolatedDarkAmount);
mBurnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount);
mAodFp.setTranslationX(mBurnInOffsetX);
mAodFp.setTranslationY(mBurnInOffsetY);
mAodFp.setProgress(mBurnInProgress);
mLockScreenFp.setTranslationX(mBurnInOffsetX);
mLockScreenFp.setTranslationY(mBurnInOffsetY);
}
void requestUdfps(boolean request, int color) {
if (request) {
mUdfpsRequestedColor = color;
@@ -105,22 +157,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
void setStatusBarState(int statusBarState) {
mStatusBarState = statusBarState;
updateColor();
}
void updateColor() {
mFingerprintView.setAlpha(1f);
mFingerprintDrawable.setLockScreenColor(getColor());
mLockScreenFp.invalidate();
}
private boolean showingUdfpsBouncer() {
return mBgProtection.getVisibility() == View.VISIBLE;
}
private int getColor() {
if (mUdfpsRequested && mUdfpsRequestedColor != -1) {
if (isUdfpsColorRequested()) {
return mUdfpsRequestedColor;
} else if (showingUdfpsBouncer()) {
return mUdfpsBouncerColor;
} else {
return mWallpaperTextColor;
}
}
private boolean isUdfpsColorRequested() {
return mUdfpsRequested && mUdfpsRequestedColor != -1;
}
/**
* @param alpha between 0 and 255
*/
@@ -129,6 +190,13 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
updateAlpha();
}
@Override
protected int updateAlpha() {
int alpha = super.updateAlpha();
mLockScreenFp.setImageAlpha(alpha);
return alpha;
}
@Override
int calculateAlpha() {
if (mPauseAuth) {
@@ -138,18 +206,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
}
void onDozeAmountChanged(float linear, float eased) {
mFingerprintDrawable.onDozeAmountChanged(linear, eased);
mHintAnimator.cancel();
mInterpolatedDarkAmount = eased;
updateBurnInOffsets();
mLockScreenFp.setProgress(1f - mInterpolatedDarkAmount);
mAodFp.setAlpha(mInterpolatedDarkAmount);
if (linear == 1f) {
mLockScreenFp.setVisibility(View.INVISIBLE);
} else {
mLockScreenFp.setVisibility(View.VISIBLE);
}
}
void animateHint() {
mFingerprintDrawable.animateHint();
if (!isShadeLocked() && !mUdfpsRequested && mAlpha == 255
&& mLockScreenFp.isVisibleToUser()) {
mHintAnimator.start();
}
}
/**
* Animates in the bg protection circle behind the fp icon to highlight the icon.
*/
void animateUdfpsBouncer(Runnable onEndAnimation) {
if (mBgProtection.getVisibility() == View.VISIBLE && mBgProtection.getAlpha() == 1f) {
if (showingUdfpsBouncer() && mBgProtection.getAlpha() == 1f) {
// already fully highlighted, don't re-animate
return;
}
@@ -157,19 +238,6 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
if (mAnimatorSet != null) {
mAnimatorSet.cancel();
}
ValueAnimator fpIconAnim;
if (isShadeLocked()) {
// set color and fade in since we weren't showing before
mFingerprintDrawable.setLockScreenColor(mTextColorPrimary);
fpIconAnim = ObjectAnimator.ofFloat(mFingerprintView, View.ALPHA, 0f, 1f);
} else {
// update icon color
fpIconAnim = new ValueAnimator();
fpIconAnim.setIntValues(getColor(), mTextColorPrimary);
fpIconAnim.setEvaluator(new ArgbEvaluator());
fpIconAnim.addUpdateListener(valueAnimator -> mFingerprintDrawable.setLockScreenColor(
(Integer) valueAnimator.getAnimatedValue()));
}
mAnimatorSet = new AnimatorSet();
mAnimatorSet.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
@@ -181,11 +249,31 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
}
});
ValueAnimator fpIconColorAnim;
if (isShadeLocked()) {
// set color and fade in since we weren't showing before
mUdfpsBouncerColor = mTextColorPrimary;
fpIconColorAnim = ValueAnimator.ofInt(0, 255);
fpIconColorAnim.addUpdateListener(valueAnimator ->
mLockScreenFp.setImageAlpha((int) valueAnimator.getAnimatedValue()));
} else {
// update icon color
fpIconColorAnim = new ValueAnimator();
fpIconColorAnim.setIntValues(
isUdfpsColorRequested() ? mUdfpsRequestedColor : mWallpaperTextColor,
mTextColorPrimary);
fpIconColorAnim.setEvaluator(ArgbEvaluator.getInstance());
fpIconColorAnim.addUpdateListener(valueAnimator -> {
mUdfpsBouncerColor = (int) valueAnimator.getAnimatedValue();
updateColor();
});
}
mAnimatorSet.playTogether(
ObjectAnimator.ofFloat(mBgProtection, View.ALPHA, 0f, 1f),
ObjectAnimator.ofFloat(mBgProtection, View.SCALE_X, 0f, 1f),
ObjectAnimator.ofFloat(mBgProtection, View.SCALE_Y, 0f, 1f),
fpIconAnim);
fpIconColorAnim);
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -197,15 +285,11 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
mAnimatorSet.start();
}
private boolean isShadeLocked() {
return mStatusBarState == StatusBarState.SHADE_LOCKED;
}
/**
* Animates out the bg protection circle behind the fp icon to unhighlight the icon.
*/
void animateAwayUdfpsBouncer(@Nullable Runnable onEndAnimation) {
if (mBgProtection.getVisibility() == View.GONE) {
if (!showingUdfpsBouncer()) {
// already hidden
return;
}
@@ -213,17 +297,25 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
if (mAnimatorSet != null) {
mAnimatorSet.cancel();
}
ValueAnimator fpIconAnim;
ValueAnimator fpIconColorAnim;
if (isShadeLocked()) {
// fade out
fpIconAnim = ObjectAnimator.ofFloat(mFingerprintView, View.ALPHA, 1f, 0f);
mUdfpsBouncerColor = mTextColorPrimary;
fpIconColorAnim = ValueAnimator.ofInt(255, 0);
fpIconColorAnim.addUpdateListener(valueAnimator ->
mLockScreenFp.setImageAlpha((int) valueAnimator.getAnimatedValue()));
} else {
// update icon color
fpIconAnim = new ValueAnimator();
fpIconAnim.setIntValues(mTextColorPrimary, getColor());
fpIconAnim.setEvaluator(new ArgbEvaluator());
fpIconAnim.addUpdateListener(valueAnimator -> mFingerprintDrawable.setLockScreenColor(
(Integer) valueAnimator.getAnimatedValue()));
fpIconColorAnim = new ValueAnimator();
fpIconColorAnim.setIntValues(
mTextColorPrimary,
isUdfpsColorRequested() ? mUdfpsRequestedColor : mWallpaperTextColor);
fpIconColorAnim.setEvaluator(ArgbEvaluator.getInstance());
fpIconColorAnim.addUpdateListener(valueAnimator -> {
mUdfpsBouncerColor = (int) valueAnimator.getAnimatedValue();
updateColor();
});
}
mAnimatorSet = new AnimatorSet();
@@ -231,7 +323,7 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
ObjectAnimator.ofFloat(mBgProtection, View.ALPHA, 1f, 0f),
ObjectAnimator.ofFloat(mBgProtection, View.SCALE_X, 1f, 0f),
ObjectAnimator.ofFloat(mBgProtection, View.SCALE_Y, 1f, 0f),
fpIconAnim);
fpIconColorAnim);
mAnimatorSet.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mAnimatorSet.setDuration(500);
@@ -244,10 +336,15 @@ public class UdfpsKeyguardView extends UdfpsAnimationView {
}
}
});
mAnimatorSet.start();
}
boolean isAnimating() {
return mAnimatorSet != null && mAnimatorSet.isRunning();
}
private boolean isShadeLocked() {
return mStatusBarState == StatusBarState.SHADE_LOCKED;
}
}

View File

@@ -316,6 +316,14 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud
}
}
public void onBiometricError(int msgId, String errString,
BiometricSourceType biometricSourceType) {
if (biometricSourceType == BiometricSourceType.FACE) {
// show udfps hint when face auth fails
showHint(true);
}
}
public void onBiometricAuthenticated(int userId,
BiometricSourceType biometricSourceType, boolean isStrongBiometric) {
if (biometricSourceType == BiometricSourceType.FACE) {

View File

@@ -22,6 +22,7 @@ private const val MILLIS_PER_MINUTES = 1000 * 60f
private const val BURN_IN_PREVENTION_PERIOD_Y = 521f
private const val BURN_IN_PREVENTION_PERIOD_X = 83f
private const val BURN_IN_PREVENTION_PERIOD_SCALE = 180f
private const val BURN_IN_PREVENTION_PERIOD_PROGRESS = 120f
/**
* Returns the translation offset that should be used to avoid burn in at
@@ -36,6 +37,15 @@ fun getBurnInOffset(amplitude: Int, xAxis: Boolean): Int {
if (xAxis) BURN_IN_PREVENTION_PERIOD_X else BURN_IN_PREVENTION_PERIOD_Y).toInt()
}
/**
* Returns a progress offset (between 0f and 1.0f) that should be used to avoid burn in at
* the current time.
*/
fun getBurnInProgressOffset(): Float {
return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
1f, BURN_IN_PREVENTION_PERIOD_PROGRESS)
}
/**
* Returns a value to scale a view in order to avoid burn in.
*/