1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3; 18 19 import static com.android.app.animation.Interpolators.SCROLL; 20 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled; 21 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType; 22 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR; 23 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY; 24 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO; 25 26 import android.animation.LayoutTransition; 27 import android.annotation.SuppressLint; 28 import android.content.Context; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.graphics.Canvas; 33 import android.graphics.Rect; 34 import android.os.Bundle; 35 import android.provider.Settings; 36 import android.util.AttributeSet; 37 import android.util.Log; 38 import android.view.InputDevice; 39 import android.view.KeyEvent; 40 import android.view.MotionEvent; 41 import android.view.VelocityTracker; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.ViewDebug; 45 import android.view.ViewGroup; 46 import android.view.ViewParent; 47 import android.view.accessibility.AccessibilityEvent; 48 import android.view.accessibility.AccessibilityNodeInfo; 49 import android.widget.OverScroller; 50 import android.widget.ScrollView; 51 52 import androidx.annotation.Nullable; 53 54 import com.android.launcher3.compat.AccessibilityManagerCompat; 55 import com.android.launcher3.config.FeatureFlags; 56 import com.android.launcher3.pageindicators.PageIndicator; 57 import com.android.launcher3.touch.PagedOrientationHandler; 58 import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds; 59 import com.android.launcher3.util.EdgeEffectCompat; 60 import com.android.launcher3.util.IntSet; 61 import com.android.launcher3.util.Thunk; 62 import com.android.launcher3.views.ActivityContext; 63 64 import java.util.ArrayList; 65 import java.util.function.Consumer; 66 67 /** 68 * An abstraction of the original Workspace which supports browsing through a 69 * sequential list of "pages" 70 */ 71 public abstract class PagedView<T extends View & PageIndicator> extends ViewGroup { 72 private static final String TAG = "PagedView"; 73 private static final boolean DEBUG = false; 74 public static final boolean DEBUG_FAILED_QUICKSWITCH = false; 75 76 public static final int ACTION_MOVE_ALLOW_EASY_FLING = MotionEvent.ACTION_MASK - 1; 77 public static final int INVALID_PAGE = -1; 78 protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE; 79 80 private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; 81 // The page is moved more than halfway, automatically move to the next page on touch up. 82 private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; 83 84 private static final float MAX_SCROLL_PROGRESS = 1.0f; 85 86 private boolean mFreeScroll = false; 87 88 private int mFlingThresholdVelocity; 89 private int mEasyFlingThresholdVelocity; 90 private int mMinFlingVelocity; 91 private int mMinSnapVelocity; 92 private int mPageSnapAnimationDuration; 93 94 protected boolean mFirstLayout = true; 95 96 @ViewDebug.ExportedProperty(category = "launcher") 97 protected int mCurrentPage; 98 // Difference between current scroll position and mCurrentPage's page scroll. Used to maintain 99 // relative scroll position unchanged in updateCurrentPageScroll. Cleared when snapping to a 100 // page. 101 protected int mCurrentPageScrollDiff; 102 // The current page the PagedView is scrolling over on it's way to the destination page. 103 protected int mCurrentScrollOverPage; 104 105 @ViewDebug.ExportedProperty(category = "launcher") 106 protected int mNextPage = INVALID_PAGE; 107 protected int mMaxScroll; 108 protected int mMinScroll; 109 protected OverScroller mScroller; 110 private VelocityTracker mVelocityTracker; 111 protected int mPageSpacing = 0; 112 113 private float mDownMotionX; 114 private float mDownMotionY; 115 private float mDownMotionPrimary; 116 private int mLastMotion; 117 private float mTotalMotion; 118 // Used in special cases where the fling checks can be relaxed for an intentional gesture 119 private boolean mAllowEasyFling; 120 protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT; 121 122 private final ArrayList<Runnable> mOnPageScrollsInitializedCallbacks = new ArrayList<>(); 123 124 // We should always check pageScrollsInitialized() is true when using mPageScrolls. 125 @Nullable protected int[] mPageScrolls = null; 126 private boolean mIsBeingDragged; 127 128 // The amount of movement to begin scrolling 129 protected int mTouchSlop; 130 // The amount of movement to begin paging 131 protected int mPageSlop; 132 private int mMaximumVelocity; 133 protected boolean mAllowOverScroll = true; 134 135 protected static final int INVALID_POINTER = -1; 136 137 protected int mActivePointerId = INVALID_POINTER; 138 139 protected boolean mIsPageInTransition = false; 140 private Runnable mOnPageTransitionEndCallback; 141 142 // Page Indicator 143 @Thunk int mPageIndicatorViewId; 144 protected T mPageIndicator; 145 146 protected final Rect mInsets = new Rect(); 147 protected boolean mIsRtl; 148 149 // Similar to the platform implementation of isLayoutValid(); 150 protected boolean mIsLayoutValid; 151 152 private int[] mTmpIntPair = new int[2]; 153 154 protected EdgeEffectCompat mEdgeGlowLeft; 155 protected EdgeEffectCompat mEdgeGlowRight; 156 PagedView(Context context)157 public PagedView(Context context) { 158 this(context, null); 159 } 160 PagedView(Context context, AttributeSet attrs)161 public PagedView(Context context, AttributeSet attrs) { 162 this(context, attrs, 0); 163 } 164 PagedView(Context context, AttributeSet attrs, int defStyle)165 public PagedView(Context context, AttributeSet attrs, int defStyle) { 166 super(context, attrs, defStyle); 167 168 TypedArray a = context.obtainStyledAttributes(attrs, 169 R.styleable.PagedView, defStyle, 0); 170 mPageIndicatorViewId = a.getResourceId(R.styleable.PagedView_pageIndicator, -1); 171 a.recycle(); 172 173 setHapticFeedbackEnabled(false); 174 mIsRtl = Utilities.isRtl(getResources()); 175 176 mScroller = new OverScroller(context, SCROLL); 177 mCurrentPage = 0; 178 mCurrentScrollOverPage = 0; 179 180 final ViewConfiguration configuration = ViewConfiguration.get(context); 181 mTouchSlop = configuration.getScaledTouchSlop(); 182 mPageSlop = configuration.getScaledPagingTouchSlop(); 183 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 184 185 updateVelocityValues(); 186 187 initEdgeEffect(); 188 setDefaultFocusHighlightEnabled(false); 189 setWillNotDraw(false); 190 } 191 initEdgeEffect()192 protected void initEdgeEffect() { 193 mEdgeGlowLeft = new EdgeEffectCompat(getContext()); 194 mEdgeGlowRight = new EdgeEffectCompat(getContext()); 195 } 196 initParentViews(View parent)197 public void initParentViews(View parent) { 198 if (mPageIndicatorViewId > -1) { 199 mPageIndicator = parent.findViewById(mPageIndicatorViewId); 200 mPageIndicator.setMarkersCount(getChildCount() / getPanelCount()); 201 } 202 } 203 getPageIndicator()204 public T getPageIndicator() { 205 return mPageIndicator; 206 } 207 208 /** 209 * Returns the index of the currently displayed page. When in free scroll mode, this is the page 210 * that the user was on before entering free scroll mode (e.g. the home screen page they 211 * long-pressed on to enter the overview). Try using {@link #getDestinationPage()} 212 * to get the page the user is currently scrolling over. 213 */ getCurrentPage()214 public int getCurrentPage() { 215 return mCurrentPage; 216 } 217 218 /** 219 * Returns the index of page to be shown immediately afterwards. 220 */ getNextPage()221 public int getNextPage() { 222 return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; 223 } 224 getPageCount()225 public int getPageCount() { 226 return getChildCount(); 227 } 228 getPageAt(int index)229 public View getPageAt(int index) { 230 return getChildAt(index); 231 } 232 233 /** 234 * Updates the scroll of the current page immediately to its final scroll position. We use this 235 * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of 236 * the previous tab page. 237 */ updateCurrentPageScroll()238 protected void updateCurrentPageScroll() { 239 // If the current page is invalid, just reset the scroll position to zero 240 int newPosition = 0; 241 if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { 242 newPosition = getScrollForPage(mCurrentPage) + mCurrentPageScrollDiff; 243 } 244 mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, newPosition); 245 mScroller.startScroll(mScroller.getCurrX(), 0, newPosition - mScroller.getCurrX(), 0); 246 forceFinishScroller(); 247 } 248 249 /** 250 * Immediately finishes any overscroll effect and jumps to the end of the scroller animation. 251 */ abortScrollerAnimation()252 public void abortScrollerAnimation() { 253 mEdgeGlowLeft.finish(); 254 mEdgeGlowRight.finish(); 255 abortScrollerAnimation(true); 256 } 257 onScrollerAnimationAborted()258 protected void onScrollerAnimationAborted() { 259 // No-Op 260 } 261 abortScrollerAnimation(boolean resetNextPage)262 private void abortScrollerAnimation(boolean resetNextPage) { 263 mScroller.abortAnimation(); 264 onScrollerAnimationAborted(); 265 // We need to clean up the next page here to avoid computeScrollHelper from 266 // updating current page on the pass. 267 if (resetNextPage) { 268 mNextPage = INVALID_PAGE; 269 pageEndTransition(); 270 } 271 } 272 273 /** 274 * Immediately finishes any in-progress scroll, maintaining the current position. Also sets 275 * mNextPage = INVALID_PAGE and calls pageEndTransition(). 276 */ forceFinishScroller()277 public void forceFinishScroller() { 278 mScroller.forceFinished(true); 279 // We need to clean up the next page here to avoid computeScrollHelper from 280 // updating current page on the pass. 281 mNextPage = INVALID_PAGE; 282 pageEndTransition(); 283 } 284 validateNewPage(int newPage)285 private int validateNewPage(int newPage) { 286 newPage = ensureWithinScrollBounds(newPage); 287 // Ensure that it is clamped by the actual set of children in all cases 288 newPage = Utilities.boundToRange(newPage, 0, getPageCount() - 1); 289 290 if (getPanelCount() > 1) { 291 // Always return left most panel as new page 292 newPage = getLeftmostVisiblePageForIndex(newPage); 293 } 294 return newPage; 295 } 296 297 /** 298 * In most cases where panelCount is 1, this method will just return the page index that was 299 * passed in. 300 * But for example when two panel home is enabled we might need the leftmost visible page index 301 * because that page is the current page. 302 */ getLeftmostVisiblePageForIndex(int pageIndex)303 public int getLeftmostVisiblePageForIndex(int pageIndex) { 304 int panelCount = getPanelCount(); 305 return pageIndex - pageIndex % panelCount; 306 } 307 308 /** 309 * Returns the number of pages that are shown at the same time. 310 */ getPanelCount()311 protected int getPanelCount() { 312 return 1; 313 } 314 315 /** 316 * Returns an IntSet with the indices of the currently visible pages 317 */ getVisiblePageIndices()318 public IntSet getVisiblePageIndices() { 319 return getPageIndices(mCurrentPage); 320 } 321 322 /** 323 * In case the panelCount is 1 this just returns the same page index in an IntSet. 324 * But in cases where the panelCount > 1 this will return all the page indices that belong 325 * together, i.e. on the Workspace they are next to each other and shown at the same time. 326 */ getPageIndices(int pageIndex)327 private IntSet getPageIndices(int pageIndex) { 328 // we want to make sure the pageIndex is the leftmost page 329 pageIndex = getLeftmostVisiblePageForIndex(pageIndex); 330 331 IntSet pageIndices = new IntSet(); 332 int panelCount = getPanelCount(); 333 int pageCount = getPageCount(); 334 for (int page = pageIndex; page < pageIndex + panelCount && page < pageCount; page++) { 335 pageIndices.add(page); 336 } 337 return pageIndices; 338 } 339 340 /** 341 * Returns an IntSet with the indices of the neighbour pages that are in the focus direction. 342 */ getNeighbourPageIndices(int focus)343 private IntSet getNeighbourPageIndices(int focus) { 344 int panelCount = getPanelCount(); 345 // getNextPage is more reliable than getCurrentPage 346 int currentPage = getNextPage(); 347 348 int nextPage; 349 if (focus == View.FOCUS_LEFT) { 350 nextPage = currentPage - panelCount; 351 } else if (focus == View.FOCUS_RIGHT) { 352 nextPage = currentPage + panelCount; 353 } else { 354 // no neighbours to other directions 355 return new IntSet(); 356 } 357 nextPage = validateNewPage(nextPage); 358 if (nextPage == currentPage) { 359 // We reached the end of the pages 360 return new IntSet(); 361 } 362 363 return getPageIndices(nextPage); 364 } 365 366 /** 367 * Executes the callback against each visible page 368 */ forEachVisiblePage(Consumer<View> callback)369 public void forEachVisiblePage(Consumer<View> callback) { 370 getVisiblePageIndices().forEach(pageIndex -> { 371 View page = getPageAt(pageIndex); 372 if (page != null) { 373 callback.accept(page); 374 } 375 }); 376 } 377 378 /** 379 * Returns true if the view is on one of the current pages, false otherwise. 380 */ isVisible(View child)381 public boolean isVisible(View child) { 382 return isVisible(indexOfChild(child)); 383 } 384 385 /** 386 * Returns true if the page with the given index is currently visible, false otherwise. 387 */ isVisible(int pageIndex)388 private boolean isVisible(int pageIndex) { 389 return getLeftmostVisiblePageForIndex(pageIndex) == mCurrentPage; 390 } 391 392 /** 393 * @return The closest page to the provided page that is within mMinScrollX and mMaxScrollX. 394 */ ensureWithinScrollBounds(int page)395 private int ensureWithinScrollBounds(int page) { 396 int dir = !mIsRtl ? 1 : - 1; 397 int currScroll = getScrollForPage(page); 398 int prevScroll; 399 while (currScroll < mMinScroll) { 400 page += dir; 401 prevScroll = currScroll; 402 currScroll = getScrollForPage(page); 403 if (currScroll <= prevScroll) { 404 Log.e(TAG, "validateNewPage: failed to find a page > mMinScrollX"); 405 break; 406 } 407 } 408 while (currScroll > mMaxScroll) { 409 page -= dir; 410 prevScroll = currScroll; 411 currScroll = getScrollForPage(page); 412 if (currScroll >= prevScroll) { 413 Log.e(TAG, "validateNewPage: failed to find a page < mMaxScrollX"); 414 break; 415 } 416 } 417 return page; 418 } 419 setCurrentPage(int currentPage)420 public void setCurrentPage(int currentPage) { 421 setCurrentPage(currentPage, INVALID_PAGE); 422 } 423 424 /** 425 * Sets the current page. 426 */ setCurrentPage(int currentPage, int overridePrevPage)427 public void setCurrentPage(int currentPage, int overridePrevPage) { 428 if (!mScroller.isFinished()) { 429 abortScrollerAnimation(true); 430 } 431 // don't introduce any checks like mCurrentPage == currentPage here-- if we change the 432 // the default 433 if (getChildCount() == 0) { 434 return; 435 } 436 int prevPage = overridePrevPage != INVALID_PAGE ? overridePrevPage : mCurrentPage; 437 mCurrentPage = validateNewPage(currentPage); 438 mCurrentScrollOverPage = mCurrentPage; 439 updateCurrentPageScroll(); 440 notifyPageSwitchListener(prevPage); 441 invalidate(); 442 } 443 444 /** 445 * Should be called whenever the page changes. In the case of a scroll, we wait until the page 446 * has settled. 447 */ notifyPageSwitchListener(int prevPage)448 protected void notifyPageSwitchListener(int prevPage) { 449 updatePageIndicator(); 450 } 451 updatePageIndicator()452 private void updatePageIndicator() { 453 if (mPageIndicator != null) { 454 mPageIndicator.setActiveMarker(getNextPage()); 455 } 456 } pageBeginTransition()457 protected void pageBeginTransition() { 458 if (!mIsPageInTransition) { 459 mIsPageInTransition = true; 460 onPageBeginTransition(); 461 } 462 } 463 pageEndTransition()464 protected void pageEndTransition() { 465 if (mIsPageInTransition && !mIsBeingDragged && mScroller.isFinished() 466 && (!isShown() || (mEdgeGlowLeft.isFinished() && mEdgeGlowRight.isFinished()))) { 467 mIsPageInTransition = false; 468 onPageEndTransition(); 469 } 470 } 471 472 @Override onVisibilityAggregated(boolean isVisible)473 public void onVisibilityAggregated(boolean isVisible) { 474 pageEndTransition(); 475 super.onVisibilityAggregated(isVisible); 476 } 477 isPageInTransition()478 protected boolean isPageInTransition() { 479 return mIsPageInTransition; 480 } 481 482 /** 483 * Called when the page starts moving as part of the scroll. Subclasses can override this 484 * to provide custom behavior during animation. 485 */ onPageBeginTransition()486 protected void onPageBeginTransition() { 487 } 488 489 /** 490 * Called when the page ends moving as part of the scroll. Subclasses can override this 491 * to provide custom behavior during animation. 492 */ onPageEndTransition()493 protected void onPageEndTransition() { 494 mCurrentPageScrollDiff = 0; 495 AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext()); 496 AccessibilityManagerCompat.sendCustomAccessibilityEvent(getPageAt(mCurrentPage), 497 AccessibilityEvent.TYPE_VIEW_FOCUSED, null); 498 if (mOnPageTransitionEndCallback != null) { 499 mOnPageTransitionEndCallback.run(); 500 mOnPageTransitionEndCallback = null; 501 } 502 } 503 504 /** 505 * Sets a callback to run once when the scrolling finishes. If there is currently 506 * no page in transition, then the callback is called immediately. 507 */ setOnPageTransitionEndCallback(@ullable Runnable callback)508 public void setOnPageTransitionEndCallback(@Nullable Runnable callback) { 509 if (mIsPageInTransition || callback == null) { 510 mOnPageTransitionEndCallback = callback; 511 } else { 512 callback.run(); 513 } 514 } 515 516 @Override scrollTo(int x, int y)517 public void scrollTo(int x, int y) { 518 x = Utilities.boundToRange(x, 519 mOrientationHandler.getPrimaryValue(mMinScroll, 0), mMaxScroll); 520 y = Utilities.boundToRange(y, 521 mOrientationHandler.getPrimaryValue(0, mMinScroll), mMaxScroll); 522 super.scrollTo(x, y); 523 } 524 sendScrollAccessibilityEvent()525 private void sendScrollAccessibilityEvent() { 526 if (isObservedEventType(getContext(), AccessibilityEvent.TYPE_VIEW_SCROLLED)) { 527 if (mCurrentPage != getNextPage()) { 528 AccessibilityEvent ev = 529 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); 530 ev.setScrollable(true); 531 ev.setScrollX(getScrollX()); 532 ev.setScrollY(getScrollY()); 533 mOrientationHandler.setMaxScroll(ev, mMaxScroll); 534 sendAccessibilityEventUnchecked(ev); 535 } 536 } 537 } 538 announcePageForAccessibility()539 protected void announcePageForAccessibility() { 540 if (isAccessibilityEnabled(getContext())) { 541 // Notify the user when the page changes 542 announceForAccessibility(getCurrentPageDescription()); 543 } 544 } 545 computeScrollHelper()546 protected boolean computeScrollHelper() { 547 if (mScroller.computeScrollOffset()) { 548 // Don't bother scrolling if the page does not need to be moved 549 int oldPos = mOrientationHandler.getPrimaryScroll(this); 550 int newPos = mScroller.getCurrX(); 551 if (oldPos != newPos) { 552 mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, mScroller.getCurrX()); 553 } 554 555 if (mAllowOverScroll) { 556 if (newPos < mMinScroll && oldPos >= mMinScroll) { 557 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); 558 abortScrollerAnimation(false); 559 onEdgeAbsorbingScroll(); 560 } else if (newPos > mMaxScroll && oldPos <= mMaxScroll) { 561 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); 562 abortScrollerAnimation(false); 563 onEdgeAbsorbingScroll(); 564 } 565 } 566 567 // If the scroller has scrolled to the final position and there is no edge effect, then 568 // finish the scroller to skip waiting for additional settling 569 int finalPos = mOrientationHandler.getPrimaryValue(mScroller.getFinalX(), 570 mScroller.getFinalY()); 571 if (newPos == finalPos && mEdgeGlowLeft.isFinished() && mEdgeGlowRight.isFinished()) { 572 abortScrollerAnimation(false); 573 } 574 575 invalidate(); 576 return true; 577 } else if (mNextPage != INVALID_PAGE) { 578 sendScrollAccessibilityEvent(); 579 int prevPage = mCurrentPage; 580 mCurrentPage = validateNewPage(mNextPage); 581 mCurrentScrollOverPage = mCurrentPage; 582 mNextPage = INVALID_PAGE; 583 notifyPageSwitchListener(prevPage); 584 585 // We don't want to trigger a page end moving unless the page has settled 586 // and the user has stopped scrolling 587 if (!mIsBeingDragged) { 588 pageEndTransition(); 589 } 590 591 if (canAnnouncePageDescription()) { 592 announcePageForAccessibility(); 593 } 594 } 595 return false; 596 } 597 598 @Override computeScroll()599 public void computeScroll() { 600 computeScrollHelper(); 601 } 602 getExpectedHeight()603 public int getExpectedHeight() { 604 return getMeasuredHeight(); 605 } 606 getNormalChildHeight()607 public int getNormalChildHeight() { 608 return getExpectedHeight() - getPaddingTop() - getPaddingBottom() 609 - mInsets.top - mInsets.bottom; 610 } 611 getExpectedWidth()612 public int getExpectedWidth() { 613 return getMeasuredWidth(); 614 } 615 getNormalChildWidth()616 public int getNormalChildWidth() { 617 return getExpectedWidth() - getPaddingLeft() - getPaddingRight() 618 - mInsets.left - mInsets.right; 619 } 620 updateVelocityValues()621 private void updateVelocityValues() { 622 Resources res = getResources(); 623 mFlingThresholdVelocity = res.getDimensionPixelSize(R.dimen.fling_threshold_velocity); 624 mEasyFlingThresholdVelocity = 625 res.getDimensionPixelSize(R.dimen.easy_fling_threshold_velocity); 626 mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity); 627 mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity); 628 mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration); 629 } 630 631 @Override onConfigurationChanged(Configuration newConfig)632 protected void onConfigurationChanged(Configuration newConfig) { 633 super.onConfigurationChanged(newConfig); 634 updateVelocityValues(); 635 } 636 637 @Override requestLayout()638 public void requestLayout() { 639 mIsLayoutValid = false; 640 super.requestLayout(); 641 } 642 643 @Override forceLayout()644 public void forceLayout() { 645 mIsLayoutValid = false; 646 super.forceLayout(); 647 } 648 getPageWidthSize(int widthSize)649 private int getPageWidthSize(int widthSize) { 650 // It's necessary to add the padding back because it is remove when measuring children, 651 // like when MeasureSpec.getSize in CellLayout. 652 return (widthSize - mInsets.left - mInsets.right - getPaddingLeft() - getPaddingRight()) 653 / getPanelCount() + getPaddingLeft() + getPaddingRight(); 654 } 655 656 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)657 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 658 if (getChildCount() == 0) { 659 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 660 return; 661 } 662 663 // We measure the dimensions of the PagedView to be larger than the pages so that when we 664 // zoom out (and scale down), the view is still contained in the parent 665 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 666 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 667 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 668 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 669 670 if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { 671 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 672 return; 673 } 674 675 // Return early if we aren't given a proper dimension 676 if (widthSize <= 0 || heightSize <= 0) { 677 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 678 return; 679 } 680 681 // The children are given the same width and height as the workspace 682 // unless they were set to WRAP_CONTENT 683 if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); 684 685 int myWidthSpec = MeasureSpec.makeMeasureSpec( 686 getPageWidthSize(widthSize), MeasureSpec.EXACTLY); 687 int myHeightSpec = MeasureSpec.makeMeasureSpec( 688 heightSize - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY); 689 690 // measureChildren takes accounts for content padding, we only need to care about extra 691 // space due to insets. 692 measureChildren(myWidthSpec, myHeightSpec); 693 setMeasuredDimension(widthSize, heightSize); 694 } 695 696 /** Returns true iff this PagedView's scroll amounts are initialized to each page index. */ isPageScrollsInitialized()697 protected boolean isPageScrollsInitialized() { 698 return mPageScrolls != null && mPageScrolls.length == getChildCount(); 699 } 700 701 /** 702 * Queues the given callback to be run once {@code mPageScrolls} has been initialized. 703 */ runOnPageScrollsInitialized(Runnable callback)704 public void runOnPageScrollsInitialized(Runnable callback) { 705 mOnPageScrollsInitializedCallbacks.add(callback); 706 if (isPageScrollsInitialized()) { 707 onPageScrollsInitialized(); 708 } 709 } 710 onPageScrollsInitialized()711 protected void onPageScrollsInitialized() { 712 for (Runnable callback : mOnPageScrollsInitializedCallbacks) { 713 callback.run(); 714 } 715 mOnPageScrollsInitializedCallbacks.clear(); 716 } 717 718 @SuppressLint("DrawAllocation") 719 @Override onLayout(boolean changed, int left, int top, int right, int bottom)720 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 721 mIsLayoutValid = true; 722 final int childCount = getChildCount(); 723 int[] pageScrolls = mPageScrolls; 724 boolean pageScrollChanged = false; 725 if (!isPageScrollsInitialized()) { 726 pageScrolls = new int[childCount]; 727 pageScrollChanged = true; 728 } 729 730 if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); 731 732 pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC); 733 mPageScrolls = pageScrolls; 734 735 if (childCount == 0) { 736 onPageScrollsInitialized(); 737 return; 738 } 739 740 final LayoutTransition transition = getLayoutTransition(); 741 // If the transition is running defer updating max scroll, as some empty pages could 742 // still be present, and a max scroll change could cause sudden jumps in scroll. 743 if (transition != null && transition.isRunning()) { 744 transition.addTransitionListener(new LayoutTransition.TransitionListener() { 745 746 @Override 747 public void startTransition(LayoutTransition transition, ViewGroup container, 748 View view, int transitionType) { } 749 750 @Override 751 public void endTransition(LayoutTransition transition, ViewGroup container, 752 View view, int transitionType) { 753 // Wait until all transitions are complete. 754 if (!transition.isRunning()) { 755 transition.removeTransitionListener(this); 756 updateMinAndMaxScrollX(); 757 } 758 } 759 }); 760 } else { 761 updateMinAndMaxScrollX(); 762 } 763 764 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { 765 updateCurrentPageScroll(); 766 mFirstLayout = false; 767 } 768 769 if (mScroller.isFinished() && pageScrollChanged) { 770 // TODO(b/246283207): Remove logging once root cause of flake detected. 771 if (Utilities.isRunningInTestHarness() && !(this instanceof Workspace)) { 772 Log.d("b/246283207", this.getClass().getSimpleName() + "#onLayout() -> " 773 + "if(mScroller.isFinished() && pageScrollChanged) -> getNextPage(): " 774 + getNextPage() + ", getScrollForPage(getNextPage()): " 775 + getScrollForPage(getNextPage())); 776 } 777 setCurrentPage(getNextPage()); 778 } 779 onPageScrollsInitialized(); 780 } 781 782 /** 783 * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length 784 * of {@code outPageScrolls} should be same as the the childCount 785 */ getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)786 protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, 787 ComputePageScrollsLogic scrollLogic) { 788 final int childCount = getChildCount(); 789 790 final int startIndex = mIsRtl ? childCount - 1 : 0; 791 final int endIndex = mIsRtl ? -1 : childCount; 792 final int delta = mIsRtl ? -1 : 1; 793 794 final int pageCenter = mOrientationHandler.getCenterForPage(this, mInsets); 795 796 final int scrollOffsetStart = mOrientationHandler.getScrollOffsetStart(this, mInsets); 797 final int scrollOffsetEnd = mOrientationHandler.getScrollOffsetEnd(this, mInsets); 798 boolean pageScrollChanged = false; 799 int panelCount = getPanelCount(); 800 801 for (int i = startIndex, childStart = scrollOffsetStart; i != endIndex; i += delta) { 802 final View child = getPageAt(i); 803 if (scrollLogic.shouldIncludeView(child)) { 804 ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart, 805 pageCenter, layoutChildren); 806 final int primaryDimension = bounds.primaryDimension; 807 final int childPrimaryEnd = bounds.childPrimaryEnd; 808 809 // In case the pages are of different width, align the page to left edge for non-RTL 810 // or right edge for RTL. 811 final int pageScroll = 812 mIsRtl ? childPrimaryEnd - scrollOffsetEnd : childStart - scrollOffsetStart; 813 if (outPageScrolls[i] != pageScroll) { 814 pageScrollChanged = true; 815 outPageScrolls[i] = pageScroll; 816 } 817 childStart += primaryDimension + getChildGap(i, i + delta); 818 819 // This makes sure that the space is added after the page, not after each panel 820 int lastPanel = mIsRtl ? 0 : panelCount - 1; 821 if (i % panelCount == lastPanel) { 822 childStart += mPageSpacing; 823 } 824 } 825 } 826 827 if (panelCount > 1) { 828 for (int i = 0; i < childCount; i++) { 829 // In case we have multiple panels, always use left most panel's page scroll for all 830 // panels on the screen. 831 int adjustedScroll = outPageScrolls[getLeftmostVisiblePageForIndex(i)]; 832 if (outPageScrolls[i] != adjustedScroll) { 833 outPageScrolls[i] = adjustedScroll; 834 pageScrollChanged = true; 835 } 836 } 837 } 838 return pageScrollChanged; 839 } 840 getChildGap(int fromIndex, int toIndex)841 protected int getChildGap(int fromIndex, int toIndex) { 842 return 0; 843 } 844 updateMinAndMaxScrollX()845 protected void updateMinAndMaxScrollX() { 846 mMinScroll = computeMinScroll(); 847 mMaxScroll = computeMaxScroll(); 848 } 849 computeMinScroll()850 protected int computeMinScroll() { 851 return 0; 852 } 853 computeMaxScroll()854 protected int computeMaxScroll() { 855 int childCount = getChildCount(); 856 if (childCount > 0) { 857 final int index = mIsRtl ? 0 : childCount - 1; 858 return getScrollForPage(index); 859 } else { 860 return 0; 861 } 862 } 863 setPageSpacing(int pageSpacing)864 public void setPageSpacing(int pageSpacing) { 865 mPageSpacing = pageSpacing; 866 requestLayout(); 867 } 868 getPageSpacing()869 public int getPageSpacing() { 870 return mPageSpacing; 871 } 872 dispatchPageCountChanged()873 private void dispatchPageCountChanged() { 874 if (mPageIndicator != null) { 875 mPageIndicator.setMarkersCount(getChildCount() / getPanelCount()); 876 } 877 // This ensures that when children are added, they get the correct transforms / alphas 878 // in accordance with any scroll effects. 879 invalidate(); 880 } 881 882 @Override onViewAdded(View child)883 public void onViewAdded(View child) { 884 super.onViewAdded(child); 885 dispatchPageCountChanged(); 886 } 887 888 @Override onViewRemoved(View child)889 public void onViewRemoved(View child) { 890 super.onViewRemoved(child); 891 runOnPageScrollsInitialized(() -> { 892 mCurrentPage = validateNewPage(mCurrentPage); 893 mCurrentScrollOverPage = mCurrentPage; 894 }); 895 dispatchPageCountChanged(); 896 } 897 getChildOffset(int index)898 protected int getChildOffset(int index) { 899 if (index < 0 || index > getChildCount() - 1) return 0; 900 View pageAtIndex = getPageAt(index); 901 return mOrientationHandler.getChildStart(pageAtIndex); 902 } 903 getChildVisibleSize(int index)904 protected int getChildVisibleSize(int index) { 905 View layout = getPageAt(index); 906 return mOrientationHandler.getMeasuredSize(layout); 907 } 908 909 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)910 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 911 int page = indexOfChild(child); 912 if (!isVisible(page) || !mScroller.isFinished()) { 913 if (immediate) { 914 setCurrentPage(page); 915 } else { 916 snapToPage(page); 917 } 918 return true; 919 } 920 return false; 921 } 922 923 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)924 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 925 int focusablePage; 926 if (mNextPage != INVALID_PAGE) { 927 focusablePage = mNextPage; 928 } else { 929 focusablePage = mCurrentPage; 930 } 931 View v = getPageAt(focusablePage); 932 if (v != null) { 933 return v.requestFocus(direction, previouslyFocusedRect); 934 } 935 return false; 936 } 937 938 @Override dispatchUnhandledMove(View focused, int direction)939 public boolean dispatchUnhandledMove(View focused, int direction) { 940 if (super.dispatchUnhandledMove(focused, direction)) { 941 return true; 942 } 943 944 if (mIsRtl) { 945 if (direction == View.FOCUS_LEFT) { 946 direction = View.FOCUS_RIGHT; 947 } else if (direction == View.FOCUS_RIGHT) { 948 direction = View.FOCUS_LEFT; 949 } 950 } 951 952 int currentPage = getNextPage(); 953 int closestNeighbourIndex = -1; 954 int closestNeighbourDistance = Integer.MAX_VALUE; 955 // Find the closest neighbour page 956 for (int neighbourPageIndex : getNeighbourPageIndices(direction)) { 957 int distance = Math.abs(neighbourPageIndex - currentPage); 958 if (closestNeighbourDistance > distance) { 959 closestNeighbourDistance = distance; 960 closestNeighbourIndex = neighbourPageIndex; 961 } 962 } 963 if (closestNeighbourIndex != -1) { 964 View page = getPageAt(closestNeighbourIndex); 965 snapToPage(closestNeighbourIndex); 966 page.requestFocus(direction); 967 return true; 968 } 969 970 return false; 971 } 972 973 @Override addFocusables(ArrayList<View> views, int direction, int focusableMode)974 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 975 if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { 976 return; 977 } 978 979 // nextPage is more reliable when multiple control movements have been done in a short 980 // period of time 981 getPageIndices(getNextPage()) 982 .addAll(getNeighbourPageIndices(direction)) 983 .forEach(pageIndex -> 984 getPageAt(pageIndex).addFocusables(views, direction, focusableMode)); 985 } 986 987 /** 988 * If one of our descendant views decides that it could be focused now, only 989 * pass that along if it's on the current page. 990 * 991 * This happens when live folders requery, and if they're off page, they 992 * end up calling requestFocus, which pulls it on page. 993 */ 994 @Override focusableViewAvailable(View focused)995 public void focusableViewAvailable(View focused) { 996 View current = getPageAt(mCurrentPage); 997 View v = focused; 998 while (true) { 999 if (v == current) { 1000 super.focusableViewAvailable(focused); 1001 return; 1002 } 1003 if (v == this) { 1004 return; 1005 } 1006 ViewParent parent = v.getParent(); 1007 if (parent instanceof View) { 1008 v = (View)v.getParent(); 1009 } else { 1010 return; 1011 } 1012 } 1013 } 1014 1015 /** 1016 * {@inheritDoc} 1017 */ 1018 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)1019 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1020 if (disallowIntercept) { 1021 // We need to make sure to cancel our long press if 1022 // a scrollable widget takes over touch events 1023 cancelCurrentPageLongPress(); 1024 } 1025 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1026 } 1027 1028 @Override onInterceptTouchEvent(MotionEvent ev)1029 public boolean onInterceptTouchEvent(MotionEvent ev) { 1030 /* 1031 * This method JUST determines whether we want to intercept the motion. 1032 * If we return true, onTouchEvent will be called and we do the actual 1033 * scrolling there. 1034 */ 1035 1036 // Skip touch handling if there are no pages to swipe 1037 if (getChildCount() <= 0) return false; 1038 1039 acquireVelocityTrackerAndAddMovement(ev); 1040 1041 /* 1042 * Shortcut the most recurring case: the user is in the dragging 1043 * state and he is moving his finger. We want to intercept this 1044 * motion. 1045 */ 1046 final int action = ev.getAction(); 1047 if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) { 1048 return true; 1049 } 1050 1051 switch (action & MotionEvent.ACTION_MASK) { 1052 case MotionEvent.ACTION_MOVE: { 1053 /* 1054 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1055 * whether the user has moved far enough from their original down touch. 1056 */ 1057 if (mActivePointerId != INVALID_POINTER) { 1058 determineScrollingStart(ev); 1059 } 1060 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 1061 // event. in that case, treat the first occurrence of a move event as a ACTION_DOWN 1062 // i.e. fall through to the next case (don't break) 1063 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 1064 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 1065 break; 1066 } 1067 1068 case MotionEvent.ACTION_DOWN: { 1069 final float x = ev.getX(); 1070 final float y = ev.getY(); 1071 // Remember location of down touch 1072 mDownMotionX = x; 1073 mDownMotionY = y; 1074 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0); 1075 mLastMotion = (int) mDownMotionPrimary; 1076 mTotalMotion = 0; 1077 mAllowEasyFling = false; 1078 mActivePointerId = ev.getPointerId(0); 1079 updateIsBeingDraggedOnTouchDown(ev); 1080 break; 1081 } 1082 1083 case MotionEvent.ACTION_UP: 1084 case MotionEvent.ACTION_CANCEL: 1085 resetTouchState(); 1086 break; 1087 1088 case MotionEvent.ACTION_POINTER_UP: 1089 onSecondaryPointerUp(ev); 1090 releaseVelocityTracker(); 1091 break; 1092 } 1093 1094 /* 1095 * The only time we want to intercept motion events is if we are in the 1096 * drag mode. 1097 */ 1098 return mIsBeingDragged; 1099 } 1100 1101 /** 1102 * If being flinged and user touches the screen, initiate drag; otherwise don't. 1103 */ updateIsBeingDraggedOnTouchDown(MotionEvent ev)1104 protected void updateIsBeingDraggedOnTouchDown(MotionEvent ev) { 1105 // mScroller.isFinished should be false when being flinged. 1106 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); 1107 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3); 1108 1109 if (finishedScrolling) { 1110 mIsBeingDragged = false; 1111 if (!mScroller.isFinished() && !mFreeScroll) { 1112 setCurrentPage(getNextPage()); 1113 pageEndTransition(); 1114 } 1115 mIsBeingDragged = !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished(); 1116 } else { 1117 mIsBeingDragged = true; 1118 } 1119 1120 // Catch the edge effect if it is active. 1121 float displacement = mOrientationHandler.getSecondaryValue(ev.getX(), ev.getY()) 1122 / mOrientationHandler.getSecondaryValue(getWidth(), getHeight()); 1123 if (!mEdgeGlowLeft.isFinished()) { 1124 mEdgeGlowLeft.onPullDistance(0f, 1f - displacement); 1125 } 1126 if (!mEdgeGlowRight.isFinished()) { 1127 mEdgeGlowRight.onPullDistance(0f, displacement); 1128 } 1129 } 1130 isHandlingTouch()1131 public boolean isHandlingTouch() { 1132 return mIsBeingDragged; 1133 } 1134 determineScrollingStart(MotionEvent ev)1135 protected void determineScrollingStart(MotionEvent ev) { 1136 determineScrollingStart(ev, 1.0f); 1137 } 1138 1139 /* 1140 * Determines if we should change the touch state to start scrolling after the 1141 * user moves their touch point too far. 1142 */ determineScrollingStart(MotionEvent ev, float touchSlopScale)1143 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 1144 // Disallow scrolling if we don't have a valid pointer index 1145 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1146 if (pointerIndex == -1) return; 1147 1148 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex); 1149 final int diff = (int) Math.abs(primaryDirection - mLastMotion); 1150 final int touchSlop = Math.round(touchSlopScale * mTouchSlop); 1151 boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING; 1152 1153 if (moved) { 1154 // Scroll if the user moved far enough along the X axis 1155 mIsBeingDragged = true; 1156 mTotalMotion += Math.abs(mLastMotion - primaryDirection); 1157 mLastMotion = (int) primaryDirection; 1158 pageBeginTransition(); 1159 // Stop listening for things like pinches. 1160 requestDisallowInterceptTouchEvent(true); 1161 } 1162 } 1163 cancelCurrentPageLongPress()1164 protected void cancelCurrentPageLongPress() { 1165 // Try canceling the long press. It could also have been scheduled 1166 // by a distant descendant, so use the mAllowLongPress flag to block 1167 // everything 1168 forEachVisiblePage(View::cancelLongPress); 1169 } 1170 getScrollProgress(int screenCenter, View v, int page)1171 protected float getScrollProgress(int screenCenter, View v, int page) { 1172 final int halfScreenSize = getMeasuredWidth() / 2; 1173 int delta = screenCenter - (getScrollForPage(page) + halfScreenSize); 1174 int panelCount = getPanelCount(); 1175 int pageCount = getChildCount(); 1176 1177 int adjacentPage = page + panelCount; 1178 if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { 1179 adjacentPage = page - panelCount; 1180 } 1181 1182 final int totalDistance; 1183 if (adjacentPage < 0 || adjacentPage > pageCount - 1) { 1184 totalDistance = (v.getMeasuredWidth() + mPageSpacing) * panelCount; 1185 } else { 1186 totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); 1187 } 1188 1189 float scrollProgress = delta / (totalDistance * 1.0f); 1190 scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); 1191 scrollProgress = Math.max(scrollProgress, -MAX_SCROLL_PROGRESS); 1192 return scrollProgress; 1193 } 1194 getScrollForPage(int index)1195 public int getScrollForPage(int index) { 1196 if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) { 1197 return 0; 1198 } else { 1199 return mPageScrolls[index]; 1200 } 1201 } 1202 1203 // While layout transitions are occurring, a child's position may stray from its baseline 1204 // position. This method returns the magnitude of this stray at any given time. getLayoutTransitionOffsetForPage(int index)1205 public int getLayoutTransitionOffsetForPage(int index) { 1206 if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) { 1207 return 0; 1208 } else { 1209 View child = getChildAt(index); 1210 1211 int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); 1212 int baselineX = mPageScrolls[index] + scrollOffset; 1213 return (int) (child.getX() - baselineX); 1214 } 1215 } 1216 setEnableFreeScroll(boolean freeScroll)1217 public void setEnableFreeScroll(boolean freeScroll) { 1218 if (mFreeScroll == freeScroll) { 1219 return; 1220 } 1221 1222 boolean wasFreeScroll = mFreeScroll; 1223 mFreeScroll = freeScroll; 1224 1225 if (mFreeScroll) { 1226 setCurrentPage(getNextPage()); 1227 } else if (wasFreeScroll) { 1228 if (getScrollForPage(getNextPage()) != getScrollX()) { 1229 snapToPage(getNextPage()); 1230 } 1231 } 1232 } 1233 setEnableOverscroll(boolean enable)1234 protected void setEnableOverscroll(boolean enable) { 1235 mAllowOverScroll = enable; 1236 } 1237 isSignificantMove(float absoluteDelta, int pageOrientedSize)1238 protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) { 1239 return absoluteDelta > pageOrientedSize * SIGNIFICANT_MOVE_THRESHOLD; 1240 } 1241 1242 @Override onTouchEvent(MotionEvent ev)1243 public boolean onTouchEvent(MotionEvent ev) { 1244 // Skip touch handling if there are no pages to swipe 1245 if (getChildCount() <= 0) return false; 1246 1247 acquireVelocityTrackerAndAddMovement(ev); 1248 1249 final int action = ev.getAction(); 1250 1251 switch (action & MotionEvent.ACTION_MASK) { 1252 case MotionEvent.ACTION_DOWN: 1253 updateIsBeingDraggedOnTouchDown(ev); 1254 1255 /* 1256 * If being flinged and user touches, stop the fling. isFinished 1257 * will be false if being flinged. 1258 */ 1259 if (!mScroller.isFinished()) { 1260 abortScrollerAnimation(false); 1261 } 1262 1263 // Remember where the motion event started 1264 mDownMotionX = ev.getX(); 1265 mDownMotionY = ev.getY(); 1266 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0); 1267 mLastMotion = (int) mDownMotionPrimary; 1268 mTotalMotion = 0; 1269 mAllowEasyFling = false; 1270 mActivePointerId = ev.getPointerId(0); 1271 if (mIsBeingDragged) { 1272 pageBeginTransition(); 1273 } 1274 break; 1275 1276 case ACTION_MOVE_ALLOW_EASY_FLING: 1277 // Start scrolling immediately 1278 determineScrollingStart(ev); 1279 mAllowEasyFling = true; 1280 break; 1281 1282 case MotionEvent.ACTION_MOVE: 1283 if (mIsBeingDragged) { 1284 // Scroll to follow the motion event 1285 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1286 1287 if (pointerIndex == -1) return true; 1288 int oldScroll = mOrientationHandler.getPrimaryScroll(this); 1289 int dx = (int) ev.getX(pointerIndex); 1290 int dy = (int) ev.getY(pointerIndex); 1291 1292 int direction = mOrientationHandler.getPrimaryValue(dx, dy); 1293 int delta = mLastMotion - direction; 1294 1295 int width = getWidth(); 1296 int height = getHeight(); 1297 float size = mOrientationHandler.getPrimaryValue(width, height); 1298 float displacement = (width == 0 || height == 0) ? 0 1299 : (float) mOrientationHandler.getSecondaryValue(dx, dy) 1300 / mOrientationHandler.getSecondaryValue(width, height); 1301 mTotalMotion += Math.abs(delta); 1302 1303 if (mAllowOverScroll) { 1304 int consumed = 0; 1305 if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) { 1306 consumed = Math.round(size * 1307 mEdgeGlowRight.onPullDistance(delta / size, displacement)); 1308 } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) { 1309 consumed = Math.round(-size * 1310 mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement)); 1311 } 1312 delta -= consumed; 1313 } 1314 delta /= mOrientationHandler.getPrimaryScale(this); 1315 1316 // Only scroll and update mLastMotionX if we have moved some discrete amount. We 1317 // keep the remainder because we are actually testing if we've moved from the last 1318 // scrolled position (which is discrete). 1319 mLastMotion = direction; 1320 1321 if (delta != 0) { 1322 mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, delta); 1323 1324 if (mAllowOverScroll) { 1325 final float pulledToX = oldScroll + delta; 1326 1327 if (pulledToX < mMinScroll) { 1328 mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement); 1329 if (!mEdgeGlowRight.isFinished()) { 1330 mEdgeGlowRight.onRelease(); 1331 } 1332 } else if (pulledToX > mMaxScroll) { 1333 mEdgeGlowRight.onPullDistance(delta / size, displacement); 1334 if (!mEdgeGlowLeft.isFinished()) { 1335 mEdgeGlowLeft.onRelease(); 1336 } 1337 } 1338 1339 if (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()) { 1340 postInvalidateOnAnimation(); 1341 } 1342 } 1343 1344 } else { 1345 awakenScrollBars(); 1346 } 1347 } else { 1348 determineScrollingStart(ev); 1349 } 1350 break; 1351 1352 case MotionEvent.ACTION_UP: 1353 if (mIsBeingDragged) { 1354 final int activePointerId = mActivePointerId; 1355 final int pointerIndex = ev.findPointerIndex(activePointerId); 1356 if (pointerIndex == -1) return true; 1357 1358 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, 1359 pointerIndex); 1360 final VelocityTracker velocityTracker = mVelocityTracker; 1361 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1362 1363 int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker, 1364 mActivePointerId); 1365 float delta = primaryDirection - mDownMotionPrimary; 1366 1367 View current = getPageAt(mCurrentPage); 1368 if (current == null) { 1369 Log.e(TAG, "current page was null. this should not happen."); 1370 return true; 1371 } 1372 1373 int pageOrientedSize = (int) (mOrientationHandler.getMeasuredSize(current) 1374 * mOrientationHandler.getPrimaryScale(this)); 1375 boolean isSignificantMove = isSignificantMove(Math.abs(delta), pageOrientedSize); 1376 1377 mTotalMotion += Math.abs(mLastMotion - primaryDirection); 1378 boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop; 1379 boolean isFling = passedSlop && shouldFlingForVelocity(velocity); 1380 boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0; 1381 boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0; 1382 if (DEBUG_FAILED_QUICKSWITCH && !isFling && mAllowEasyFling) { 1383 Log.d("Quickswitch", "isFling=false vel=" + velocity 1384 + " threshold=" + mEasyFlingThresholdVelocity); 1385 } 1386 1387 if (!mFreeScroll) { 1388 // In the case that the page is moved far to one direction and then is flung 1389 // in the opposite direction, we use a threshold to determine whether we should 1390 // just return to the starting page, or if we should skip one further. 1391 boolean returnToOriginalPage = false; 1392 if (Math.abs(delta) > pageOrientedSize * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1393 Math.signum(velocity) != Math.signum(delta) && isFling) { 1394 returnToOriginalPage = true; 1395 } 1396 1397 int finalPage; 1398 // We give flings precedence over large moves, which is why we short-circuit our 1399 // test for a large move if a fling has been registered. That is, a large 1400 // move to the left and fling to the right will register as a fling to the right. 1401 1402 if (((isSignificantMove && !isDeltaLeft && !isFling) || 1403 (isFling && !isVelocityLeft)) && mCurrentPage > 0) { 1404 finalPage = returnToOriginalPage 1405 ? mCurrentPage : mCurrentPage - getPanelCount(); 1406 runOnPageScrollsInitialized( 1407 () -> snapToPageWithVelocity(finalPage, velocity)); 1408 } else if (((isSignificantMove && isDeltaLeft && !isFling) || 1409 (isFling && isVelocityLeft)) && 1410 mCurrentPage < getChildCount() - 1) { 1411 finalPage = returnToOriginalPage 1412 ? mCurrentPage : mCurrentPage + getPanelCount(); 1413 runOnPageScrollsInitialized( 1414 () -> snapToPageWithVelocity(finalPage, velocity)); 1415 } else { 1416 runOnPageScrollsInitialized(this::snapToDestination); 1417 } 1418 } else { 1419 if (!mScroller.isFinished()) { 1420 abortScrollerAnimation(true); 1421 } 1422 1423 int initialScroll = mOrientationHandler.getPrimaryScroll(this); 1424 int maxScroll = mMaxScroll; 1425 int minScroll = mMinScroll; 1426 1427 if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) || 1428 ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) { 1429 mScroller.springBack(initialScroll, 0, minScroll, maxScroll, 0, 0); 1430 mNextPage = getDestinationPage(); 1431 } else { 1432 int velocity1 = -velocity; 1433 // Continue a scroll or fling in progress 1434 mScroller.fling(initialScroll, 0, velocity1, 0, minScroll, maxScroll, 0, 0, 1435 Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR), 0); 1436 1437 int finalPos = mScroller.getFinalX(); 1438 mNextPage = getDestinationPage(finalPos); 1439 runOnPageScrollsInitialized(this::onNotSnappingToPageInFreeScroll); 1440 } 1441 invalidate(); 1442 } 1443 } 1444 1445 mEdgeGlowLeft.onRelease(); 1446 mEdgeGlowRight.onRelease(); 1447 // End any intermediate reordering states 1448 resetTouchState(); 1449 break; 1450 1451 case MotionEvent.ACTION_CANCEL: 1452 if (mIsBeingDragged) { 1453 runOnPageScrollsInitialized(this::snapToDestination); 1454 } 1455 mEdgeGlowLeft.onRelease(); 1456 mEdgeGlowRight.onRelease(); 1457 resetTouchState(); 1458 break; 1459 1460 case MotionEvent.ACTION_POINTER_UP: 1461 onSecondaryPointerUp(ev); 1462 releaseVelocityTracker(); 1463 break; 1464 } 1465 1466 return true; 1467 } 1468 onNotSnappingToPageInFreeScroll()1469 protected void onNotSnappingToPageInFreeScroll() { } 1470 1471 /** 1472 * Called when the view edges absorb part of the scroll. Subclasses can override this 1473 * to provide custom behavior during animation. 1474 */ onEdgeAbsorbingScroll()1475 protected void onEdgeAbsorbingScroll() { 1476 } 1477 1478 /** 1479 * Called when the current page closest to the center of the screen changes as part of the 1480 * scroll. Subclasses can override this to provide custom behavior during scroll. 1481 */ onScrollOverPageChanged()1482 protected void onScrollOverPageChanged() { 1483 } 1484 shouldFlingForVelocity(int velocity)1485 protected boolean shouldFlingForVelocity(int velocity) { 1486 float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity; 1487 return Math.abs(velocity) > threshold; 1488 } 1489 resetTouchState()1490 protected void resetTouchState() { 1491 releaseVelocityTracker(); 1492 mIsBeingDragged = false; 1493 mActivePointerId = INVALID_POINTER; 1494 } 1495 1496 @Override onGenericMotionEvent(MotionEvent event)1497 public boolean onGenericMotionEvent(MotionEvent event) { 1498 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 1499 switch (event.getAction()) { 1500 case MotionEvent.ACTION_SCROLL: { 1501 // Handle mouse (or ext. device) by shifting the page depending on the scroll 1502 final float vscroll; 1503 final float hscroll; 1504 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 1505 vscroll = 0; 1506 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1507 } else { 1508 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1509 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1510 } 1511 if (!canScroll(Math.abs(vscroll), Math.abs(hscroll))) { 1512 return false; 1513 } 1514 if (hscroll != 0 || vscroll != 0) { 1515 boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) 1516 : (hscroll > 0 || vscroll > 0); 1517 if (isForwardScroll) { 1518 scrollRight(); 1519 } else { 1520 scrollLeft(); 1521 } 1522 return true; 1523 } 1524 } 1525 } 1526 } 1527 return super.onGenericMotionEvent(event); 1528 } 1529 1530 /** 1531 * Returns true if the paged view can scroll for the provided vertical and horizontal 1532 * scroll values 1533 */ canScroll(float absVScroll, float absHScroll)1534 protected boolean canScroll(float absVScroll, float absHScroll) { 1535 ActivityContext ac = ActivityContext.lookupContext(getContext()); 1536 return (ac == null || AbstractFloatingView.getTopOpenView(ac) == null); 1537 } 1538 acquireVelocityTrackerAndAddMovement(MotionEvent ev)1539 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 1540 if (mVelocityTracker == null) { 1541 mVelocityTracker = VelocityTracker.obtain(); 1542 } 1543 mVelocityTracker.addMovement(ev); 1544 } 1545 releaseVelocityTracker()1546 private void releaseVelocityTracker() { 1547 if (mVelocityTracker != null) { 1548 mVelocityTracker.clear(); 1549 mVelocityTracker.recycle(); 1550 mVelocityTracker = null; 1551 } 1552 } 1553 onSecondaryPointerUp(MotionEvent ev)1554 private void onSecondaryPointerUp(MotionEvent ev) { 1555 final int pointerIndex = ev.getActionIndex(); 1556 final int pointerId = ev.getPointerId(pointerIndex); 1557 if (pointerId == mActivePointerId) { 1558 // This was our active pointer going up. Choose a new 1559 // active pointer and adjust accordingly. 1560 // TODO: Make this decision more intelligent. 1561 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1562 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, newPointerIndex); 1563 mLastMotion = (int) mDownMotionPrimary; 1564 mActivePointerId = ev.getPointerId(newPointerIndex); 1565 if (mVelocityTracker != null) { 1566 mVelocityTracker.clear(); 1567 } 1568 } 1569 } 1570 1571 @Override requestChildFocus(View child, View focused)1572 public void requestChildFocus(View child, View focused) { 1573 super.requestChildFocus(child, focused); 1574 1575 // In case the device is controlled by a controller, mCurrentPage isn't updated properly 1576 // which results in incorrect navigation 1577 int nextPage = getNextPage(); 1578 if (nextPage != mCurrentPage) { 1579 setCurrentPage(nextPage); 1580 } 1581 1582 int page = indexOfChild(child); 1583 if (page >= 0 && !isVisible(page) && !isInTouchMode()) { 1584 snapToPage(page); 1585 } 1586 } 1587 getDestinationPage()1588 public int getDestinationPage() { 1589 return getDestinationPage(mOrientationHandler.getPrimaryScroll(this)); 1590 } 1591 getDestinationPage(int primaryScroll)1592 protected int getDestinationPage(int primaryScroll) { 1593 return getPageNearestToCenterOfScreen(primaryScroll); 1594 } 1595 getPageNearestToCenterOfScreen()1596 public int getPageNearestToCenterOfScreen() { 1597 return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this)); 1598 } 1599 getPageNearestToCenterOfScreen(int primaryScroll)1600 private int getPageNearestToCenterOfScreen(int primaryScroll) { 1601 int screenCenter = getScreenCenter(primaryScroll); 1602 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 1603 int minDistanceFromScreenCenterIndex = -1; 1604 final int childCount = getChildCount(); 1605 for (int i = 0; i < childCount; ++i) { 1606 int distanceFromScreenCenter = Math.abs( 1607 getDisplacementFromScreenCenter(i, screenCenter)); 1608 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 1609 minDistanceFromScreenCenter = distanceFromScreenCenter; 1610 minDistanceFromScreenCenterIndex = i; 1611 } 1612 } 1613 return minDistanceFromScreenCenterIndex; 1614 } 1615 getDisplacementFromScreenCenter(int childIndex, int screenCenter)1616 private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) { 1617 int childSize = getChildVisibleSize(childIndex); 1618 int halfChildSize = (childSize / 2); 1619 int childCenter = getChildOffset(childIndex) + halfChildSize; 1620 return childCenter - screenCenter; 1621 } 1622 getDisplacementFromScreenCenter(int childIndex)1623 protected int getDisplacementFromScreenCenter(int childIndex) { 1624 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 1625 int screenCenter = getScreenCenter(primaryScroll); 1626 return getDisplacementFromScreenCenter(childIndex, screenCenter); 1627 } 1628 getScreenCenter(int primaryScroll)1629 protected int getScreenCenter(int primaryScroll) { 1630 float primaryScale = mOrientationHandler.getPrimaryScale(this); 1631 float primaryPivot = mOrientationHandler.getPrimaryValue(getPivotX(), getPivotY()); 1632 int pageOrientationSize = mOrientationHandler.getMeasuredSize(this); 1633 return Math.round(primaryScroll + (pageOrientationSize / 2f - primaryPivot) / primaryScale 1634 + primaryPivot); 1635 } 1636 snapToDestination()1637 protected void snapToDestination() { 1638 snapToPage(getDestinationPage(), mPageSnapAnimationDuration); 1639 } 1640 1641 // We want the duration of the page snap animation to be influenced by the distance that 1642 // the screen has to travel, however, we don't want this duration to be effected in a 1643 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 1644 // of travel has on the overall snap duration. distanceInfluenceForSnapDuration(float f)1645 private float distanceInfluenceForSnapDuration(float f) { 1646 f -= 0.5f; // center the values about 0. 1647 f *= 0.3f * Math.PI / 2.0f; 1648 return (float) Math.sin(f); 1649 } 1650 snapToPageWithVelocity(int whichPage, int velocity)1651 protected boolean snapToPageWithVelocity(int whichPage, int velocity) { 1652 whichPage = validateNewPage(whichPage); 1653 int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2; 1654 1655 final int newLoc = getScrollForPage(whichPage); 1656 int delta = newLoc - mOrientationHandler.getPrimaryScroll(this); 1657 int duration = 0; 1658 1659 if (Math.abs(velocity) < mMinFlingVelocity) { 1660 // If the velocity is low enough, then treat this more as an automatic page advance 1661 // as opposed to an apparent physical response to flinging 1662 return snapToPage(whichPage, mPageSnapAnimationDuration); 1663 } 1664 1665 // Here we compute a "distance" that will be used in the computation of the overall 1666 // snap duration. This is a function of the actual distance that needs to be traveled; 1667 // we keep this value close to half screen size in order to reduce the variance in snap 1668 // duration as a function of the distance the page needs to travel. 1669 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 1670 float distance = halfScreenSize + halfScreenSize * 1671 distanceInfluenceForSnapDuration(distanceRatio); 1672 1673 velocity = Math.abs(velocity); 1674 velocity = Math.max(mMinSnapVelocity, velocity); 1675 1676 // we want the page's snap velocity to approximately match the velocity at which the 1677 // user flings, so we scale the duration by a value near to the derivative of the scroll 1678 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 1679 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 1680 1681 return snapToPage(whichPage, delta, duration); 1682 } 1683 snapToPage(int whichPage)1684 public boolean snapToPage(int whichPage) { 1685 return snapToPage(whichPage, mPageSnapAnimationDuration); 1686 } 1687 snapToPageImmediately(int whichPage)1688 public boolean snapToPageImmediately(int whichPage) { 1689 return snapToPage(whichPage, mPageSnapAnimationDuration, true); 1690 } 1691 snapToPage(int whichPage, int duration)1692 public boolean snapToPage(int whichPage, int duration) { 1693 return snapToPage(whichPage, duration, false); 1694 } 1695 snapToPage(int whichPage, int duration, boolean immediate)1696 protected boolean snapToPage(int whichPage, int duration, boolean immediate) { 1697 whichPage = validateNewPage(whichPage); 1698 1699 int newLoc = getScrollForPage(whichPage); 1700 final int delta = newLoc - mOrientationHandler.getPrimaryScroll(this); 1701 return snapToPage(whichPage, delta, duration, immediate); 1702 } 1703 snapToPage(int whichPage, int delta, int duration)1704 protected boolean snapToPage(int whichPage, int delta, int duration) { 1705 return snapToPage(whichPage, delta, duration, false); 1706 } 1707 snapToPage(int whichPage, int delta, int duration, boolean immediate)1708 protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate) { 1709 if (mFirstLayout) { 1710 setCurrentPage(whichPage); 1711 return false; 1712 } 1713 1714 if (FeatureFlags.IS_STUDIO_BUILD && !Utilities.isRunningInTestHarness()) { 1715 duration *= Settings.Global.getFloat(getContext().getContentResolver(), 1716 Settings.Global.WINDOW_ANIMATION_SCALE, 1); 1717 } 1718 1719 whichPage = validateNewPage(whichPage); 1720 1721 mNextPage = whichPage; 1722 1723 awakenScrollBars(duration); 1724 if (immediate) { 1725 duration = 0; 1726 } else if (duration == 0) { 1727 duration = Math.abs(delta); 1728 } 1729 1730 if (duration != 0) { 1731 pageBeginTransition(); 1732 } 1733 1734 if (!mScroller.isFinished()) { 1735 abortScrollerAnimation(false); 1736 } 1737 1738 mScroller.startScroll(mOrientationHandler.getPrimaryScroll(this), 0, delta, 0, duration); 1739 updatePageIndicator(); 1740 1741 // Trigger a compute() to finish switching pages if necessary 1742 if (immediate) { 1743 computeScroll(); 1744 pageEndTransition(); 1745 } 1746 1747 invalidate(); 1748 return Math.abs(delta) > 0; 1749 } 1750 scrollLeft()1751 public boolean scrollLeft() { 1752 if (getNextPage() > 0) { 1753 snapToPage(getNextPage() - getPanelCount()); 1754 return true; 1755 } 1756 return mAllowOverScroll; 1757 } 1758 scrollRight()1759 public boolean scrollRight() { 1760 if (getNextPage() < getChildCount() - 1) { 1761 snapToPage(getNextPage() + getPanelCount()); 1762 return true; 1763 } 1764 return mAllowOverScroll; 1765 } 1766 1767 @Override onScrollChanged(int l, int t, int oldl, int oldt)1768 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 1769 if (mScroller.isFinished()) { 1770 // This was not caused by the scroller, skip it. 1771 return; 1772 } 1773 int newDestinationPage = getDestinationPage(); 1774 if (newDestinationPage >= 0 && newDestinationPage != mCurrentScrollOverPage) { 1775 mCurrentScrollOverPage = newDestinationPage; 1776 onScrollOverPageChanged(); 1777 } 1778 } 1779 1780 @Override getAccessibilityClassName()1781 public CharSequence getAccessibilityClassName() { 1782 // Some accessibility services have special logic for ScrollView. Since we provide same 1783 // accessibility info as ScrollView, inform the service to handle use the same way. 1784 return ScrollView.class.getName(); 1785 } 1786 isPageOrderFlipped()1787 protected boolean isPageOrderFlipped() { 1788 return false; 1789 } 1790 1791 /* Accessibility */ 1792 @SuppressWarnings("deprecation") 1793 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1794 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1795 super.onInitializeAccessibilityNodeInfo(info); 1796 final boolean pagesFlipped = isPageOrderFlipped(); 1797 info.setScrollable(getPageCount() > 0); 1798 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 1799 if (getCurrentPage() < getPageCount() - getPanelCount() 1800 || (getCurrentPage() == getPageCount() - getPanelCount() 1801 && primaryScroll != getScrollForPage(getPageCount() - getPanelCount()))) { 1802 info.addAction(pagesFlipped ? 1803 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD 1804 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1805 info.addAction(mIsRtl ? 1806 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT 1807 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT); 1808 } 1809 if (getCurrentPage() > 0 1810 || (getCurrentPage() == 0 && primaryScroll != getScrollForPage(0))) { 1811 info.addAction(pagesFlipped ? 1812 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD 1813 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1814 info.addAction(mIsRtl ? 1815 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT 1816 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT); 1817 } 1818 // Accessibility-wise, PagedView doesn't support long click, so disabling it. 1819 // Besides disabling the accessibility long-click, this also prevents this view from getting 1820 // accessibility focus. 1821 info.setLongClickable(false); 1822 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 1823 } 1824 1825 @Override sendAccessibilityEvent(int eventType)1826 public void sendAccessibilityEvent(int eventType) { 1827 // Don't let the view send real scroll events. 1828 if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { 1829 super.sendAccessibilityEvent(eventType); 1830 } 1831 } 1832 1833 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1834 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1835 super.onInitializeAccessibilityEvent(event); 1836 event.setScrollable(mAllowOverScroll || getPageCount() > 1); 1837 } 1838 1839 @Override performAccessibilityAction(int action, Bundle arguments)1840 public boolean performAccessibilityAction(int action, Bundle arguments) { 1841 if (super.performAccessibilityAction(action, arguments)) { 1842 return true; 1843 } 1844 final boolean pagesFlipped = isPageOrderFlipped(); 1845 switch (action) { 1846 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1847 if (pagesFlipped ? scrollLeft() : scrollRight()) { 1848 return true; 1849 } 1850 } break; 1851 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1852 if (pagesFlipped ? scrollRight() : scrollLeft()) { 1853 return true; 1854 } 1855 } break; 1856 case android.R.id.accessibilityActionPageRight: { 1857 if (!mIsRtl) { 1858 return scrollRight(); 1859 } else { 1860 return scrollLeft(); 1861 } 1862 } 1863 case android.R.id.accessibilityActionPageLeft: { 1864 if (!mIsRtl) { 1865 return scrollLeft(); 1866 } else { 1867 return scrollRight(); 1868 } 1869 } 1870 } 1871 return false; 1872 } 1873 canAnnouncePageDescription()1874 protected boolean canAnnouncePageDescription() { 1875 return true; 1876 } 1877 getCurrentPageDescription()1878 protected String getCurrentPageDescription() { 1879 return getContext().getString(R.string.default_scroll_format, 1880 getNextPage() + 1, getChildCount()); 1881 } 1882 getDownMotionX()1883 protected float getDownMotionX() { 1884 return mDownMotionX; 1885 } 1886 getDownMotionY()1887 protected float getDownMotionY() { 1888 return mDownMotionY; 1889 } 1890 1891 protected interface ComputePageScrollsLogic { 1892 shouldIncludeView(View view)1893 boolean shouldIncludeView(View view); 1894 } 1895 getVisibleChildrenRange()1896 public int[] getVisibleChildrenRange() { 1897 float visibleLeft = 0; 1898 float visibleRight = visibleLeft + getMeasuredWidth(); 1899 float scaleX = getScaleX(); 1900 if (scaleX < 1 && scaleX > 0) { 1901 float mid = getMeasuredWidth() / 2; 1902 visibleLeft = mid - ((mid - visibleLeft) / scaleX); 1903 visibleRight = mid + ((visibleRight - mid) / scaleX); 1904 } 1905 1906 int leftChild = -1; 1907 int rightChild = -1; 1908 final int childCount = getChildCount(); 1909 for (int i = 0; i < childCount; i++) { 1910 final View child = getPageAt(i); 1911 1912 float left = child.getLeft() + child.getTranslationX() - getScrollX(); 1913 if (left <= visibleRight && (left + child.getMeasuredWidth()) >= visibleLeft) { 1914 if (leftChild == -1) { 1915 leftChild = i; 1916 } 1917 rightChild = i; 1918 } 1919 } 1920 mTmpIntPair[0] = leftChild; 1921 mTmpIntPair[1] = rightChild; 1922 return mTmpIntPair; 1923 } 1924 1925 @Override draw(Canvas canvas)1926 public void draw(Canvas canvas) { 1927 super.draw(canvas); 1928 drawEdgeEffect(canvas); 1929 pageEndTransition(); 1930 } 1931 drawEdgeEffect(Canvas canvas)1932 protected void drawEdgeEffect(Canvas canvas) { 1933 if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) { 1934 final int width = getWidth(); 1935 final int height = getHeight(); 1936 if (!mEdgeGlowLeft.isFinished()) { 1937 final int restoreCount = canvas.save(); 1938 canvas.rotate(-90); 1939 canvas.translate(-height, Math.min(mMinScroll, getScrollX())); 1940 mEdgeGlowLeft.setSize(height, width); 1941 if (mEdgeGlowLeft.draw(canvas)) { 1942 postInvalidateOnAnimation(); 1943 } 1944 canvas.restoreToCount(restoreCount); 1945 } 1946 if (!mEdgeGlowRight.isFinished()) { 1947 final int restoreCount = canvas.save(); 1948 canvas.rotate(90, width, 0); 1949 canvas.translate(width, -(Math.max(mMaxScroll, getScrollX()))); 1950 1951 mEdgeGlowRight.setSize(height, width); 1952 if (mEdgeGlowRight.draw(canvas)) { 1953 postInvalidateOnAnimation(); 1954 } 1955 canvas.restoreToCount(restoreCount); 1956 } 1957 } 1958 } 1959 } 1960