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