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