Loading packages/SystemUI/res/layout/qs_footer_impl.xml +2 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center_vertical" android:focusable="true" android:importantForAccessibility="no" android:tint="?attr/shadeActive" android:visibility="gone" /> Loading packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +38 −4 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; Loading @@ -22,6 +23,7 @@ import android.view.animation.OvershootInterpolator; import android.widget.Scroller; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; Loading @@ -43,6 +45,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private static final int NO_PAGE = -1; private static final int REVEAL_SCROLL_DURATION_MILLIS = 750; private static final int SINGLE_PAGE_SCROLL_DURATION_MILLIS = 300; private static final float BOUNCE_ANIMATION_TENSION = 1.3f; private static final long BOUNCE_ANIMATION_DURATION = 450L; private static final int TILE_ANIMATION_STAGGER_DELAY = 85; Loading @@ -63,8 +66,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private PageListener mPageListener; private boolean mListening; private Scroller mScroller; @VisibleForTesting Scroller mScroller; /* set of animations used to indicate which tiles were just revealed */ @Nullable private AnimatorSet mBounceAnimatorSet; private float mLastExpansion; Loading Loading @@ -306,6 +310,38 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mPageIndicator = indicator; mPageIndicator.setNumPages(mPages.size()); mPageIndicator.setLocation(mPageIndicatorPosition); mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we // have a chance to intercept ACTION_UP. if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) { scrollByX(getDeltaXForKeyboardScrolling(keyCode), SINGLE_PAGE_SCROLL_DURATION_MILLIS); } return true; } return false; }); } private int getDeltaXForKeyboardScrolling(int keyCode) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) { return -getWidth(); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && getCurrentItem() != mPages.size() - 1) { return getWidth(); } return 0; } private void scrollByX(int x, int durationMillis) { if (x != 0) { mScroller.startScroll(/* startX= */ getScrollX(), /* startY= */ getScrollY(), /* dx= */ x, /* dy= */ 0, /* duration= */ durationMillis); // scroller just sets its state, we need to invalidate view to actually start scrolling postInvalidateOnAnimation(); } } @Override Loading Loading @@ -596,9 +632,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { }); setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated. int dx = getWidth() * lastPageNumber; mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, REVEAL_SCROLL_DURATION_MILLIS); postInvalidateOnAnimation(); scrollByX(isLayoutRtl() ? -dx : dx, REVEAL_SCROLL_DURATION_MILLIS); } private boolean shouldNotRunAnimation(Set<String> tilesToReveal) { Loading packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line package com.android.systemui.qs import android.content.Context import android.testing.AndroidTestingRunner import android.view.KeyEvent import android.view.View import android.widget.Scroller import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat 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.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class PagedTileLayoutTest : SysuiTestCase() { @Mock private lateinit var pageIndicator: PageIndicator @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener> private lateinit var pageTileLayout: TestPagedTileLayout private lateinit var scroller: Scroller @Before fun setUp() { MockitoAnnotations.initMocks(this) pageTileLayout = TestPagedTileLayout(mContext) pageTileLayout.setPageIndicator(pageIndicator) verify(pageIndicator).setOnKeyListener(captor.capture()) setViewWidth(pageTileLayout, width = PAGE_WIDTH) scroller = pageTileLayout.mScroller } private fun setViewWidth(view: View, width: Int) { view.left = 0 view.right = width } @Test fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() { pageTileLayout.currentPageIndex = 0 sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH) } @Test fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() { pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH) } private fun sendUpEvent(keyCode: Int) { val event = KeyEvent(KeyEvent.ACTION_UP, keyCode) captor.value.onKey(pageIndicator, keyCode, event) } /** * Custom PagedTileLayout to easy mock "currentItem" i.e. currently visible page. Setting this * up otherwise would require setting adapter etc */ class TestPagedTileLayout(context: Context) : PagedTileLayout(context, null) { var currentPageIndex: Int = 0 override fun getCurrentItem(): Int { return currentPageIndex } } companion object { const val PAGE_WIDTH = 200 } } Loading
packages/SystemUI/res/layout/qs_footer_impl.xml +2 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="center_vertical" android:focusable="true" android:importantForAccessibility="no" android:tint="?attr/shadeActive" android:visibility="gone" /> Loading
packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +38 −4 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; Loading @@ -22,6 +23,7 @@ import android.view.animation.OvershootInterpolator; import android.widget.Scroller; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; Loading @@ -43,6 +45,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private static final int NO_PAGE = -1; private static final int REVEAL_SCROLL_DURATION_MILLIS = 750; private static final int SINGLE_PAGE_SCROLL_DURATION_MILLIS = 300; private static final float BOUNCE_ANIMATION_TENSION = 1.3f; private static final long BOUNCE_ANIMATION_DURATION = 450L; private static final int TILE_ANIMATION_STAGGER_DELAY = 85; Loading @@ -63,8 +66,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private PageListener mPageListener; private boolean mListening; private Scroller mScroller; @VisibleForTesting Scroller mScroller; /* set of animations used to indicate which tiles were just revealed */ @Nullable private AnimatorSet mBounceAnimatorSet; private float mLastExpansion; Loading Loading @@ -306,6 +310,38 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mPageIndicator = indicator; mPageIndicator.setNumPages(mPages.size()); mPageIndicator.setLocation(mPageIndicatorPosition); mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we // have a chance to intercept ACTION_UP. if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) { scrollByX(getDeltaXForKeyboardScrolling(keyCode), SINGLE_PAGE_SCROLL_DURATION_MILLIS); } return true; } return false; }); } private int getDeltaXForKeyboardScrolling(int keyCode) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) { return -getWidth(); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && getCurrentItem() != mPages.size() - 1) { return getWidth(); } return 0; } private void scrollByX(int x, int durationMillis) { if (x != 0) { mScroller.startScroll(/* startX= */ getScrollX(), /* startY= */ getScrollY(), /* dx= */ x, /* dy= */ 0, /* duration= */ durationMillis); // scroller just sets its state, we need to invalidate view to actually start scrolling postInvalidateOnAnimation(); } } @Override Loading Loading @@ -596,9 +632,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { }); setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated. int dx = getWidth() * lastPageNumber; mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, REVEAL_SCROLL_DURATION_MILLIS); postInvalidateOnAnimation(); scrollByX(isLayoutRtl() ? -dx : dx, REVEAL_SCROLL_DURATION_MILLIS); } private boolean shouldNotRunAnimation(Set<String> tilesToReveal) { Loading
packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line package com.android.systemui.qs import android.content.Context import android.testing.AndroidTestingRunner import android.view.KeyEvent import android.view.View import android.widget.Scroller import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat 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.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class PagedTileLayoutTest : SysuiTestCase() { @Mock private lateinit var pageIndicator: PageIndicator @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener> private lateinit var pageTileLayout: TestPagedTileLayout private lateinit var scroller: Scroller @Before fun setUp() { MockitoAnnotations.initMocks(this) pageTileLayout = TestPagedTileLayout(mContext) pageTileLayout.setPageIndicator(pageIndicator) verify(pageIndicator).setOnKeyListener(captor.capture()) setViewWidth(pageTileLayout, width = PAGE_WIDTH) scroller = pageTileLayout.mScroller } private fun setViewWidth(view: View, width: Int) { view.left = 0 view.right = width } @Test fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() { pageTileLayout.currentPageIndex = 0 sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH) } @Test fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() { pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH) } private fun sendUpEvent(keyCode: Int) { val event = KeyEvent(KeyEvent.ACTION_UP, keyCode) captor.value.onKey(pageIndicator, keyCode, event) } /** * Custom PagedTileLayout to easy mock "currentItem" i.e. currently visible page. Setting this * up otherwise would require setting adapter etc */ class TestPagedTileLayout(context: Context) : PagedTileLayout(context, null) { var currentPageIndex: Int = 0 override fun getCurrentItem(): Int { return currentPageIndex } } companion object { const val PAGE_WIDTH = 200 } }