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