Implement updated low light animations

The transition between low light and the user dream is currently the
only dream to dream transition. The prior dream does not get any time
to play animations as the default dream transition is played as soon as
the new dream starts.

This change allows an animation to play before setting the system
dream to low light for a smooth transition.

The overlay exit animation is reintroduced from ag/20377749.
The animation was removed since transitions between dreams and core
SysUI components is now centrally controlled by keyguard. Since this
transition is between two dreams, keyguard is not able to coordinate.

Screen capture from device: http://screencast/cast/NDU0NzAyNjIyMzEwNDAwMHwyNmU0ZWExNy0xNg

Bug: 260638159
Test: atest LowLightTransitionCoordinatorTest LowLightDreamManagerTest DreamOverlayAnimationsControllerTest DreamOverlayContainerViewControllerTest
Change-Id: Ib38a8848ea6361d3d6ef267f2f3a84d3daf6711d
This commit is contained in:
William Xiao
2023-02-23 15:02:02 -08:00
parent 6c7e455405
commit eba5951782
9 changed files with 366 additions and 18 deletions

View File

@@ -72,6 +72,7 @@ public final class LowLightDreamManager {
public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2;
private final DreamManager mDreamManager;
private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
@Nullable
private final ComponentName mLowLightDreamComponent;
@@ -81,8 +82,10 @@ public final class LowLightDreamManager {
@Inject
public LowLightDreamManager(
DreamManager dreamManager,
LowLightTransitionCoordinator lowLightTransitionCoordinator,
@Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) {
mDreamManager = dreamManager;
mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
mLowLightDreamComponent = lowLightDreamComponent;
}
@@ -111,7 +114,9 @@ public final class LowLightDreamManager {
mAmbientLightMode = ambientLightMode;
mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT
? mLowLightDreamComponent : null);
boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT;
mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight,
() -> mDreamManager.setSystemDreamComponent(
shouldEnterLowLight ? mLowLightDreamComponent : null));
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.dream.lowlight;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Helper class that allows listening and running animations before entering or exiting low light.
*/
@Singleton
public class LowLightTransitionCoordinator {
/**
* Listener that is notified before low light entry.
*/
public interface LowLightEnterListener {
/**
* Callback that is notified before the device enters low light.
*
* @return an optional animator that will be waited upon before entering low light.
*/
Animator onBeforeEnterLowLight();
}
/**
* Listener that is notified before low light exit.
*/
public interface LowLightExitListener {
/**
* Callback that is notified before the device exits low light.
*
* @return an optional animator that will be waited upon before exiting low light.
*/
Animator onBeforeExitLowLight();
}
private LowLightEnterListener mLowLightEnterListener;
private LowLightExitListener mLowLightExitListener;
@Inject
public LowLightTransitionCoordinator() {
}
/**
* Sets the listener for the low light enter event.
*
* Only one listener can be set at a time. This method will overwrite any previously set
* listener. Null can be used to unset the listener.
*/
public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) {
mLowLightEnterListener = lowLightEnterListener;
}
/**
* Sets the listener for the low light exit event.
*
* Only one listener can be set at a time. This method will overwrite any previously set
* listener. Null can be used to unset the listener.
*/
public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) {
mLowLightExitListener = lowLightExitListener;
}
/**
* Notifies listeners that the device is about to enter or exit low light.
*
* @param entering true if listeners should be notified before entering low light, false if this
* is notifying before exiting.
* @param callback callback that will be run after listeners complete.
*/
void notifyBeforeLowLightTransition(boolean entering, Runnable callback) {
Animator animator = null;
if (entering && mLowLightEnterListener != null) {
animator = mLowLightEnterListener.onBeforeEnterLowLight();
} else if (!entering && mLowLightExitListener != null) {
animator = mLowLightExitListener.onBeforeExitLowLight();
}
// If the listener returned an animator to indicate it was running an animation, run the
// callback after the animation completes, otherwise call the callback directly.
if (animator != null) {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
callback.run();
}
});
} else {
callback.run();
}
}
}

View File

