Transitions - Fix scrim flash as bouncer goes away

When tapping a notification and being prompted by the bouncer, the
animation should start from the notification and not from the
bouncer. Fade the bouncer away quickly to show the animation from the
keyguard. This was most noticeable in user switcher mode.

Fixes: 271823107
Test: atest PrimaryBouncerToGoneTransitionViewModelTest
Change-Id: I97e5883e962a3315f534b4d41d6a16b7ae107889
This commit is contained in:
Matt Pietal
2023-03-07 13:02:59 +00:00
parent 19f6c52f6f
commit 636f92393b
11 changed files with 212 additions and 38 deletions

View File

@@ -39,6 +39,7 @@ import static java.lang.Integer.max;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
@@ -1067,10 +1068,14 @@ public class KeyguardSecurityContainer extends ConstraintLayout {
int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);
AnimatorSet anims = new AnimatorSet();
ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
yAnim.setInterpolator(Interpolators.STANDARD_ACCELERATE);
yAnim.setDuration(500);
yAnim.start();
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
0f);
anims.setInterpolator(Interpolators.STANDARD_ACCELERATE);
anims.playTogether(alphaAnim, yAnim);
anims.start();
}
private void setupUserSwitcher() {

View File

@@ -127,6 +127,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
private View.OnKeyListener mOnKeyListener = (v, keyCode, event) -> interceptMediaKey(event);
private ActivityStarter.OnDismissAction mDismissAction;
private Runnable mCancelAction;
private boolean mWillRunDismissFromKeyguard;
private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
@@ -262,8 +263,10 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
// If there's a pending runnable because the user interacted with a widget
// and we're leaving keyguard, then run it.
boolean deferKeyguardDone = false;
mWillRunDismissFromKeyguard = false;
if (mDismissAction != null) {
deferKeyguardDone = mDismissAction.onDismiss();
mWillRunDismissFromKeyguard = mDismissAction.willRunAnimationOnKeyguard();
mDismissAction = null;
mCancelAction = null;
}
@@ -525,6 +528,13 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
return mDismissAction != null || mCancelAction != null;
}
/**
* @return will the dismissal run from the keyguard layout (instead of from bouncer)
*/
public boolean willRunDismissFromKeyguard() {
return mWillRunDismissFromKeyguard;
}
/**
* Remove any dismiss action or cancel action that was set.
*/

View File

@@ -52,6 +52,7 @@ interface BouncerViewDelegate {
cancelAction: Runnable?,
)
fun willDismissWithActions(): Boolean
fun willRunDismissFromKeyguard(): Boolean
/** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */
fun getBackCallback(): OnBackAnimationCallback
}

View File

