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