Fade out/in header with customizer in/out
As the QSPanel doesn't reach the top of the screen, the LargeScreenShadeHeader peeks out from behind the Edit screen in certain configurations. In order to hide it, we fade it out/in together with the circular reveal. Test: manual Test: atest NotificationQSContainerControllerTest Test: atest LargeScreenShadeHeaderControllerTest Test: atest LargeScreenShadeHeaderControllerCombinedTest Fixes: 244455245 Change-Id: Ie8b8fb07b28e18b9620a948fbb6589394c4224d6
This commit is contained in:
@@ -3,7 +3,9 @@ package com.android.systemui.plugins.qs
|
||||
interface QSContainerController {
|
||||
fun setCustomizerAnimating(animating: Boolean)
|
||||
|
||||
fun setCustomizerShowing(showing: Boolean)
|
||||
fun setCustomizerShowing(showing: Boolean) = setCustomizerShowing(showing, 0L)
|
||||
|
||||
fun setCustomizerShowing(showing: Boolean, animationDuration: Long)
|
||||
|
||||
fun setDetailShowing(showing: Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,15 @@ public class QSDetailClipper {
|
||||
mBackground = (TransitionDrawable) detail.getBackground();
|
||||
}
|
||||
|
||||
public void animateCircularClip(int x, int y, boolean in, AnimatorListener listener) {
|
||||
updateCircularClip(true /* animate */, x, y, in, listener);
|
||||
/**
|
||||
* @param x x position where animation should originate
|
||||
* @param y y position where animation should originate
|
||||
* @param in whether animating in or out
|
||||
* @param listener Animation listener. Called whether or not {@code animate} is true.
|
||||
* @return the duration of the circular animator
|
||||
*/
|
||||
public long animateCircularClip(int x, int y, boolean in, AnimatorListener listener) {
|
||||
return updateCircularClip(true /* animate */, x, y, in, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,8 +57,9 @@ public class QSDetailClipper {
|
||||
* @param y y position where animation should originate
|
||||
* @param in whether animating in or out
|
||||
* @param listener Animation listener. Called whether or not {@code animate} is true.
|
||||
* @return the duration of the circular animator
|
||||
*/
|
||||
public void updateCircularClip(boolean animate, int x, int y, boolean in,
|
||||
public long updateCircularClip(boolean animate, int x, int y, boolean in,
|
||||
AnimatorListener listener) {
|
||||
if (mAnimator != null) {
|
||||
mAnimator.cancel();
|
||||
@@ -87,6 +95,7 @@ public class QSDetailClipper {
|
||||
mAnimator.addListener(mGoneOnEnd);
|
||||
}
|
||||
mAnimator.start();
|
||||
return mAnimator.getDuration();
|
||||
}
|
||||
|
||||
private final Runnable mReverseBackground = new Runnable() {
|
||||
|
||||
@@ -125,9 +125,10 @@ public class QSCustomizer extends LinearLayout {
|
||||
isShown = true;
|
||||
mOpening = true;
|
||||
setVisibility(View.VISIBLE);
|
||||
mClipper.animateCircularClip(mX, mY, true, new ExpandAnimatorListener(tileAdapter));
|
||||
long duration = mClipper.animateCircularClip(
|
||||
mX, mY, true, new ExpandAnimatorListener(tileAdapter));
|
||||
mQsContainerController.setCustomizerAnimating(true);
|
||||
mQsContainerController.setCustomizerShowing(true);
|
||||
mQsContainerController.setCustomizerShowing(true, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,13 +154,14 @@ public class QSCustomizer extends LinearLayout {
|
||||
// Make sure we're not opening (because we're closing). Nobody can think we are
|
||||
// customizing after the next two lines.
|
||||
mOpening = false;
|
||||
long duration = 0;
|
||||
if (animate) {
|
||||
mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener);
|
||||
duration = mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener);
|
||||
} else {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
mQsContainerController.setCustomizerAnimating(animate);
|
||||
mQsContainerController.setCustomizerShowing(false);
|
||||
mQsContainerController.setCustomizerShowing(false, duration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.Interpolators
|
||||
import com.android.systemui.animation.ShadeInterpolation
|
||||
import com.android.systemui.battery.BatteryMeterView
|
||||
import com.android.systemui.battery.BatteryMeterViewController
|
||||
@@ -310,6 +311,14 @@ class LargeScreenShadeHeaderController @Inject constructor(
|
||||
updateVisibility()
|
||||
}
|
||||
|
||||
fun startCustomizingAnimation(show: Boolean, duration: Long) {
|
||||
header.animate()
|
||||
.setDuration(duration)
|
||||
.alpha(if (show) 0f else 1f)
|
||||
.setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun loadConstraints() {
|
||||
if (header is MotionLayout) {
|
||||
// Use resources.getXml instead of passing the resource id due to bug b/205018300
|
||||
|
||||
@@ -35,6 +35,7 @@ class NotificationsQSContainerController @Inject constructor(
|
||||
view: NotificationsQuickSettingsContainer,
|
||||
private val navigationModeController: NavigationModeController,
|
||||
private val overviewProxyService: OverviewProxyService,
|
||||
private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
|
||||
private val featureFlags: FeatureFlags,
|
||||
@Main private val delayableExecutor: DelayableExecutor
|
||||
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
|
||||
@@ -156,9 +157,12 @@ class NotificationsQSContainerController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setCustomizerShowing(showing: Boolean) {
|
||||
isQSCustomizing = showing
|
||||
updateBottomSpacing()
|
||||
override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
|
||||
if (showing != isQSCustomizing) {
|
||||
isQSCustomizing = showing
|
||||
largeScreenShadeHeaderController.startCustomizingAnimation(showing, animationDuration)
|
||||
updateBottomSpacing()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDetailShowing(showing: Boolean) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.graphics.Rect
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.view.DisplayCutout
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.WindowInsets
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
@@ -30,6 +31,7 @@ import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.Interpolators
|
||||
import com.android.systemui.animation.ShadeInterpolation
|
||||
import com.android.systemui.battery.BatteryMeterView
|
||||
import com.android.systemui.battery.BatteryMeterViewController
|
||||
@@ -64,6 +66,7 @@ import org.mockito.Answers
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.anyBoolean
|
||||
import org.mockito.Mockito.anyFloat
|
||||
import org.mockito.Mockito.anyInt
|
||||
@@ -614,6 +617,34 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateOutOnStartCustomizing() {
|
||||
val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
|
||||
val duration = 1000L
|
||||
whenever(view.animate()).thenReturn(animator)
|
||||
|
||||
controller.startCustomizingAnimation(show = true, duration)
|
||||
|
||||
verify(animator).setDuration(duration)
|
||||
verify(animator).alpha(0f)
|
||||
verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
|
||||
verify(animator).start()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateInOnEndCustomizing() {
|
||||
val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
|
||||
val duration = 1000L
|
||||
whenever(view.animate()).thenReturn(animator)
|
||||
|
||||
controller.startCustomizingAnimation(show = false, duration)
|
||||
|
||||
verify(animator).setDuration(duration)
|
||||
verify(animator).alpha(1f)
|
||||
verify(animator).setInterpolator(Interpolators.ALPHA_IN)
|
||||
verify(animator).start()
|
||||
}
|
||||
|
||||
private fun createWindowInsets(
|
||||
topCutout: Rect? = Rect()
|
||||
): WindowInsets {
|
||||
|
||||
@@ -4,10 +4,12 @@ import android.app.StatusBarManager
|
||||
import android.content.Context
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.TextView
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.Interpolators
|
||||
import com.android.systemui.animation.ShadeInterpolation
|
||||
import com.android.systemui.battery.BatteryMeterView
|
||||
import com.android.systemui.battery.BatteryMeterViewController
|
||||
@@ -29,8 +31,10 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Answers
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyZeroInteractions
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
@@ -198,4 +202,32 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
|
||||
context.getString(com.android.internal.R.string.status_bar_alarm_clock)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateOutOnStartCustomizing() {
|
||||
val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
|
||||
val duration = 1000L
|
||||
whenever(view.animate()).thenReturn(animator)
|
||||
|
||||
mLargeScreenShadeHeaderController.startCustomizingAnimation(show = true, duration)
|
||||
|
||||
verify(animator).setDuration(duration)
|
||||
verify(animator).alpha(0f)
|
||||
verify(animator).setInterpolator(Interpolators.ALPHA_OUT)
|
||||
verify(animator).start()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateInOnEndCustomizing() {
|
||||
val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
|
||||
val duration = 1000L
|
||||
whenever(view.animate()).thenReturn(animator)
|
||||
|
||||
mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, duration)
|
||||
|
||||
verify(animator).setDuration(duration)
|
||||
verify(animator).alpha(1f)
|
||||
verify(animator).setInterpolator(Interpolators.ALPHA_IN)
|
||||
verify(animator).start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import java.util.function.Consumer
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -33,10 +34,10 @@ import org.mockito.Mockito.doNothing
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.function.Consumer
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@@ -63,6 +64,8 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
|
||||
@Mock
|
||||
private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController
|
||||
@Mock
|
||||
private lateinit var featureFlags: FeatureFlags
|
||||
@Captor
|
||||
lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
|
||||
@@ -92,6 +95,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
|
||||
notificationsQSContainer,
|
||||
navigationModeController,
|
||||
overviewProxyService,
|
||||
largeScreenShadeHeaderController,
|
||||
featureFlags,
|
||||
delayableExecutor
|
||||
)
|
||||
@@ -371,8 +375,14 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
|
||||
container.removeAllViews()
|
||||
container.addView(newViewWithId(1))
|
||||
container.addView(newViewWithId(View.NO_ID))
|
||||
val controller = NotificationsQSContainerController(container, navigationModeController,
|
||||
overviewProxyService, featureFlags, delayableExecutor)
|
||||
val controller = NotificationsQSContainerController(
|
||||
container,
|
||||
navigationModeController,
|
||||
overviewProxyService,
|
||||
largeScreenShadeHeaderController,
|
||||
featureFlags,
|
||||
delayableExecutor
|
||||
)
|
||||
controller.updateConstraints()
|
||||
|
||||
assertThat(container.getChildAt(0).id).isEqualTo(1)
|
||||
@@ -397,6 +407,21 @@ class NotificationQSContainerControllerTest : SysuiTestCase() {
|
||||
verify(notificationsQSContainer).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStartCustomizingWithDuration() {
|
||||
controller.setCustomizerShowing(true, 100L)
|
||||
verify(largeScreenShadeHeaderController).startCustomizingAnimation(true, 100L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEndCustomizingWithDuration() {
|
||||
controller.setCustomizerShowing(true, 0L) // Only tracks changes
|
||||
reset(largeScreenShadeHeaderController)
|
||||
|
||||
controller.setCustomizerShowing(false, 100L)
|
||||
verify(largeScreenShadeHeaderController).startCustomizingAnimation(false, 100L)
|
||||
}
|
||||
|
||||
private fun disableSplitShade() {
|
||||
setSplitShadeEnabled(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user