Home | History | Annotate | Download | only in qs
      1 package com.android.systemui.qs;
      2 
      3 import android.animation.Animator;
      4 import android.animation.AnimatorListenerAdapter;
      5 import android.animation.AnimatorSet;
      6 import android.animation.ObjectAnimator;
      7 import android.animation.PropertyValuesHolder;
      8 import android.content.Context;
      9 import android.content.res.Configuration;
     10 import android.content.res.Resources;
     11 import android.graphics.Rect;
     12 import android.os.Bundle;
     13 import android.util.AttributeSet;
     14 import android.util.Log;
     15 import android.view.LayoutInflater;
     16 import android.view.View;
     17 import android.view.ViewGroup;
     18 import android.view.animation.Interpolator;
     19 import android.view.animation.OvershootInterpolator;
     20 import android.widget.Scroller;
     21 
     22 import androidx.viewpager.widget.PagerAdapter;
     23 import androidx.viewpager.widget.ViewPager;
     24 
     25 import com.android.systemui.R;
     26 import com.android.systemui.qs.QSPanel.QSTileLayout;
     27 import com.android.systemui.qs.QSPanel.TileRecord;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Set;
     31 
     32 public class PagedTileLayout extends ViewPager implements QSTileLayout {
     33 
     34     private static final boolean DEBUG = false;
     35     private static final String CURRENT_PAGE = "current_page";
     36 
     37     private static final String TAG = "PagedTileLayout";
     38     private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
     39     private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
     40     private static final long BOUNCE_ANIMATION_DURATION = 450L;
     41     private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
     42     private static final Interpolator SCROLL_CUBIC = (t) -> {
     43         t -= 1.0f;
     44         return t * t * t + 1.0f;
     45     };
     46 
     47     private final ArrayList<TileRecord> mTiles = new ArrayList<>();
     48     private final ArrayList<TilePage> mPages = new ArrayList<>();
     49 
     50     private PageIndicator mPageIndicator;
     51     private float mPageIndicatorPosition;
     52 
     53     private PageListener mPageListener;
     54 
     55     private boolean mListening;
     56     private Scroller mScroller;
     57 
     58     private AnimatorSet mBounceAnimatorSet;
     59     private float mLastExpansion;
     60     private boolean mDistributeTiles = false;
     61     private int mPageToRestore = -1;
     62     private int mLayoutOrientation;
     63     private int mLayoutDirection;
     64     private int mHorizontalClipBound;
     65     private final Rect mClippingRect;
     66     private int mLastMaxHeight = -1;
     67 
     68     public PagedTileLayout(Context context, AttributeSet attrs) {
     69         super(context, attrs);
     70         mScroller = new Scroller(context, SCROLL_CUBIC);
     71         setAdapter(mAdapter);
     72         setOnPageChangeListener(mOnPageChangeListener);
     73         setCurrentItem(0, false);
     74         mLayoutOrientation = getResources().getConfiguration().orientation;
     75         mLayoutDirection = getLayoutDirection();
     76         mClippingRect = new Rect();
     77     }
     78 
     79     public void saveInstanceState(Bundle outState) {
     80         outState.putInt(CURRENT_PAGE, getCurrentItem());
     81     }
     82 
     83     public void restoreInstanceState(Bundle savedInstanceState) {
     84         // There's only 1 page at this point. We want to restore the correct page once the
     85         // pages have been inflated
     86         mPageToRestore = savedInstanceState.getInt(CURRENT_PAGE, -1);
     87     }
     88 
     89     @Override
     90     protected void onConfigurationChanged(Configuration newConfig) {
     91         super.onConfigurationChanged(newConfig);
     92         if (mLayoutOrientation != newConfig.orientation) {
     93             mLayoutOrientation = newConfig.orientation;
     94             setCurrentItem(0, false);
     95             mPageToRestore = 0;
     96         }
     97     }
     98 
     99     @Override
    100     public void onRtlPropertiesChanged(int layoutDirection) {
    101         super.onRtlPropertiesChanged(layoutDirection);
    102         if (mLayoutDirection != layoutDirection) {
    103             mLayoutDirection = layoutDirection;
    104             setAdapter(mAdapter);
    105             setCurrentItem(0, false);
    106             mPageToRestore = 0;
    107         }
    108     }
    109 
    110     @Override
    111     public void setCurrentItem(int item, boolean smoothScroll) {
    112         if (isLayoutRtl()) {
    113             item = mPages.size() - 1 - item;
    114         }
    115         super.setCurrentItem(item, smoothScroll);
    116     }
    117 
    118     /**
    119      * Obtains the current page number respecting RTL
    120      */
    121     private int getCurrentPageNumber() {
    122         int page = getCurrentItem();
    123         if (mLayoutDirection == LAYOUT_DIRECTION_RTL) {
    124             page = mPages.size() - 1 - page;
    125         }
    126         return page;
    127     }
    128 
    129     @Override
    130     public void setListening(boolean listening) {
    131         if (mListening == listening) return;
    132         mListening = listening;
    133         updateListening();
    134     }
    135 
    136     private void updateListening() {
    137         for (TilePage tilePage : mPages) {
    138             tilePage.setListening(tilePage.getParent() == null ? false : mListening);
    139         }
    140     }
    141 
    142     @Override
    143     public void computeScroll() {
    144         if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
    145             fakeDragBy(getScrollX() - mScroller.getCurrX());
    146             // Keep on drawing until the animation has finished.
    147             postInvalidateOnAnimation();
    148             return;
    149         } else if (isFakeDragging()) {
    150             endFakeDrag();
    151             mBounceAnimatorSet.start();
    152             setOffscreenPageLimit(1);
    153         }
    154         super.computeScroll();
    155     }
    156 
    157     @Override
    158     public boolean hasOverlappingRendering() {
    159         return false;
    160     }
    161 
    162     @Override
    163     protected void onFinishInflate() {
    164         super.onFinishInflate();
    165         mPages.add((TilePage) LayoutInflater.from(getContext())
    166                 .inflate(R.layout.qs_paged_page, this, false));
    167         mAdapter.notifyDataSetChanged();
    168     }
    169 
    170     public void setPageIndicator(PageIndicator indicator) {
    171         mPageIndicator = indicator;
    172         mPageIndicator.setNumPages(mPages.size());
    173         mPageIndicator.setLocation(mPageIndicatorPosition);
    174     }
    175 
    176     @Override
    177     public int getOffsetTop(TileRecord tile) {
    178         final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
    179         if (parent == null) return 0;
    180         return parent.getTop() + getTop();
    181     }
    182 
    183     @Override
    184     public void addTile(TileRecord tile) {
    185         mTiles.add(tile);
    186         mDistributeTiles = true;
    187         requestLayout();
    188     }
    189 
    190     @Override
    191     public void removeTile(TileRecord tile) {
    192         if (mTiles.remove(tile)) {
    193             mDistributeTiles = true;
    194             requestLayout();
    195         }
    196     }
    197 
    198     @Override
    199     public void setExpansion(float expansion) {
    200         mLastExpansion = expansion;
    201         updateSelected();
    202     }
    203 
    204     private void updateSelected() {
    205         // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
    206         // other expansion ratios since there is no way way to pause the marquee.
    207         if (mLastExpansion > 0f && mLastExpansion < 1f) {
    208             return;
    209         }
    210         boolean selected = mLastExpansion == 1f;
    211 
    212         // Disable accessibility temporarily while we update selected state purely for the
    213         // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
    214         // event on any of the children.
    215         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    216         int currentItem = getCurrentPageNumber();
    217         for (int i = 0; i < mPages.size(); i++) {
    218             mPages.get(i).setSelected(i == currentItem ? selected : false);
    219         }
    220         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
    221     }
    222 
    223     public void setPageListener(PageListener listener) {
    224         mPageListener = listener;
    225     }
    226 
    227     private void distributeTiles() {
    228         emptyAndInflateOrRemovePages();
    229 
    230         final int tileCount = mPages.get(0).maxTiles();
    231         if (DEBUG) Log.d(TAG, "Distributing tiles");
    232         int index = 0;
    233         final int NT = mTiles.size();
    234         for (int i = 0; i < NT; i++) {
    235             TileRecord tile = mTiles.get(i);
    236             if (mPages.get(index).mRecords.size() == tileCount) index++;
    237             if (DEBUG) {
    238                 Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
    239                         + index);
    240             }
    241             mPages.get(index).addTile(tile);
    242         }
    243     }
    244 
    245     private void emptyAndInflateOrRemovePages() {
    246         final int nTiles = mTiles.size();
    247         // We should always have at least one page, even if it's empty.
    248         int numPages = Math.max(nTiles / mPages.get(0).maxTiles(), 1);
    249 
    250         // Add one more not full page if needed
    251         numPages += (nTiles % mPages.get(0).maxTiles() == 0 ? 0 : 1);
    252 
    253         final int NP = mPages.size();
    254         for (int i = 0; i < NP; i++) {
    255             mPages.get(i).removeAllViews();
    256         }
    257         if (NP == numPages) {
    258             return;
    259         }
    260         while (mPages.size() < numPages) {
    261             if (DEBUG) Log.d(TAG, "Adding page");
    262             mPages.add((TilePage) LayoutInflater.from(getContext())
    263                     .inflate(R.layout.qs_paged_page, this, false));
    264         }
    265         while (mPages.size() > numPages) {
    266             if (DEBUG) Log.d(TAG, "Removing page");
    267             mPages.remove(mPages.size() - 1);
    268         }
    269         mPageIndicator.setNumPages(mPages.size());
    270         setAdapter(mAdapter);
    271         mAdapter.notifyDataSetChanged();
    272         if (mPageToRestore != -1) {
    273             setCurrentItem(mPageToRestore, false);
    274             mPageToRestore = -1;
    275         }
    276     }
    277 
    278     @Override
    279     public boolean updateResources() {
    280         // Update bottom padding, useful for removing extra space once the panel page indicator is
    281         // hidden.
    282         Resources res = getContext().getResources();
    283         mHorizontalClipBound = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
    284         setPadding(0, 0, 0,
    285                 getContext().getResources().getDimensionPixelSize(
    286                         R.dimen.qs_paged_tile_layout_padding_bottom));
    287         boolean changed = false;
    288         for (int i = 0; i < mPages.size(); i++) {
    289             changed |= mPages.get(i).updateResources();
    290         }
    291         if (changed) {
    292             mDistributeTiles = true;
    293             requestLayout();
    294         }
    295         return changed;
    296     }
    297 
    298     @Override
    299     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    300         super.onLayout(changed, l, t, r, b);
    301         mClippingRect.set(mHorizontalClipBound, 0, (r - l) - mHorizontalClipBound, b - t);
    302         setClipBounds(mClippingRect);
    303     }
    304 
    305     @Override
    306     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    307 
    308         final int nTiles = mTiles.size();
    309         // If we have no reason to recalculate the number of rows, skip this step. In particular,
    310         // if the height passed by its parent is the same as the last time, we try not to remeasure.
    311         if (mDistributeTiles || mLastMaxHeight != MeasureSpec.getSize(heightMeasureSpec)) {
    312 
    313             mLastMaxHeight = MeasureSpec.getSize(heightMeasureSpec);
    314             // Only change the pages if the number of rows or columns (from updateResources) has
    315             // changed or the tiles have changed
    316             if (mPages.get(0).updateMaxRows(heightMeasureSpec, nTiles) || mDistributeTiles) {
    317                 mDistributeTiles = false;
    318                 distributeTiles();
    319             }
    320 
    321             final int nRows = mPages.get(0).mRows;
    322             for (int i = 0; i < mPages.size(); i++) {
    323                 TilePage t = mPages.get(i);
    324                 t.mRows = nRows;
    325             }
    326         }
    327 
    328         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    329 
    330         // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
    331         // of the pages.
    332         int maxHeight = 0;
    333         final int N = getChildCount();
    334         for (int i = 0; i < N; i++) {
    335             int height = getChildAt(i).getMeasuredHeight();
    336             if (height > maxHeight) {
    337                 maxHeight = height;
    338             }
    339         }
    340         setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
    341     }
    342 
    343     public int getColumnCount() {
    344         if (mPages.size() == 0) return 0;
    345         return mPages.get(0).mColumns;
    346     }
    347 
    348     public int getNumVisibleTiles() {
    349         if (mPages.size() == 0) return 0;
    350         TilePage currentPage = mPages.get(getCurrentPageNumber());
    351         return currentPage.mRecords.size();
    352     }
    353 
    354     public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
    355         if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
    356             // Do not start the reveal animation unless there are tiles to animate, multiple
    357             // TilePages available and the user has not already started dragging.
    358             return;
    359         }
    360 
    361         final int lastPageNumber = mPages.size() - 1;
    362         final TilePage lastPage = mPages.get(lastPageNumber);
    363         final ArrayList<Animator> bounceAnims = new ArrayList<>();
    364         for (TileRecord tr : lastPage.mRecords) {
    365             if (tileSpecs.contains(tr.tile.getTileSpec())) {
    366                 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
    367             }
    368         }
    369 
    370         if (bounceAnims.isEmpty()) {
    371             // All tileSpecs are on the first page. Nothing to do.
    372             // TODO: potentially show a bounce animation for first page QS tiles
    373             endFakeDrag();
    374             return;
    375         }
    376 
    377         mBounceAnimatorSet = new AnimatorSet();
    378         mBounceAnimatorSet.playTogether(bounceAnims);
    379         mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
    380             @Override
    381             public void onAnimationEnd(Animator animation) {
    382                 mBounceAnimatorSet = null;
    383                 postAnimation.run();
    384             }
    385         });
    386         setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
    387         int dx = getWidth() * lastPageNumber;
    388         mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx  : dx, 0,
    389             REVEAL_SCROLL_DURATION_MILLIS);
    390         postInvalidateOnAnimation();
    391     }
    392 
    393     private static Animator setupBounceAnimator(View view, int ordinal) {
    394         view.setAlpha(0f);
    395         view.setScaleX(0f);
    396         view.setScaleY(0f);
    397         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
    398                 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
    399                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
    400                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
    401         animator.setDuration(BOUNCE_ANIMATION_DURATION);
    402         animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
    403         animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
    404         return animator;
    405     }
    406 
    407     private final ViewPager.OnPageChangeListener mOnPageChangeListener =
    408             new ViewPager.SimpleOnPageChangeListener() {
    409                 @Override
    410                 public void onPageSelected(int position) {
    411                     updateSelected();
    412                     if (mPageIndicator == null) return;
    413                     if (mPageListener != null) {
    414                         mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
    415                                 : position == 0);
    416                     }
    417                 }
    418 
    419                 @Override
    420                 public void onPageScrolled(int position, float positionOffset,
    421                         int positionOffsetPixels) {
    422                     if (mPageIndicator == null) return;
    423                     mPageIndicatorPosition = position + positionOffset;
    424                     mPageIndicator.setLocation(mPageIndicatorPosition);
    425                     if (mPageListener != null) {
    426                         mPageListener.onPageChanged(positionOffsetPixels == 0 &&
    427                                 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
    428                     }
    429                 }
    430             };
    431 
    432     public static class TilePage extends TileLayout {
    433 
    434         public TilePage(Context context, AttributeSet attrs) {
    435             super(context, attrs);
    436         }
    437 
    438         public boolean isFull() {
    439             return mRecords.size() >= maxTiles();
    440         }
    441 
    442         public int maxTiles() {
    443             // Each page should be able to hold at least one tile. If there's not enough room to
    444             // show even 1 or there are no tiles, it probably means we are in the middle of setting
    445             // up.
    446             return Math.max(mColumns * mRows, 1);
    447         }
    448 
    449         @Override
    450         public boolean updateResources() {
    451             final int sidePadding = getContext().getResources().getDimensionPixelSize(
    452                     R.dimen.notification_side_paddings);
    453             setPadding(sidePadding, 0, sidePadding, 0);
    454             return super.updateResources();
    455         }
    456     }
    457 
    458     private final PagerAdapter mAdapter = new PagerAdapter() {
    459         @Override
    460         public void destroyItem(ViewGroup container, int position, Object object) {
    461             if (DEBUG) Log.d(TAG, "Destantiating " + position);
    462             container.removeView((View) object);
    463             updateListening();
    464         }
    465 
    466         @Override
    467         public Object instantiateItem(ViewGroup container, int position) {
    468             if (DEBUG) Log.d(TAG, "Instantiating " + position);
    469             if (isLayoutRtl()) {
    470                 position = mPages.size() - 1 - position;
    471             }
    472             ViewGroup view = mPages.get(position);
    473             if (view.getParent() != null) {
    474                 container.removeView(view);
    475             }
    476             container.addView(view);
    477             updateListening();
    478             return view;
    479         }
    480 
    481         @Override
    482         public int getCount() {
    483             return mPages.size();
    484         }
    485 
    486         @Override
    487         public boolean isViewFromObject(View view, Object object) {
    488             return view == object;
    489         }
    490     };
    491 
    492     public interface PageListener {
    493         void onPageChanged(boolean isFirst);
    494     }
    495 }
    496