• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
PagedTileLayout(Context context, AttributeSet attrs)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
onRtlPropertiesChanged(int layoutDirection)70     public void onRtlPropertiesChanged(int layoutDirection) {
71         super.onRtlPropertiesChanged(layoutDirection);
72         setAdapter(mAdapter);
73         setCurrentItem(0, false);
74     }
75 
76     @Override
setCurrentItem(int item, boolean smoothScroll)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
setListening(boolean listening)85     public void setListening(boolean listening) {
86         if (mListening == listening) return;
87         mListening = listening;
88         updateListening();
89     }
90 
updateListening()91     private void updateListening() {
92         for (TilePage tilePage : mPages) {
93             tilePage.setListening(tilePage.getParent() == null ? false : mListening);
94         }
95     }
96 
97     @Override
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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
computeScroll()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
hasOverlappingRendering()137     public boolean hasOverlappingRendering() {
138         return false;
139     }
140 
141     @Override
onFinishInflate()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 
setPageIndicator(PageIndicator indicator)148     public void setPageIndicator(PageIndicator indicator) {
149         mPageIndicator = indicator;
150         mPageIndicator.setNumPages(mNumPages);
151         mPageIndicator.setLocation(mPageIndicatorPosition);
152     }
153 
154     @Override
getOffsetTop(TileRecord tile)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
addTile(TileRecord tile)162     public void addTile(TileRecord tile) {
163         mTiles.add(tile);
164         postDistributeTiles();
165     }
166 
167     @Override
removeTile(TileRecord tile)168     public void removeTile(TileRecord tile) {
169         if (mTiles.remove(tile)) {
170             postDistributeTiles();
171         }
172     }
173 
174     @Override
setExpansion(float expansion)175     public void setExpansion(float expansion) {
176         mLastExpansion = expansion;
177         updateSelected();
178     }
179 
updateSelected()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 
setPageListener(PageListener listener)198     public void setPageListener(PageListener listener) {
199         mPageListener = listener;
200     }
201 
postDistributeTiles()202     private void postDistributeTiles() {
203         removeCallbacks(mDistribute);
204         post(mDistribute);
205     }
206 
distributeTiles()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
updateResources()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
getColumnCount()283     public int getColumnCount() {
284         if (mPages.size() == 0) return 0;
285         return mPages.get(0).mColumns;
286     }
287 
startTileReveal(Set<String> tileSpecs, final Runnable postAnimation)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 
setupBounceAnimator(View view, int ordinal)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;
TilePage(Context context, AttributeSet attrs)367         public TilePage(Context context, AttributeSet attrs) {
368             super(context, attrs);
369             updateResources();
370         }
371 
372         @Override
updateResources()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 
getRows()383         private int getRows() {
384             return Math.max(1, getResources().getInteger(R.integer.quick_settings_num_rows));
385         }
386 
setMaxRows(int maxRows)387         public void setMaxRows(int maxRows) {
388             mMaxRows = maxRows;
389         }
390 
isFull()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 {
onPageChanged(boolean isFirst)428         void onPageChanged(boolean isFirst);
429     }
430 }
431