Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e59399a0 authored by Michal Brzezinski's avatar Michal Brzezinski
Browse files

Enabling QS pages scrolling with keyboard

To scroll: focus on QS pager indicator and press left or right arrow keys.
PagedTileLayout is intercepting arrow key presses sent to pager indicator
and scrolling view accordingly.

Fixes: 301586958
Test: PagedTileLayoutTest
Change-Id: I8e130ca9350d231eb6bd0b5458aa171876144dd8
parent 6d617f78
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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" />

+39 −5
Original line number Diff line number Diff line
@@ -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;
@@ -22,16 +23,17 @@ 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;

import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.res.R;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSPanel.QSTileLayout;
import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;

import java.util.ArrayList;
import java.util.List;
@@ -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;
@@ -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;
@@ -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
@@ -590,9 +626,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) {
+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
    }
}