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:
Fabian Kozynski
2022-09-06 15:46:53 -04:00
parent 9fefe95ba6
commit c30c4e1412
8 changed files with 130 additions and 16 deletions

View File

@@ -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)
}
}

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -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)
}