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