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