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