Use PropertyAnimator for CUJ_SCREEN_OFF_SHOW_AOD

Previously ViewPropertyAnimator - e.g. View.animate() - and
PropertyAnimator were used at the same time for animations on the same
keyguard view. This made the state less predictable since one might
assume calling View.animate().cancel() would cancel all the
PropertyAnimators too (it does not). Since ViewPropertyAnimator was only
used for animating alpha, simplify things by removing usage of
View.animate() and replacing it with a new PropertyAnimator call for the
View.ALPHA property.

Also,

 - Mark KeyguardUserSwitcherController as deprecated

 - Throw IllegalArgumentException if KeyguardStatusView#animate() is
   called on a debuggable build

Test: capture perfetto trace while turning screen off
Test: turn screen off while on lock screen, observe that the fade
      and translation animations remain unchanged
from home screen
Bug: 262705829

Change-Id: I72d8f3986422e3c71a6f91b94e5692e929867737
This commit is contained in:
Peter Kalauskas
2023-04-07 12:49:56 -07:00
parent 857b09eb82
commit abf87cf331
7 changed files with 110 additions and 91 deletions

View File

@@ -19,10 +19,12 @@ package com.android.keyguard;
import static java.util.Collections.emptySet;
import android.content.Context;
import android.os.Build;
import android.os.Trace;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.GridLayout;
import com.android.systemui.R;
@@ -117,6 +119,16 @@ public class KeyguardStatusView extends GridLayout {
}
}
@Override
public ViewPropertyAnimator animate() {
if (Build.IS_DEBUGGABLE) {
throw new IllegalArgumentException(
"KeyguardStatusView does not support ViewPropertyAnimator. "
+ "Use PropertyAnimator instead.");
}
return super.animate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Trace.beginSection("KeyguardStatusView#onMeasure");

View File

@@ -18,8 +18,8 @@ package com.android.keyguard;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import android.util.Property;
import android.view.View;
import android.view.ViewPropertyAnimator;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.plugins.log.LogBuffer;
@@ -34,6 +34,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.google.errorprone.annotations.CompileTimeConstant;
import java.util.function.Consumer;
/**
* Helper class for updating visibility of keyguard views based on keyguard and status bar state.
* This logic is shared by both the keyguard status view and the keyguard user switcher.
@@ -83,47 +85,49 @@ public class KeyguardVisibilityHelper {
boolean keyguardFadingAway,
boolean goingToFullShade,
int oldStatusBarState) {
mView.animate().cancel();
PropertyAnimator.cancelAnimation(mView, AnimatableProperty.ALPHA);
boolean isOccluded = mKeyguardStateController.isOccluded();
mKeyguardViewVisibilityAnimating = false;
if ((!keyguardFadingAway && oldStatusBarState == KEYGUARD
&& statusBarState != KEYGUARD) || goingToFullShade) {
mKeyguardViewVisibilityAnimating = true;
mView.animate()
.alpha(0f)
.setStartDelay(0)
.setDuration(160)
.setInterpolator(Interpolators.ALPHA_OUT)
.withEndAction(
mAnimateKeyguardStatusViewGoneEndRunnable);
AnimationProperties animProps = new AnimationProperties()
.setCustomInterpolator(View.ALPHA, Interpolators.ALPHA_OUT)
.setAnimationEndAction(mSetGoneEndAction);
if (keyguardFadingAway) {
mView.animate()
.setStartDelay(mKeyguardStateController.getKeyguardFadingAwayDelay())
.setDuration(mKeyguardStateController.getShortenedFadingAwayDuration())
.start();
animProps
.setDelay(mKeyguardStateController.getKeyguardFadingAwayDelay())
.setDuration(mKeyguardStateController.getShortenedFadingAwayDuration());
log("goingToFullShade && keyguardFadingAway");
} else {
animProps.setDelay(0).setDuration(160);
log("goingToFullShade && !keyguardFadingAway");
}
PropertyAnimator.setProperty(
mView, AnimatableProperty.ALPHA, 0f, animProps, true /* animate */);
} else if (oldStatusBarState == StatusBarState.SHADE_LOCKED && statusBarState == KEYGUARD) {
mView.setVisibility(View.VISIBLE);
mKeyguardViewVisibilityAnimating = true;
mView.setAlpha(0f);
mView.animate()
.alpha(1f)
.setStartDelay(0)
.setDuration(320)
.setInterpolator(Interpolators.ALPHA_IN)
.withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
PropertyAnimator.setProperty(
mView, AnimatableProperty.ALPHA, 1f,
new AnimationProperties()
.setDelay(0)
.setDuration(320)
.setCustomInterpolator(View.ALPHA, Interpolators.ALPHA_IN)
.setAnimationEndAction(
property -> mSetVisibleEndRunnable.run()),
true /* animate */);
log("keyguardFadingAway transition w/ Y Aniamtion");
} else if (statusBarState == KEYGUARD) {
if (keyguardFadingAway) {
mKeyguardViewVisibilityAnimating = true;
ViewPropertyAnimator animator = mView.animate()
.alpha(0)
.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN)
.withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
AnimationProperties animProps = new AnimationProperties()
.setDelay(0)
.setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_LINEAR_IN)
.setAnimationEndAction(mSetInvisibleEndAction);
if (mAnimateYPos) {
float target = mView.getY() - mView.getHeight() * 0.05f;
int delay = 0;
@@ -135,13 +139,16 @@ public class KeyguardVisibilityHelper {
PropertyAnimator.setProperty(mView, AnimatableProperty.Y, target,
mAnimationProperties,
true /* animate */);
animator.setDuration(duration)
.setStartDelay(delay);
animProps.setDuration(duration)
.setDelay(delay);
log("keyguardFadingAway transition w/ Y Aniamtion");
} else {
log("keyguardFadingAway transition w/o Y Animation");
}
animator.start();
PropertyAnimator.setProperty(
mView, AnimatableProperty.ALPHA, 0f,
animProps,
true /* animate */);
} else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) {
log("ScreenOff transition");
mKeyguardViewVisibilityAnimating = true;
@@ -149,7 +156,7 @@ public class KeyguardVisibilityHelper {
// Ask the screen off animation controller to animate the keyguard visibility for us
// since it may need to be cancelled due to keyguard lifecycle events.
mScreenOffAnimationController.animateInKeyguard(
mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
mView, mSetVisibleEndRunnable);
} else {
log("Direct set Visibility to VISIBLE");
mView.setVisibility(View.VISIBLE);
@@ -163,19 +170,25 @@ public class KeyguardVisibilityHelper {
mLastOccludedState = isOccluded;
}
private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.INVISIBLE);
log("Callback Set Visibility to INVISIBLE");
private final Consumer<Property> mSetInvisibleEndAction = new Consumer<>() {
@Override
public void accept(Property property) {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.INVISIBLE);
log("Callback Set Visibility to INVISIBLE");
}
};
private final Runnable mAnimateKeyguardStatusViewGoneEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.GONE);
log("CallbackSet Visibility to GONE");
private final Consumer<Property> mSetGoneEndAction = new Consumer<>() {
@Override
public void accept(Property property) {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.GONE);
log("CallbackSet Visibility to GONE");
}
};
private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> {
private final Runnable mSetVisibleEndRunnable = () -> {
mKeyguardViewVisibilityAnimating = false;
mView.setVisibility(View.VISIBLE);
log("Callback Set Visibility to VISIBLE");

View File

@@ -48,6 +48,10 @@ public abstract class AnimatableProperty {
View.SCALE_Y, R.id.scale_y_animator_tag, R.id.scale_y_animator_start_value_tag,
R.id.scale_y_animator_end_value_tag);
public static final AnimatableProperty ALPHA = AnimatableProperty.from(
View.ALPHA, R.id.alpha_animator_tag, R.id.alpha_animator_start_value_tag,
R.id.alpha_animator_end_value_tag);
/**
* Similar to X, however this doesn't allow for any other modifications other than from this
* property. When using X, it's possible that the view is laid out during the animation,

View File

@@ -32,6 +32,7 @@ public class AnimationProperties {
public long duration;
public long delay;
private ArrayMap<Property, Interpolator> mInterpolatorMap;
private Consumer<Property> mAnimationCancelAction;
private Consumer<Property> mAnimationEndAction;
/**
@@ -50,27 +51,43 @@ public class AnimationProperties {
* @return a listener that will be added for a given property during its animation.
*/
public AnimatorListenerAdapter getAnimationFinishListener(Property property) {
if (mAnimationEndAction == null) {
if (mAnimationEndAction == null && mAnimationCancelAction == null) {
return null;
}
Consumer<Property> cancelAction = mAnimationCancelAction;
Consumer<Property> endAction = mAnimationEndAction;
return new AnimatorListenerAdapter() {
private boolean mCancelled;
@Override
public void onAnimationCancel(Animator animation) {
mCancelled = true;
mCancelled = true;
if (cancelAction != null) {
cancelAction.accept(property);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (!mCancelled) {
if (!mCancelled && endAction != null) {
endAction.accept(property);
}
}
};
}
/**
* Add a callback for animation cancellation.
*/
public AnimationProperties setAnimationCancelAction(Consumer<Property> listener) {
mAnimationCancelAction = listener;
return this;
}
/**
* Add a callback for animation ending successfully. The callback will not be called when the
* animations is cancelled.
*/
public AnimationProperties setAnimationEndAction(Consumer<Property> listener) {
mAnimationEndAction = listener;
return this;

View File

@@ -175,15 +175,19 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// We animate the Y properly separately using the PropertyAnimator, as the panel
// view also needs to update the end position.
PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY,
PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY,
AnimationProperties().setDuration(duration.toLong()),
true /* animate */)
keyguardView.animate()
// Cancel any existing CUJs before starting the animation
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
PropertyAnimator.setProperty(
keyguardView, AnimatableProperty.ALPHA, 1f,
AnimationProperties()
.setDelay(0)
.setDuration(duration.toLong())
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.alpha(1f)
.withEndAction {
.setAnimationEndAction {
aodUiAnimationPlaying = false
// Lock the keyguard if it was waiting for the screen off animation to end.
@@ -199,30 +203,23 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Done going to sleep, reset this flag.
decidedToAnimateGoingToSleep = null
// We need to unset the listener. These are persistent for future animators
keyguardView.animate().setListener(null)
interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
}
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationCancel(animation: Animator?) {
// If we're cancelled, reset state flags/listeners. The end action above
// will not be called, which is what we want since that will finish the
// screen off animation and show the lockscreen, which we don't want if we
// were cancelled.
aodUiAnimationPlaying = false
decidedToAnimateGoingToSleep = null
keyguardView.animate().setListener(null)
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
}
override fun onAnimationStart(animation: Animator?) {
interactionJankMonitor.begin(
mCentralSurfaces.notificationShadeWindowView,
CUJ_SCREEN_OFF_SHOW_AOD)
}
})
.start()
.setAnimationCancelAction {
// If we're cancelled, reset state flags/listeners. The end action above
// will not be called, which is what we want since that will finish the
// screen off animation and show the lockscreen, which we don't want if we
// were cancelled.
aodUiAnimationPlaying = false
decidedToAnimateGoingToSleep = null
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
}
.setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
true /* animate */)
interactionJankMonitor.begin(
mCentralSurfaces.notificationShadeWindowView,
CUJ_SCREEN_OFF_SHOW_AOD
)
}
override fun onStartedWakingUp() {

View File

@@ -61,6 +61,7 @@ import javax.inject.Inject;
* Manages the user switcher on the Keyguard.
*/
@KeyguardUserSwitcherScope
@Deprecated
public class KeyguardUserSwitcherController extends ViewController<KeyguardUserSwitcherView> {
private static final String TAG = "KeyguardUserSwitcherController";

View File

@@ -16,12 +16,10 @@
package com.android.systemui.statusbar.phone
import android.animation.Animator
import android.os.Handler
import android.os.PowerManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.SysuiTestCase
@@ -39,10 +37,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
@@ -111,27 +107,6 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() {
controller.onStartedWakingUp()
}
@Test
fun testAnimClearsEndListener() {
val keyguardView = View(context)
val animator = spy(keyguardView.animate())
val keyguardSpy = spy(keyguardView)
Mockito.`when`(keyguardSpy.animate()).thenReturn(animator)
val listener = ArgumentCaptor.forClass(Animator.AnimatorListener::class.java)
val endAction = ArgumentCaptor.forClass(Runnable::class.java)
controller.animateInKeyguard(keyguardSpy, Runnable {})
Mockito.verify(animator).setListener(listener.capture())
Mockito.verify(animator).withEndAction(endAction.capture())
// Verify that the listener is cleared if we cancel it.
listener.value.onAnimationCancel(null)
Mockito.verify(animator).setListener(null)
// Verify that the listener is also cleared if the end action is triggered.
endAction.value.run()
verify(animator, times(2)).setListener(null)
}
/**
* The AOD UI is shown during the screen off animation, after a delay to allow the light reveal
* animation to start. If the device is woken up during the screen off, we should *never* do