• 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.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 
PagedTileLayout(Context context, AttributeSet attrs)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 
saveInstanceState(Bundle outState)79     public void saveInstanceState(Bundle outState) {
80         outState.putInt(CURRENT_PAGE, getCurrentItem());
81     }
82 
restoreInstanceState(Bundle savedInstanceState)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
onConfigurationChanged(Configuration newConfig)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
onRtlPropertiesChanged(int layoutDirection)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
setCurrentItem(int item, boolean smoothScroll)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      */
getCurrentPageNumber()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
setListening(boolean listening)130     public void setListening(boolean listening) {
131         if (mListening == listening) return;
132         mListening = listening;
133         updateListening();
134     }
135 
updateListening()136     private void updateListening() {
137         for (TilePage tilePage : mPages) {
138             tilePage.setListening(tilePage.getParent() == null ? false : mListening);
139         }
140     }
141 
142     @Override
computeScroll()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
hasOverlappingRendering()158     public boolean hasOverlappingRendering() {
159         return false;
160     }
161 
162     @Override
onFinishInflate()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 
setPageIndicator(PageIndicator indicator)170     public void setPageIndicator(PageIndicator indicator) {
171         mPageIndicator = indicator;
172         mPageIndicator.setNumPages(mPages.size());
173         mPageIndicator.setLocation(mPageIndicatorPosition);
174     }
175 
176     @Override
getOffsetTop(TileRecord tile)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
addTile(TileRecord tile)184     public void addTile(TileRecord tile) {
185         mTiles.add(tile);
186         mDistributeTiles = true;
187         requestLayout();
188     }
189 
190     @Override
removeTile(TileRecord tile)191     public void removeTile(TileRecord tile) {
192         if (mTiles.remove(tile)) {
193             mDistributeTiles = true;
194             requestLayout();
195         }
196     }
197 
198     @Override
setExpansion(float expansion)199     public void setExpansion(float expansion) {
200         mLastExpansion = expansion;
201         updateSelected();
202     }
203 
updateSelected()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 
setPageListener(PageListener listener)223     public void setPageListener(PageListener listener) {
224         mPageListener = listener;
225     }
226 
distributeTiles()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 
emptyAndInflateOrRemovePages()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
updateResources()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
onLayout(boolean changed, int l, int t, int r, int b)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
getColumnCount()343     public int getColumnCount() {
344         if (mPages.size() == 0) return 0;
345         return mPages.get(0).mColumns;
346     }
347 
getNumVisibleTiles()348     public int getNumVisibleTiles() {
349         if (mPages.size() == 0) return 0;
350         TilePage currentPage = mPages.get(getCurrentPageNumber());
351         return currentPage.mRecords.size();
352     }
353 
startTileReveal(Set<String> tileSpecs, final Runnable postAnimation)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 
setupBounceAnimator(View view, int ordinal)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 
TilePage(Context context, AttributeSet attrs)434         public TilePage(Context context, AttributeSet attrs) {
435             super(context, attrs);
436         }
437 
isFull()438         public boolean isFull() {
439             return mRecords.size() >= maxTiles();
440         }
441 
maxTiles()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
updateResources()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 {
onPageChanged(boolean isFirst)493         void onPageChanged(boolean isFirst);
494     }
495 }
496