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