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