• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.systemui.qs;
2 
3 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE;
4 
5 import android.animation.Animator;
6 import android.animation.AnimatorListenerAdapter;
7 import android.animation.AnimatorSet;
8 import android.animation.ObjectAnimator;
9 import android.animation.PropertyValuesHolder;
10 import android.content.Context;
11 import android.content.res.Configuration;
12 import android.os.Bundle;
13 import android.util.AttributeSet;
14 import android.view.LayoutInflater;
15 import android.view.View;
16 import android.view.ViewGroup;
17 import android.view.accessibility.AccessibilityEvent;
18 import android.view.accessibility.AccessibilityNodeInfo;
19 import android.view.animation.Interpolator;
20 import android.view.animation.OvershootInterpolator;
21 import android.widget.Scroller;
22 
23 import androidx.annotation.Nullable;
24 import androidx.viewpager.widget.PagerAdapter;
25 import androidx.viewpager.widget.ViewPager;
26 
27 import com.android.internal.jank.InteractionJankMonitor;
28 import com.android.internal.logging.UiEventLogger;
29 import com.android.systemui.R;
30 import com.android.systemui.plugins.qs.QSTile;
31 import com.android.systemui.qs.QSPanel.QSTileLayout;
32 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
33 import com.android.systemui.qs.logging.QSLogger;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Set;
38 
39 public class PagedTileLayout extends ViewPager implements QSTileLayout {
40 
41     private static final String CURRENT_PAGE = "current_page";
42     private static final int NO_PAGE = -1;
43 
44     private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
45     private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
46     private static final long BOUNCE_ANIMATION_DURATION = 450L;
47     private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
48     private static final Interpolator SCROLL_CUBIC = (t) -> {
49         t -= 1.0f;
50         return t * t * t + 1.0f;
51     };
52 
53     private final ArrayList<TileRecord> mTiles = new ArrayList<>();
54     private final ArrayList<TileLayout> mPages = new ArrayList<>();
55 
56     private QSLogger mLogger;
57     @Nullable
58     private PageIndicator mPageIndicator;
59     private float mPageIndicatorPosition;
60 
61     @Nullable
62     private PageListener mPageListener;
63 
64     private boolean mListening;
65     private Scroller mScroller;
66 
67     @Nullable
68     private AnimatorSet mBounceAnimatorSet;
69     private float mLastExpansion;
70     private boolean mDistributeTiles = false;
71     private int mPageToRestore = -1;
72     private int mLayoutOrientation;
73     private int mLayoutDirection;
74     private final UiEventLogger mUiEventLogger = QSEvents.INSTANCE.getQsUiEventsLogger();
75     private int mExcessHeight;
76     private int mLastExcessHeight;
77     private int mMinRows = 1;
78     private int mMaxColumns = TileLayout.NO_MAX_COLUMNS;
79 
PagedTileLayout(Context context, AttributeSet attrs)80     public PagedTileLayout(Context context, AttributeSet attrs) {
81         super(context, attrs);
82         mScroller = new Scroller(context, SCROLL_CUBIC);
83         setAdapter(mAdapter);
84         setOnPageChangeListener(mOnPageChangeListener);
85         setCurrentItem(0, false);
86         mLayoutOrientation = getResources().getConfiguration().orientation;
87         mLayoutDirection = getLayoutDirection();
88     }
89     private int mLastMaxHeight = -1;
90 
91     @Override
setPageMargin(int marginPixels)92     public void setPageMargin(int marginPixels) {
93         // Using page margins creates some rounding issues that interfere with the correct position
94         // in the onPageChangedListener and therefore present bad positions to the PageIndicator.
95         // Instead, we use negative margins in the container and positive padding in the pages,
96         // matching the margin set from QSContainerImpl (note that new pages will always be inflated
97         // with the correct value.
98         // QSContainerImpl resources are set onAttachedView, so this view will always have the right
99         // values when attached.
100         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
101         lp.setMarginStart(-marginPixels);
102         lp.setMarginEnd(-marginPixels);
103         setLayoutParams(lp);
104 
105         int nPages = mPages.size();
106         for (int i = 0; i < nPages; i++) {
107             View v = mPages.get(i);
108             v.setPadding(marginPixels, v.getPaddingTop(), marginPixels, v.getPaddingBottom());
109         }
110     }
111 
saveInstanceState(Bundle outState)112     public void saveInstanceState(Bundle outState) {
113         int resolvedPage = mPageToRestore != NO_PAGE ? mPageToRestore : getCurrentPageNumber();
114         outState.putInt(CURRENT_PAGE, resolvedPage);
115     }
116 
restoreInstanceState(Bundle savedInstanceState)117     public void restoreInstanceState(Bundle savedInstanceState) {
118         // There's only 1 page at this point. We want to restore the correct page once the
119         // pages have been inflated
120         mPageToRestore = savedInstanceState.getInt(CURRENT_PAGE, NO_PAGE);
121     }
122 
123     @Override
getTilesHeight()124     public int getTilesHeight() {
125         // Use the first page as that is the maximum height we need to show.
126         TileLayout tileLayout = mPages.get(0);
127         if (tileLayout == null) {
128             return 0;
129         }
130         return tileLayout.getTilesHeight();
131     }
132 
133     @Override
onConfigurationChanged(Configuration newConfig)134     protected void onConfigurationChanged(Configuration newConfig) {
135         super.onConfigurationChanged(newConfig);
136         // Pass configuration change to non-attached pages as well. Some config changes will cause
137         // QS to recreate itself (as determined in FragmentHostManager), but in order to minimize
138         // those, make sure that all get passed to all pages.
139         int numPages = mPages.size();
140         for (int i = 0; i < numPages; i++) {
141             View page = mPages.get(i);
142             if (page.getParent() == null) {
143                 page.dispatchConfigurationChanged(newConfig);
144             }
145         }
146         if (mLayoutOrientation != newConfig.orientation) {
147             mLayoutOrientation = newConfig.orientation;
148             forceTilesRedistribution("orientation changed to " + mLayoutOrientation);
149             setCurrentItem(0, false);
150             mPageToRestore = 0;
151         } else {
152             // logging in case we missed redistribution because orientation was not changed
153             // while configuration changed, can be removed after b/255208946 is fixed
154             mLogger.d(
155                     "Orientation didn't change, tiles might be not redistributed, new config",
156                     newConfig);
157         }
158     }
159 
160     @Override
onRtlPropertiesChanged(int layoutDirection)161     public void onRtlPropertiesChanged(int layoutDirection) {
162         // The configuration change will change the flag in the view (that's returned in
163         // isLayoutRtl). As we detect the change, we use the cached direction to store the page
164         // before setting it.
165         final int page = getPageNumberForDirection(mLayoutDirection == LAYOUT_DIRECTION_RTL);
166         super.onRtlPropertiesChanged(layoutDirection);
167         if (mLayoutDirection != layoutDirection) {
168             mLayoutDirection = layoutDirection;
169             setAdapter(mAdapter);
170             setCurrentItem(page, false);
171         }
172     }
173 
174     @Override
setCurrentItem(int item, boolean smoothScroll)175     public void setCurrentItem(int item, boolean smoothScroll) {
176         if (isLayoutRtl()) {
177             item = mPages.size() - 1 - item;
178         }
179         super.setCurrentItem(item, smoothScroll);
180     }
181 
182     /**
183      * Obtains the current page number respecting RTL
184      */
getCurrentPageNumber()185     private int getCurrentPageNumber() {
186         return getPageNumberForDirection(isLayoutRtl());
187     }
188 
getPageNumberForDirection(boolean isLayoutRTL)189     private int getPageNumberForDirection(boolean isLayoutRTL) {
190         int page = getCurrentItem();
191         if (isLayoutRTL) {
192             page = mPages.size() - 1 - page;
193         }
194         return page;
195     }
196 
197     // This will dump to the ui log all the tiles that are visible in this page
logVisibleTiles(TileLayout page)198     private void logVisibleTiles(TileLayout page) {
199         for (int i = 0; i < page.mRecords.size(); i++) {
200             QSTile t = page.mRecords.get(i).tile;
201             mUiEventLogger.logWithInstanceId(QSEvent.QS_TILE_VISIBLE, 0, t.getMetricsSpec(),
202                     t.getInstanceId());
203         }
204     }
205 
206     @Override
setListening(boolean listening, UiEventLogger uiEventLogger)207     public void setListening(boolean listening, UiEventLogger uiEventLogger) {
208         if (mListening == listening) return;
209         mListening = listening;
210         updateListening();
211     }
212 
213     @Override
setSquishinessFraction(float squishinessFraction)214     public void setSquishinessFraction(float squishinessFraction) {
215         int nPages = mPages.size();
216         for (int i = 0; i < nPages; i++) {
217             mPages.get(i).setSquishinessFraction(squishinessFraction);
218         }
219     }
220 
updateListening()221     private void updateListening() {
222         for (TileLayout tilePage : mPages) {
223             tilePage.setListening(tilePage.getParent() != null && mListening);
224         }
225     }
226 
227     @Override
fakeDragBy(float xOffset)228     public void fakeDragBy(float xOffset) {
229         try {
230             super.fakeDragBy(xOffset);
231             // Keep on drawing until the animation has finished.
232             postInvalidateOnAnimation();
233         } catch (NullPointerException e) {
234             mLogger.logException("FakeDragBy called before begin", e);
235             // If we were trying to fake drag, it means we just added a new tile to the last
236             // page, so animate there.
237             final int lastPageNumber = mPages.size() - 1;
238             post(() -> {
239                 setCurrentItem(lastPageNumber, true);
240                 if (mBounceAnimatorSet != null) {
241                     mBounceAnimatorSet.start();
242                 }
243                 setOffscreenPageLimit(1);
244             });
245         }
246     }
247 
248     @Override
endFakeDrag()249     public void endFakeDrag() {
250         try {
251             super.endFakeDrag();
252         } catch (NullPointerException e) {
253             // Not sure what's going on. Let's log it
254             mLogger.logException("endFakeDrag called without velocityTracker", e);
255         }
256     }
257 
258     @Override
computeScroll()259     public void computeScroll() {
260         if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
261             if (!isFakeDragging()) {
262                 beginFakeDrag();
263             }
264             fakeDragBy(getScrollX() - mScroller.getCurrX());
265         } else if (isFakeDragging()) {
266             endFakeDrag();
267             if (mBounceAnimatorSet != null) {
268                 mBounceAnimatorSet.start();
269             }
270             setOffscreenPageLimit(1);
271         }
272         super.computeScroll();
273     }
274 
275     @Override
hasOverlappingRendering()276     public boolean hasOverlappingRendering() {
277         return false;
278     }
279 
280     @Override
onFinishInflate()281     protected void onFinishInflate() {
282         super.onFinishInflate();
283         mPages.add(createTileLayout());
284         mAdapter.notifyDataSetChanged();
285     }
286 
createTileLayout()287     private TileLayout createTileLayout() {
288         TileLayout page = (TileLayout) LayoutInflater.from(getContext())
289                 .inflate(R.layout.qs_paged_page, this, false);
290         page.setMinRows(mMinRows);
291         page.setMaxColumns(mMaxColumns);
292         page.setSelected(false);
293         return page;
294     }
295 
setPageIndicator(PageIndicator indicator)296     public void setPageIndicator(PageIndicator indicator) {
297         mPageIndicator = indicator;
298         mPageIndicator.setNumPages(mPages.size());
299         mPageIndicator.setLocation(mPageIndicatorPosition);
300     }
301 
302     @Override
getOffsetTop(TileRecord tile)303     public int getOffsetTop(TileRecord tile) {
304         final ViewGroup parent = (ViewGroup) tile.tileView.getParent();
305         if (parent == null) return 0;
306         return parent.getTop() + getTop();
307     }
308 
309     @Override
addTile(TileRecord tile)310     public void addTile(TileRecord tile) {
311         mTiles.add(tile);
312         forceTilesRedistribution("adding new tile");
313         requestLayout();
314     }
315 
316     @Override
removeTile(TileRecord tile)317     public void removeTile(TileRecord tile) {
318         if (mTiles.remove(tile)) {
319             forceTilesRedistribution("removing tile");
320             requestLayout();
321         }
322     }
323 
324     @Override
setExpansion(float expansion, float proposedTranslation)325     public void setExpansion(float expansion, float proposedTranslation) {
326         mLastExpansion = expansion;
327         updateSelected();
328     }
329 
updateSelected()330     private void updateSelected() {
331         // Start the marquee when fully expanded and stop when fully collapsed. Leave as is for
332         // other expansion ratios since there is no way way to pause the marquee.
333         if (mLastExpansion > 0f && mLastExpansion < 1f) {
334             return;
335         }
336         boolean selected = mLastExpansion == 1f;
337 
338         // Disable accessibility temporarily while we update selected state purely for the
339         // marquee. This will ensure that accessibility doesn't announce the TYPE_VIEW_SELECTED
340         // event on any of the children.
341         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
342         int currentItem = getCurrentPageNumber();
343         for (int i = 0; i < mPages.size(); i++) {
344             TileLayout page = mPages.get(i);
345             page.setSelected(i == currentItem ? selected : false);
346             if (page.isSelected()) {
347                 logVisibleTiles(page);
348             }
349         }
350         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
351     }
352 
setPageListener(PageListener listener)353     public void setPageListener(PageListener listener) {
354         mPageListener = listener;
355     }
356 
getSpecsForPage(int page)357     public List<String> getSpecsForPage(int page) {
358         ArrayList<String> out = new ArrayList<>();
359         if (page < 0) return out;
360         int perPage = mPages.get(0).maxTiles();
361         int startOfPage = page * perPage;
362         int endOfPage = (page + 1) * perPage;
363         for (int i = startOfPage; i < endOfPage && i < mTiles.size(); i++) {
364             out.add(mTiles.get(i).tile.getTileSpec());
365         }
366         return out;
367     }
368 
distributeTiles()369     private void distributeTiles() {
370         emptyAndInflateOrRemovePages();
371 
372         final int tilesPerPageCount = mPages.get(0).maxTiles();
373         int index = 0;
374         final int totalTilesCount = mTiles.size();
375         mLogger.logTileDistributionInProgress(tilesPerPageCount, totalTilesCount);
376         for (int i = 0; i < totalTilesCount; i++) {
377             TileRecord tile = mTiles.get(i);
378             if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++;
379             mLogger.logTileDistributed(tile.tile.getClass().getSimpleName(), index);
380             mPages.get(index).addTile(tile);
381         }
382     }
383 
emptyAndInflateOrRemovePages()384     private void emptyAndInflateOrRemovePages() {
385         final int numPages = getNumPages();
386         final int NP = mPages.size();
387         for (int i = 0; i < NP; i++) {
388             mPages.get(i).removeAllViews();
389         }
390         if (NP == numPages) {
391             return;
392         }
393         while (mPages.size() < numPages) {
394             mLogger.d("Adding new page");
395             mPages.add(createTileLayout());
396         }
397         while (mPages.size() > numPages) {
398             mLogger.d("Removing page");
399             mPages.remove(mPages.size() - 1);
400         }
401         mPageIndicator.setNumPages(mPages.size());
402         setAdapter(mAdapter);
403         mAdapter.notifyDataSetChanged();
404         if (mPageToRestore != NO_PAGE) {
405             setCurrentItem(mPageToRestore, false);
406             mPageToRestore = NO_PAGE;
407         }
408     }
409 
410     @Override
updateResources()411     public boolean updateResources() {
412         boolean changed = false;
413         for (int i = 0; i < mPages.size(); i++) {
414             changed |= mPages.get(i).updateResources();
415         }
416         if (changed) {
417             forceTilesRedistribution("resources in pages changed");
418             requestLayout();
419         } else {
420             // logging in case we missed redistribution because number of column in updateResources
421             // was not changed, can be removed after b/255208946 is fixed
422             mLogger.d("resource in pages didn't change, tiles might be not redistributed");
423         }
424         return changed;
425     }
426 
427     @Override
setMinRows(int minRows)428     public boolean setMinRows(int minRows) {
429         mMinRows = minRows;
430         boolean changed = false;
431         for (int i = 0; i < mPages.size(); i++) {
432             if (mPages.get(i).setMinRows(minRows)) {
433                 changed = true;
434                 forceTilesRedistribution("minRows changed in page");
435             }
436         }
437         return changed;
438     }
439 
440     @Override
setMaxColumns(int maxColumns)441     public boolean setMaxColumns(int maxColumns) {
442         mMaxColumns = maxColumns;
443         boolean changed = false;
444         for (int i = 0; i < mPages.size(); i++) {
445             if (mPages.get(i).setMaxColumns(maxColumns)) {
446                 changed = true;
447                 forceTilesRedistribution("maxColumns in pages changed");
448             }
449         }
450         return changed;
451     }
452 
453     /**
454      * Set the amount of excess space that we gave this view compared to the actual available
455      * height. This is because this view is in a scrollview.
456      */
setExcessHeight(int excessHeight)457     public void setExcessHeight(int excessHeight) {
458         mExcessHeight = excessHeight;
459     }
460 
461     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)462     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
463 
464         final int nTiles = mTiles.size();
465         // If we have no reason to recalculate the number of rows, skip this step. In particular,
466         // if the height passed by its parent is the same as the last time, we try not to remeasure.
467         if (mDistributeTiles || mLastMaxHeight != MeasureSpec.getSize(heightMeasureSpec)
468                 || mLastExcessHeight != mExcessHeight) {
469 
470             mLastMaxHeight = MeasureSpec.getSize(heightMeasureSpec);
471             mLastExcessHeight = mExcessHeight;
472             // Only change the pages if the number of rows or columns (from updateResources) has
473             // changed or the tiles have changed
474             int availableHeight = mLastMaxHeight - mExcessHeight;
475             if (mPages.get(0).updateMaxRows(availableHeight, nTiles) || mDistributeTiles) {
476                 mDistributeTiles = false;
477                 distributeTiles();
478             }
479 
480             final int nRows = mPages.get(0).mRows;
481             for (int i = 0; i < mPages.size(); i++) {
482                 TileLayout t = mPages.get(i);
483                 t.mRows = nRows;
484             }
485         }
486 
487         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
488 
489         // The ViewPager likes to eat all of the space, instead force it to wrap to the max height
490         // of the pages.
491         int maxHeight = 0;
492         final int N = getChildCount();
493         for (int i = 0; i < N; i++) {
494             int height = getChildAt(i).getMeasuredHeight();
495             if (height > maxHeight) {
496                 maxHeight = height;
497             }
498         }
499         if (mPages.get(0).getParent() == null) {
500             // Measure page 0 so we know how tall it is if it's not attached to the pager.
501             mPages.get(0).measure(widthMeasureSpec, heightMeasureSpec);
502             int height = mPages.get(0).getMeasuredHeight();
503             if (height > maxHeight) {
504                 maxHeight = height;
505             }
506         }
507         setMeasuredDimension(getMeasuredWidth(), maxHeight + getPaddingBottom());
508     }
509 
510     @Override
onLayout(boolean changed, int l, int t, int r, int b)511     protected void onLayout(boolean changed, int l, int t, int r, int b) {
512         super.onLayout(changed, l, t, r, b);
513         if (mPages.get(0).getParent() == null) {
514             // Layout page 0, so we can get the bottom of the tiles. We only do this if the page
515             // is not attached.
516             mPages.get(0).layout(l, t, r, b);
517         }
518     }
519 
getColumnCount()520     public int getColumnCount() {
521         if (mPages.size() == 0) return 0;
522         return mPages.get(0).mColumns;
523     }
524 
525     /**
526      * Gets the number of pages in this paged tile layout
527      */
getNumPages()528     public int getNumPages() {
529         final int nTiles = mTiles.size();
530         // We should always have at least one page, even if it's empty.
531         int numPages = Math.max(nTiles / mPages.get(0).maxTiles(), 1);
532 
533         // Add one more not full page if needed
534         if (nTiles > numPages * mPages.get(0).maxTiles()) {
535             numPages++;
536         }
537 
538         return numPages;
539     }
540 
getNumVisibleTiles()541     public int getNumVisibleTiles() {
542         if (mPages.size() == 0) return 0;
543         TileLayout currentPage = mPages.get(getCurrentPageNumber());
544         return currentPage.mRecords.size();
545     }
546 
getNumTilesFirstPage()547     public int getNumTilesFirstPage() {
548         if (mPages.size() == 0) return 0;
549         return mPages.get(0).mRecords.size();
550     }
551 
startTileReveal(Set<String> tileSpecs, final Runnable postAnimation)552     public void startTileReveal(Set<String> tileSpecs, final Runnable postAnimation) {
553         if (tileSpecs.isEmpty() || mPages.size() < 2 || getScrollX() != 0 || !beginFakeDrag()) {
554             // Do not start the reveal animation unless there are tiles to animate, multiple
555             // TileLayouts available and the user has not already started dragging.
556             return;
557         }
558 
559         final int lastPageNumber = mPages.size() - 1;
560         final TileLayout lastPage = mPages.get(lastPageNumber);
561         final ArrayList<Animator> bounceAnims = new ArrayList<>();
562         for (TileRecord tr : lastPage.mRecords) {
563             if (tileSpecs.contains(tr.tile.getTileSpec())) {
564                 bounceAnims.add(setupBounceAnimator(tr.tileView, bounceAnims.size()));
565             }
566         }
567 
568         if (bounceAnims.isEmpty()) {
569             // All tileSpecs are on the first page. Nothing to do.
570             // TODO: potentially show a bounce animation for first page QS tiles
571             endFakeDrag();
572             return;
573         }
574 
575         mBounceAnimatorSet = new AnimatorSet();
576         mBounceAnimatorSet.playTogether(bounceAnims);
577         mBounceAnimatorSet.addListener(new AnimatorListenerAdapter() {
578             @Override
579             public void onAnimationEnd(Animator animation) {
580                 mBounceAnimatorSet = null;
581                 postAnimation.run();
582             }
583         });
584         setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
585         int dx = getWidth() * lastPageNumber;
586         mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
587                 REVEAL_SCROLL_DURATION_MILLIS);
588         postInvalidateOnAnimation();
589     }
590 
sanitizePageAction(int action)591     private int sanitizePageAction(int action) {
592         int pageLeftId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT.getId();
593         int pageRightId = AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT.getId();
594         if (action == pageLeftId || action == pageRightId) {
595             if (!isLayoutRtl()) {
596                 if (action == pageLeftId) {
597                     return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
598                 } else {
599                     return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
600                 }
601             } else {
602                 if (action == pageLeftId) {
603                     return AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
604                 } else {
605                     return AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
606                 }
607             }
608         }
609         return action;
610     }
611 
612     @Override
performAccessibilityAction(int action, Bundle arguments)613     public boolean performAccessibilityAction(int action, Bundle arguments) {
614         action = sanitizePageAction(action);
615         boolean performed = super.performAccessibilityAction(action, arguments);
616         if (performed && (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
617                 || action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
618             requestAccessibilityFocus();
619         }
620         return performed;
621     }
622 
623     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)624     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
625         super.onInitializeAccessibilityNodeInfoInternal(info);
626         // getCurrentItem does not respect RTL, so it works well together with page actions that
627         // use left/right positioning.
628         if (getCurrentItem() != 0) {
629             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
630         }
631         if (getCurrentItem() != mPages.size() - 1) {
632             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
633         }
634     }
635 
636     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)637     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
638         super.onInitializeAccessibilityEvent(event);
639         if (mAdapter != null && mAdapter.getCount() > 0) {
640             event.setItemCount(mAdapter.getCount());
641             event.setFromIndex(getCurrentPageNumber());
642             event.setToIndex(getCurrentPageNumber());
643         }
644     }
645 
setupBounceAnimator(View view, int ordinal)646     private static Animator setupBounceAnimator(View view, int ordinal) {
647         view.setAlpha(0f);
648         view.setScaleX(0f);
649         view.setScaleY(0f);
650         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view,
651                 PropertyValuesHolder.ofFloat(View.ALPHA, 1),
652                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
653                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
654         animator.setDuration(BOUNCE_ANIMATION_DURATION);
655         animator.setStartDelay(ordinal * TILE_ANIMATION_STAGGER_DELAY);
656         animator.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION));
657         return animator;
658     }
659 
660     private final ViewPager.OnPageChangeListener mOnPageChangeListener =
661             new ViewPager.SimpleOnPageChangeListener() {
662 
663                 private int mCurrentScrollState = SCROLL_STATE_IDLE;
664                 // Flag to avoid redundant call InteractionJankMonitor::begin()
665                 private boolean mIsScrollJankTraceBegin = false;
666 
667                 @Override
668                 public void onPageSelected(int position) {
669                     updateSelected();
670                     if (mPageIndicator == null) return;
671                     if (mPageListener != null) {
672                         int pageNumber = isLayoutRtl() ? mPages.size() - 1 - position : position;
673                         mPageListener.onPageChanged(pageNumber == 0, pageNumber);
674                     }
675                 }
676 
677                 @Override
678                 public void onPageScrolled(int position, float positionOffset,
679                         int positionOffsetPixels) {
680 
681                     if (!mIsScrollJankTraceBegin && mCurrentScrollState == SCROLL_STATE_DRAGGING) {
682                         InteractionJankMonitor.getInstance().begin(PagedTileLayout.this,
683                                 CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE);
684                         mIsScrollJankTraceBegin = true;
685                     }
686 
687                     if (mPageIndicator == null) return;
688                     mPageIndicatorPosition = position + positionOffset;
689                     mPageIndicator.setLocation(mPageIndicatorPosition);
690                     if (mPageListener != null) {
691                         int pageNumber = isLayoutRtl() ? mPages.size() - 1 - position : position;
692                         mPageListener.onPageChanged(
693                                 positionOffsetPixels == 0 && pageNumber == 0,
694                                 // Send only valid page number on integer pages
695                                 positionOffsetPixels == 0 ? pageNumber : PageListener.INVALID_PAGE
696                         );
697                     }
698                 }
699 
700                 @Override
701                 public void onPageScrollStateChanged(int state) {
702                     if (state != mCurrentScrollState && state == SCROLL_STATE_IDLE) {
703                         InteractionJankMonitor.getInstance().end(
704                                 CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE);
705                         mIsScrollJankTraceBegin = false;
706                     }
707                     mCurrentScrollState = state;
708                 }
709             };
710 
711     private final PagerAdapter mAdapter = new PagerAdapter() {
712         @Override
713         public void destroyItem(ViewGroup container, int position, Object object) {
714             mLogger.d("Destantiating page at", position);
715             container.removeView((View) object);
716             updateListening();
717         }
718 
719         @Override
720         public Object instantiateItem(ViewGroup container, int position) {
721             mLogger.d("Instantiating page at", position);
722             if (isLayoutRtl()) {
723                 position = mPages.size() - 1 - position;
724             }
725             ViewGroup view = mPages.get(position);
726             if (view.getParent() != null) {
727                 container.removeView(view);
728             }
729             container.addView(view);
730             updateListening();
731             return view;
732         }
733 
734         @Override
735         public int getCount() {
736             return mPages.size();
737         }
738 
739         @Override
740         public boolean isViewFromObject(View view, Object object) {
741             return view == object;
742         }
743     };
744 
745     /**
746      * Force all tiles to be redistributed across pages.
747      * Should be called when one of the following changes: rows, columns, number of tiles.
748      */
forceTilesRedistribution(String reason)749     public void forceTilesRedistribution(String reason) {
750         mLogger.d("forcing tile redistribution across pages, reason", reason);
751         mDistributeTiles = true;
752     }
753 
setLogger(QSLogger qsLogger)754     public void setLogger(QSLogger qsLogger) {
755         mLogger = qsLogger;
756     }
757 
758     public interface PageListener {
759         int INVALID_PAGE = -1;
760 
onPageChanged(boolean isFirst, int pageNumber)761         void onPageChanged(boolean isFirst, int pageNumber);
762     }
763 }
764