@@ -21,7 +21,10 @@ import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE
import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -43,45 +46,53 @@ public class LowLightDreamManagerTest {
@Mock
private DreamManager mDreamManager;
@Mock
private LowLightTransitionCoordinator mTransitionCoordinator;
@Mock
private ComponentName mDreamComponent;
LowLightDreamManager mLowLightDreamManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// Automatically run any provided Runnable to mTransitionCoordinator to simplify testing.
doAnswer(invocation -> {
((Runnable) invocation.getArgument(1)).run();
return null;
}).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(),
any(Runnable.class));
mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator,
mDreamComponent);
}
@Test
public void setAmbientLightMode_lowLight_setSystemDream() {
final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
mDreamComponent);
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any());
verify(mDreamManager).setSystemDreamComponent(mDreamComponent);
}
@Test
public void setAmbientLightMode_regularLight_clearSystemDream() {
final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
mDreamComponent);
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any());
verify(mDreamManager).setSystemDreamComponent(null);
}
@Test
public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() {
final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
mDreamComponent);
// Set to low light first.
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
clearInvocations(mDreamManager);
// Return to default unknown mode.
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN);
mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN);
verify(mDreamManager).setSystemDreamComponent(null);
}
@@ -89,7 +100,7 @@ public class LowLightDreamManagerTest {
@Test
public void setAmbientLightMode_dreamComponentNotSet_doNothing() {
final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager,
null /*dream component*/);
mTransitionCoordinator, null /*dream component*/);
lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);

View File

@@ -0,0 +1,113 @@
/*
* 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.dream.lowlight;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.animation.Animator;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class LowLightTransitionCoordinatorTest {
@Mock
private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener;
@Mock
private LowLightTransitionCoordinator.LowLightExitListener mExitListener;
@Mock
private Animator mAnimator;
@Captor
private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor;
@Mock
private Runnable mRunnable;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void onEnterCalledOnListeners() {
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
coordinator.setLowLightEnterListener(mEnterListener);
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
verify(mEnterListener).onBeforeEnterLowLight();
verify(mRunnable).run();
}
@Test
public void onExitCalledOnListeners() {
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
coordinator.setLowLightExitListener(mExitListener);
coordinator.notifyBeforeLowLightTransition(false, mRunnable);
verify(mExitListener).onBeforeExitLowLight();
verify(mRunnable).run();
}
@Test
public void listenerNotCalledAfterRemoval() {
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
coordinator.setLowLightEnterListener(mEnterListener);
coordinator.setLowLightEnterListener(null);
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
verifyZeroInteractions(mEnterListener);
verify(mRunnable).run();
}
@Test
public void runnableCalledAfterAnimationEnds() {
when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator);
LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator();
coordinator.setLowLightEnterListener(mEnterListener);
coordinator.notifyBeforeLowLightTransition(true, mRunnable);
// Animator listener is added and the runnable is not run yet.
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
verifyZeroInteractions(mRunnable);
// Runnable is run once the animation ends.
mAnimatorListenerCaptor.getValue().onAnimationEnd(null);
verify(mRunnable).run();
}
}

View File

@@ -774,7 +774,7 @@
<!-- Duration in milliseconds of the dream in complications fade-in animation. -->
<integer name="config_dreamOverlayInComplicationsDurationMs">250</integer>
<!-- Duration in milliseconds of the y-translation animation when entering a dream -->
<integer name="config_dreamOverlayInTranslationYDurationMs">917</integer>
<integer name="config_dreamOverlayInTranslationYDurationMs">1167</integer>
<!-- Delay in milliseconds before switching to the dock user and dreaming if a secondary user is
active when the device is locked and docked. 0 indicates disabled. Default is 1 minute. -->

View File

@@ -174,6 +174,71 @@ constructor(
}
}
/**
* Starts the dream content and dream overlay exit animations.
*
* This should only be used when the low light dream is entering, animations to/from other SysUI
* views is controlled by `transitionViewModel`.
*/
// TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is
// done.
@JvmOverloads
fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator {
cancelAnimations()
mAnimator =
animatorBuilder().apply {
playTogether(
translationYAnimator(
from = 0f,
to = -mDreamInTranslationYDistance.toFloat(),
durationMs = mDreamInTranslationYDurationMs,
delayMs = 0,
interpolator = Interpolators.EMPHASIZED
),
alphaAnimator(
from =
mCurrentAlphaAtPosition.getOrDefault(
key = POSITION_BOTTOM,
defaultValue = 1f
),
to = 0f,
durationMs = mDreamInComplicationsAnimDurationMs,
delayMs = 0,
positions = POSITION_BOTTOM
)
.apply {
doOnEnd {
// The logical end of the animation is once the alpha and blur
// animations finish, end the animation so that any listeners are
// notified. The Y translation animation is much longer than all of
// the other animations due to how the spec is defined, but is not
// expected to run to completion.
mAnimator?.end()
}
},
alphaAnimator(
from =
mCurrentAlphaAtPosition.getOrDefault(
key = POSITION_TOP,
defaultValue = 1f
),
to = 0f,
durationMs = mDreamInComplicationsAnimDurationMs,
delayMs = 0,
positions = POSITION_TOP
)
)
doOnEnd {
mAnimator = null
mOverlayStateController.setExitAnimationsRunning(false)
}
start()
}
mOverlayStateController.setExitAnimationsRunning(true)
return mAnimator as AnimatorSet
}
/** Starts the dream content and dream overlay exit animations. */
fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) {
cancelAnimations()

View File

@@ -23,6 +23,7 @@ import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM;
import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP;
import android.animation.Animator;
import android.content.res.Resources;
import android.os.Handler;
import android.util.MathUtils;
@@ -31,6 +32,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.android.dream.lowlight.LowLightTransitionCoordinator;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.dagger.qualifiers.Main;
@@ -54,11 +56,14 @@ import javax.inject.Named;
* View controller for {@link DreamOverlayContainerView}.
*/
@DreamOverlayComponent.DreamOverlayScope
public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> {
public class DreamOverlayContainerViewController extends
ViewController<DreamOverlayContainerView> implements
LowLightTransitionCoordinator.LowLightEnterListener {
private final DreamOverlayStatusBarViewController mStatusBarViewController;
private final BlurUtils mBlurUtils;
private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
private final DreamOverlayStateController mStateController;
private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
private final ComplicationHostViewController mComplicationHostViewController;
@@ -164,6 +169,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
ComplicationHostViewController complicationHostViewController,
@Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView,
DreamOverlayStatusBarViewController statusBarViewController,
LowLightTransitionCoordinator lowLightTransitionCoordinator,
BlurUtils blurUtils,
@Main Handler handler,
@Main Resources resources,
@@ -181,6 +187,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
mBlurUtils = blurUtils;
mDreamOverlayAnimationsController = animationsController;
mStateController = stateController;
mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
mBouncerlessScrimController = bouncerlessScrimController;
mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
@@ -207,6 +214,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
mStatusBarViewController.init();
mComplicationHostViewController.init();
mDreamOverlayAnimationsController.init(mView);
mLowLightTransitionCoordinator.setLowLightEnterListener(this);
}
@Override
@@ -305,4 +313,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor);
}
@Override
public Animator onBeforeEnterLowLight() {
// Return the animator so that the transition coordinator waits for the overlay exit
// animations to finish before entering low light, as otherwise the default DreamActivity
// animation plays immediately and there's no time for this animation to play.
return mDreamOverlayAnimationsController.startExitAnimations();
}
}