@@ -380,6 +380,11 @@ constructor(
return primaryBouncerView.delegate?.willDismissWithActions() == true
}
/** Will the dismissal run from the keyguard layout (instead of from bouncer) */
fun willRunDismissFromKeyguard(): Boolean {
return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true
}
/** Returns whether the bouncer should be full screen. */
private fun needsFullscreenBouncer(): Boolean {
val mode: KeyguardSecurityModel.SecurityMode =

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 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.keyguard.shared.model
/** Alpha values for scrim updates */
data class ScrimAlpha(
val frontAlpha: Float = 0f,
val behindAlpha: Float = 0f,
val notificationsAlpha: Float = 0f,
)

View File

@@ -97,6 +97,10 @@ object KeyguardBouncerViewBinder {
override fun willDismissWithActions(): Boolean {
return securityContainerController.hasDismissActions()
}
override fun willRunDismissFromKeyguard(): Boolean {
return securityContainerController.willRunDismissFromKeyguard()
}
}
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {

View File

@@ -20,11 +20,14 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.model.ScrimAlpha
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.statusbar.SysuiStatusBarStateController
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Breaks down PRIMARY_BOUNCER->GONE transition into discrete steps for corresponding views to
@@ -36,6 +39,7 @@ class PrimaryBouncerToGoneTransitionViewModel
constructor(
private val interactor: KeyguardTransitionInteractor,
private val statusBarStateController: SysuiStatusBarStateController,
private val primaryBouncerInteractor: PrimaryBouncerInteractor,
) {
private val transitionAnimation =
KeyguardTransitionAnimationFlow(
@@ -44,26 +48,49 @@ constructor(
)
private var leaveShadeOpen: Boolean = false
private var willRunDismissFromKeyguard: Boolean = false
/** Bouncer container alpha */
val bouncerAlpha: Flow<Float> =
transitionAnimation.createFlow(
duration = 200.milliseconds,
onStep = { 1f - it },
)
/** Scrim behind alpha */
val scrimBehindAlpha: Flow<Float> =
transitionAnimation.createFlow(
duration = TO_GONE_DURATION,
interpolator = EMPHASIZED_ACCELERATE,
onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() },
onStart = {
willRunDismissFromKeyguard = primaryBouncerInteractor.willRunDismissFromKeyguard()
},
onStep = {
if (leaveShadeOpen) {
1f
if (willRunDismissFromKeyguard) {
0f
} else {
1f - it
}
},
)
/** Scrim alpha values */
val scrimAlpha: Flow<ScrimAlpha> =
transitionAnimation
.createFlow(
duration = TO_GONE_DURATION,
interpolator = EMPHASIZED_ACCELERATE,
onStart = {
leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
willRunDismissFromKeyguard =
primaryBouncerInteractor.willRunDismissFromKeyguard()
},
onStep = { 1f - it },
)
.map {
if (willRunDismissFromKeyguard) {
ScrimAlpha(
notificationsAlpha = 1f,
)
} else if (leaveShadeOpen) {
ScrimAlpha(
behindAlpha = 1f,
notificationsAlpha = 1f,
)
} else {
ScrimAlpha(behindAlpha = it)
}
}
}

View File

@@ -59,13 +59,13 @@ import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.keyguard.shared.model.ScrimAlpha;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.stack.ViewState;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -209,7 +209,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
private final ScreenOffAnimationController mScreenOffAnimationController;
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private final SysuiStatusBarStateController mStatusBarStateController;
private GradientColors mColors;
private boolean mNeedsDrawableColorUpdate;
@@ -270,12 +269,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
private CoroutineDispatcher mMainDispatcher;
private boolean mIsBouncerToGoneTransitionRunning = false;
private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
private final Consumer<Float> mScrimAlphaConsumer =
(Float alpha) -> {
private final Consumer<ScrimAlpha> mScrimAlphaConsumer =
(ScrimAlpha alphas) -> {
mInFrontAlpha = alphas.getFrontAlpha();
mScrimInFront.setViewAlpha(mInFrontAlpha);
mNotificationsAlpha = alphas.getNotificationsAlpha();
mNotificationsScrim.setViewAlpha(mNotificationsAlpha);
mBehindAlpha = alpha;
mScrimBehind.setViewAlpha(alpha);
mBehindAlpha = alphas.getBehindAlpha();
mScrimBehind.setViewAlpha(mBehindAlpha);
};
Consumer<TransitionStep> mPrimaryBouncerToGoneTransition;
@@ -297,7 +300,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
KeyguardTransitionInteractor keyguardTransitionInteractor,
SysuiStatusBarStateController sysuiStatusBarStateController,
@Main CoroutineDispatcher mainDispatcher,
LargeScreenShadeInterpolator largeScreenShadeInterpolator,
FeatureFlags featureFlags) {
@@ -309,7 +311,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
mKeyguardStateController = keyguardStateController;
mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mStatusBarStateController = sysuiStatusBarStateController;
mKeyguardVisibilityCallback = new KeyguardVisibilityCallback();
mHandler = handler;
mMainExecutor = mainExecutor;
@@ -409,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
collectFlow(behindScrim, mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition(),
mPrimaryBouncerToGoneTransition, mMainDispatcher);
collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha(),
collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha(),
mScrimAlphaConsumer, mMainDispatcher);
}
@@ -1114,8 +1115,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
mBehindAlpha = 1;
}
// Prevent notification scrim flicker when transitioning away from keyguard.
if (mKeyguardStateController.isKeyguardGoingAway()
&& !mStatusBarStateController.leaveOpenOnKeyguardHide()) {
if (mKeyguardStateController.isKeyguardGoingAway()) {
mNotificationsAlpha = 0;
}

View File

@@ -518,6 +518,38 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
assertTrue("Action should exist", mKeyguardSecurityContainerController.hasDismissActions());
}
@Test
public void testWillRunDismissFromKeyguardIsTrue() {
ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class);
when(action.willRunAnimationOnKeyguard()).thenReturn(true);
mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */);
mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */);
assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isTrue();
}
@Test
public void testWillRunDismissFromKeyguardIsFalse() {
ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class);
when(action.willRunAnimationOnKeyguard()).thenReturn(false);
mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */);
mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */);
assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse();
}
@Test
public void testWillRunDismissFromKeyguardIsFalseWhenNoDismissActionSet() {
mKeyguardSecurityContainerController.setOnDismissAction(null /* action */,
null /* cancelAction */);
mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */);
assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse();
}
@Test
public void testOnStartingToHide() {
mKeyguardSecurityContainerController.onStartingToHide();

View File

@@ -21,7 +21,9 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.ScrimAlpha
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -44,21 +46,86 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
private lateinit var underTest: PrimaryBouncerToGoneTransitionViewModel
private lateinit var repository: FakeKeyguardTransitionRepository
@Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
@Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
repository = FakeKeyguardTransitionRepository()
val interactor = KeyguardTransitionInteractor(repository)
underTest = PrimaryBouncerToGoneTransitionViewModel(interactor, statusBarStateController)
underTest =
PrimaryBouncerToGoneTransitionViewModel(
interactor,
statusBarStateController,
primaryBouncerInteractor
)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(false)
whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false)
}
@Test
fun bouncerAlpha() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val job = underTest.bouncerAlpha.onEach { values.add(it) }.launchIn(this)
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.6f))
assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
job.cancel()
}
@Test
fun bouncerAlpha_runDimissFromKeyguard() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val job = underTest.bouncerAlpha.onEach { values.add(it) }.launchIn(this)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.6f))
assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isEqualTo(0f) }
job.cancel()
}
@Test
fun scrimAlpha_runDimissFromKeyguard() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<ScrimAlpha>()
val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
repository.sendTransitionStep(step(0.3f))
repository.sendTransitionStep(step(0.6f))
repository.sendTransitionStep(step(1f))
assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isEqualTo(ScrimAlpha(notificationsAlpha = 1f)) }
job.cancel()
}
@Test
fun scrimBehindAlpha_leaveShadeOpen() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val values = mutableListOf<ScrimAlpha>()
val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this)
val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this)
whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(true)
@@ -68,7 +135,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
repository.sendTransitionStep(step(1f))
assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isEqualTo(1f) }
values.forEach {
assertThat(it).isEqualTo(ScrimAlpha(notificationsAlpha = 1f, behindAlpha = 1f))
}
job.cancel()
}
@@ -76,9 +145,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
@Test
fun scrimBehindAlpha_doNotLeaveShadeOpen() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val values = mutableListOf<ScrimAlpha>()
val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this)
val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this)
whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false)
@@ -88,8 +157,10 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
repository.sendTransitionStep(step(1f))
assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
assertThat(values[3]).isEqualTo(0f)
values.forEach { assertThat(it.notificationsAlpha).isEqualTo(0f) }
values.forEach { assertThat(it.frontAlpha).isEqualTo(0f) }
values.forEach { assertThat(it.behindAlpha).isIn(Range.closed(0f, 1f)) }
assertThat(values[3].behindAlpha).isEqualTo(0f)
job.cancel()
}

