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