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.support.v4.view.PagerAdapter;
     12 import android.support.v4.view.ViewPager;
     13 import android.util.AttributeSet;
     14 import android.util.Log;
     15 import android.view.LayoutInflater;
     16 import android.view.MotionEvent;
     17 import android.view.View;
     18 import android.view.ViewGroup;
     19 import android.view.animation.Interpolator;
     20 import android.view.animation.OvershootInterpolator;
     21 import android.widget.Scroller;
     22 
     23 import com.android.systemui.R;
     24 import com.android.systemui.qs.QSPanel.QSTileLayout;
     25 import com.android.systemui.qs.QSPanel.TileRecord;
     26 
     27 import java.util.ArrayList;
     28 import java.util.Set;
     29 
     30 public class PagedTileLayout extends ViewPager implements QSTileLayout {
     31 
     32     private static final boolean DEBUG = false;
     33 
     34     private static final String TAG = "PagedTileLayout";
     35     private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
     36     private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
     37     private static final long BOUNCE_ANIMATION_DURATION = 450L;
     38     private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
     39     private static final Interpolator SCROLL_CUBIC = (t) -> {
     40         t -= 1.0f;
     41         return t * t * t + 1.0f;
     42     };
     43 
     44 
     45     private final ArrayList<TileRecord> mTiles = new ArrayList<>();
     46     private final ArrayList<TilePage> mPages = new ArrayList<>();
     47 
     48     private PageIndicator mPageIndicator;
     49     private float mPageIndicatorPosition;
     50 
     51     private int mNumPages;
     52     private PageListener mPageListener;
     53 
     54     private boolean mListening;
     55     private Scroller mScroller;
     56 
     57     private AnimatorSet mBounceAnimatorSet;
     58     private int mAnimatingToPage = -1;
     59     private float mLastExpansion;
     60 
     61     public PagedTileLayout(Context context, AttributeSet attrs) {
     62         super(context, attrs);
     63         mScroller = new Scroller(context, SCROLL_CUBIC);
     64         setAdapter(mAdapter);
     65         setOnPageChangeListener(mOnPageChangeListener);
     66         setCurrentItem(0, false);
     67     }
     68 
     69     @Override
     70     public void onRtlPropertiesChanged(int layoutDirection) {
     71         super.onRtlPropertiesChanged(layoutDirection);
     72         setAdapter(mAdapter);
     73         setCurrentItem(0, false);
     74     }
     75 
     76     @Override
     77     public void setCurrentItem(int item, boolean smoothScroll) {
     78         if (isLayoutRtl()) {
     79             item = mPages.size() - 1 - item;
     80         }
     81         super.setCurrentItem(item, smoothScroll);
     82     }
     83 
     84     @Override
     85     public void setListening(boolean listening) {
     86         if (mListening == listening) return;
     87         mListening = listening;
     88         updateListening();
     89     }
     90 
     91     private void updateListening() {
     92         for (TilePage tilePage : mPages) {
     93             tilePage.setListening(tilePage.getParent() == null ? false : mListening);
     94         }
     95     }
     96 
     97     @Override
     98     public boolean onInterceptTouchEvent(MotionEvent ev) {
     99         // Suppress all touch event during reveal animation.
    100         if (mAnimatingToPage != -1) {
    101             return true;
    102         }
    103         return super.onInterceptTouchEvent(ev);
    104     }
    105 
    106     @Override
    107     public boolean onTouchEvent(MotionEvent ev) {
    108         // Suppress all touch event during reveal animation.
    109         if (mAnimatingToPage != -1) {
    110             return true;
    111         }
    112         return super.onTouchEvent(ev);
    113     }
    114 
    115     @Override
    116     public void computeScroll() {
    117         if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
    118             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    119             float pageFraction = (float) getScrollX() / getWidth();
    120             int position = (int) pageFraction;
    121             float positionOffset = pageFraction - position;
    122             mOnPageChangeListener.onPageScrolled(position, positionOffset, getScrollX());
    123             // Keep on drawing until the animation has finished.
    124             postInvalidateOnAnimation();
    125             return;
    126         }
    127         if (mAnimatingToPage != -1) {
    128             setCurrentItem(mAnimatingToPage, true);
    129             mBounceAnimatorSet.start();
    130             setOffscreenPageLimit(1);
    131             mAnimatingToPage = -1;
    132         }
    133         super.computeScroll();
    134     }
    135 
    136     @Override
    137     public boolean hasOverlappingRendering() {
    138         return false;
    139     }
    140 
    141     @Override
    142     protected void onFinishInflate() {
    143         super.onFinishInflate();
    144         mPages.add((TilePage) LayoutInflater.from(getContext())
    145                 .inflate(R.layout.qs_paged_page, this, false));
    146     }
    147 
    148     public void setPageIndicator(PageIndicator indicator) {
    149         mPageIndicator = indicator;
    150         mPageIndicator.setNumPages(mNumPages);
    151         mPageIndicator.setLocation(mPageIndicatorPosition);
    152     }
    153 
    154     @Override
    155     public int getOffsetTop(TileRecord tile) {
    156         final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
    157         if (parent == null) return 0;
    158         return parent.getTop() + getTop();
    159     }
    160 
    161     @Override
    162     public void addTile(TileRecord tile) {
    163         mTiles.add(tile);
    164         postDistributeTiles();
    165     }
    166 
    167     @Override
    168     public void removeTile(TileRecord tile) {
    169         if (mTiles.remove(tile)) {
    170             postDistributeTiles();
    171         }
    172     }
    173 
    174     @Override
    175     public void setExpansion(float expansion) {
    176         mLastExpansion = expansion;
    177         updateSelected();
    178     }
    179 
    180     private void updateSelected() {
    181         // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
    182         // other expansion ratios since there is no way way to pause the marquee.
    183         if (mLastExpansion > 0f && mLastExpansion < 1f) {
    184             return;
    185         }
    186         boolean selected = mLastExpansion == 1f;
    187 
    188         // Disable accessibility temporarily while we update selected state purely for the
    189         // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
    190         // event on any of the children.
    191         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    192         for (int i = 0; i < mPages.size(); i++) {
    193             mPages.get(i).setSelected(i == getCurrentItem() ? selected : false);
    194         }
    195         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
    196     }
    197 
    198     public void setPageListener(PageListener listener) {
    199         mPageListener = listener;
    200     }
    201 
    202     private void postDistributeTiles() {
    203         removeCallbacks(mDistribute);
    204         post(mDistribute);
    205     }
    206 
    207     private void distributeTiles() {
    208         if (DEBUG) Log.d(TAG, "Distributing tiles");
    209         final int NP = mPages.size();
    210         for (int i = 0; i < NP; i++) {
    211             mPages.get(i).removeAllViews();
    212         }
    213         int index = 0;
    214         final int NT = mTiles.size();
    215         for (int i = 0; i < NT; i++) {
    216             TileRecord tile = mTiles.get(i);
    217             if (mPages.get(index).isFull()) {
    218                 if (++index == mPages.size()) {
    219                     if (DEBUG) Log.d(TAG, "Adding page for "
    220                             + tile.tile.getClass().getSimpleName());
    221                     mPages.add((TilePage) LayoutInflater.from(getContext())
    222                             .inflate(R.layout.qs_paged_page, this, false));
    223                 }
    224             }
    225             if (DEBUG) Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
    226                     + index);
    227             mPages.get(index).addTile(tile);
    228         }
    229         if (mNumPages != index + 1) {
    230             mNumPages = index + 1;
    231             while (mPages.size() > mNumPages) {
    232                 mPages.remove(mPages.size() - 1);
    233             }
    234             if (DEBUG) Log.d(TAG, "Size: " + mNumPages);
    235             mPageIndicator.setNumPages(mNumPages);
    236             setAdapter(mAdapter);
    237             mAdapter.notifyDataSetChanged();
    238             setCurrentItem(0, false);
    239         }
    240     }
    241 
    242     @Override
    243     public boolean updateResources() {
    244         // Update bottom padding, useful for removing extra space once the panel page indicator is
    245         // hidden.
    246         setPadding(0, 0, 0,
    247                 getContext().getResources().getDimensionPixelSize(
    248                         R.dimen.qs_paged_tile_layout_padding_bottom));
    249 
    250         boolean changed = false;
    251         for (int i = 0; i < mPages.size(); i++) {
    252             changed |= mPages.get(i).updateResources();
    253         }
    254         if (changed) {
    255             distributeTiles();
    256         }
    257         return changed;
    258     }
    259 
    260     @Override
    261     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    262         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    263         // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
    264         // of the pages.
    265         int maxHeight = 0;
    266         final int N = getChildCount();
    267         for (int i = 0; i < N; i++) {
    268             int height = getChildAt(i).getMeasuredHeight();
    269             if (height > maxHeight) {
    270                 maxHeight = height;
    271             }
    272         }
    273         setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
    274     }
    275 
    276     private final Runnable mDistribute = new Runnable() {
    277         @Override
    278         public void run() {
    279             distributeTiles();
    280         }
    281     };
    282 
    283     public int getColumnCount() {
    284         if (mPages.size() == 0) return 0;
    285         return mPages.get(0).mColumns;
    286     }
    287 
    288     public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
    289         if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0) {
    290             // Do not start the reveal animation unless there are tiles to animate, multiple
    291             // TilePages available and the user has not already started dragging.
    292             return;
    293         }
    294 
    295         final int lastPageNumber = mPages.size() - 1;
    296         final TilePage lastPage = mPages.get(lastPageNumber);
    297         final ArrayList<Animator> bounceAnims = new ArrayList<>();
    298         for (TileRecord tr : lastPage.mRecords) {
    299             if (tileSpecs.contains(tr.tile.getTileSpec())) {
    300                 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
    301             }
    302         }
    303 
    304         if (bounceAnims.isEmpty()) {
    305             // All tileSpecs are on the first page. Nothing to do.
    306             // TODO: potentially show a bounce animation for first page QS tiles
    307             return;
    308         }
    309 
    310         mBounceAnimatorSet = new AnimatorSet();
    311         mBounceAnimatorSet.playTogether(bounceAnims);
    312         mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
    313             @Override
    314             public void onAnimationEnd(Animator animation) {
    315                 mBounceAnimatorSet = null;
    316                 postAnimation.run();
    317             }
    318         });
    319         mAnimatingToPage = lastPageNumber;
    320         setOffscreenPageLimit(mAnimatingToPage); // Ensure the page to reveal has been inflated.
    321         mScroller.startScroll(getScrollX(), getScrollY(), getWidth() * mAnimatingToPage, 0,
    322                 REVEAL_SCROLL_DURATION_MILLIS);
    323         postInvalidateOnAnimation();
    324     }
    325 
    326     private static Animator setupBounceAnimator(View view, int ordinal) {
    327         view.setAlpha(0f);
    328         view.setScaleX(0f);
    329         view.setScaleY(0f);
    330         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
    331                 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
    332                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
    333                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
    334         animator.setDuration(BOUNCE_ANIMATION_DURATION);
    335         animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
    336         animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
    337         return animator;
    338     }
    339 
    340     private final ViewPager.OnPageChangeListener mOnPageChangeListener =
    341             new ViewPager.SimpleOnPageChangeListener() {
    342                 @Override
    343                 public void onPageSelected(int position) {
    344                     updateSelected();
    345                     if (mPageIndicator == null) return;
    346                     if (mPageListener != null) {
    347                         mPageListener.onPageChanged(isLayoutRtl() ? position == mPages.size() - 1
    348                                 : position == 0);
    349                     }
    350                 }
    351 
    352                 @Override
    353                 public void onPageScrolled(int position, float positionOffset,
    354                         int positionOffsetPixels) {
    355                     if (mPageIndicator == null) return;
    356                     mPageIndicatorPosition = position + positionOffset;
    357                     mPageIndicator.setLocation(mPageIndicatorPosition);
    358                     if (mPageListener != null) {
    359                         mPageListener.onPageChanged(positionOffsetPixels == 0 &&
    360                                 (isLayoutRtl() ? position == mPages.size() - 1 : position == 0));
    361                     }
    362                 }
    363             };
    364 
    365     public static class TilePage extends TileLayout {
    366         private int mMaxRows = 3;
    367         public TilePage(Context context, AttributeSet attrs) {
    368             super(context, attrs);
    369             updateResources();
    370         }
    371 
    372         @Override
    373         public boolean updateResources() {
    374             final int rows = getRows();
    375             boolean changed = rows != mMaxRows;
    376             if (changed) {
    377                 mMaxRows = rows;
    378                 requestLayout();
    379             }
    380             return super.updateResources() || changed;
    381         }
    382 
    383         private int getRows() {
    384             return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
    385         }
    386 
    387         public void setMaxRows(int maxRows) {
    388             mMaxRows = maxRows;
    389         }
    390 
    391         public boolean isFull() {
    392             return mRecords.size() >= mColumns * mMaxRows;
    393         }
    394     }
    395 
    396     private final PagerAdapter mAdapter = new PagerAdapter() {
    397         @Override
    398         public void destroyItem(ViewGroup container, int position, Object object) {
    399             if (DEBUG) Log.d(TAG, "Destantiating " + position);
    400             container.removeView((View) object);
    401             updateListening();
    402         }
    403 
    404         @Override
    405         public Object instantiateItem(ViewGroup container, int position) {
    406             if (DEBUG) Log.d(TAG, "Instantiating " + position);
    407             if (isLayoutRtl()) {
    408                 position = mPages.size() - 1 - position;
    409             }
    410             ViewGroup view = mPages.get(position);
    411             container.addView(view);
    412             updateListening();
    413             return view;
    414         }
    415 
    416         @Override
    417         public int getCount() {
    418             return mNumPages;
    419         }
    420 
    421         @Override
    422         public boolean isViewFromObject(View view, Object object) {
    423             return view == object;
    424         }
    425     };
    426 
    427     public interface PageListener {
    428         void onPageChanged(boolean isFirst);
    429     }
    430 }
    431