View File

@@ -1,5 +1,6 @@
package com.android.systemui.dreams
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.testing.AndroidTestingRunner
@@ -11,6 +12,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransition
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.Dispatchers
@@ -73,6 +75,19 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
runBlocking(Dispatchers.Main.immediate) { controller.init(mockView) }
}
@Test
fun testExitAnimationUpdatesState() {
controller.startExitAnimations(animatorBuilder = { mockAnimator })
verify(stateController).setExitAnimationsRunning(true)
val captor = argumentCaptor<Animator.AnimatorListener>()
verify(mockAnimator).addListener(captor.capture())
captor.value.onAnimationEnd(mockAnimator)
verify(stateController).setExitAnimationsRunning(false)
}
@Test
fun testWakeUpCallsExecutor() {
val mockExecutor: DelayableExecutor = mock()

View File

@@ -34,6 +34,7 @@ import android.view.ViewTreeObserver;
import androidx.test.filters.SmallTest;
import com.android.dream.lowlight.LowLightTransitionCoordinator;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.complication.ComplicationHostViewController;
@@ -65,6 +66,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
@Mock
DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController;
@Mock
LowLightTransitionCoordinator mLowLightTransitionCoordinator;
@Mock
DreamOverlayContainerView mDreamOverlayContainerView;
@@ -110,6 +114,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
mComplicationHostViewController,
mDreamOverlayContentView,
mDreamOverlayStatusBarViewController,
mLowLightTransitionCoordinator,
mBlurUtils,
mHandler,
mResources,
@@ -234,6 +239,13 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
verify(mAnimationsController).startEntryAnimations(true);
}
@Test
public void testStartsExitAnimationsBeforeEnteringLowLight() {
mController.onBeforeEnterLowLight();
verify(mAnimationsController).startExitAnimations();
}
@Test
public void testCancelDreamEntryAnimationsOnDetached() {
mController.onViewAttached();