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