View File

@@ -71,7 +71,6 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.policy.FakeConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -134,7 +133,6 @@ public class ScrimControllerTest extends SysuiTestCase {
@Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Mock private CoroutineDispatcher mMainDispatcher;
@Mock private SysuiStatusBarStateController mSysuiStatusBarStateController;
// TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
// event-dispatch-on-registration pattern caused some of these unit tests to fail.)
@@ -249,7 +247,7 @@ public class ScrimControllerTest extends SysuiTestCase {
when(mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition())
.thenReturn(emptyFlow());
when(mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha())
when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha())
.thenReturn(emptyFlow());
mScrimController = new ScrimController(
@@ -268,7 +266,6 @@ public class ScrimControllerTest extends SysuiTestCase {
mStatusBarKeyguardViewManager,
mPrimaryBouncerToGoneTransitionViewModel,
mKeyguardTransitionInteractor,
mSysuiStatusBarStateController,
mMainDispatcher,
mLinearLargeScreenShadeInterpolator,
mFeatureFlags);
@@ -984,7 +981,6 @@ public class ScrimControllerTest extends SysuiTestCase {
mStatusBarKeyguardViewManager,
mPrimaryBouncerToGoneTransitionViewModel,
mKeyguardTransitionInteractor,
mSysuiStatusBarStateController,
mMainDispatcher,
mLinearLargeScreenShadeInterpolator,
mFeatureFlags);