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.compat.AccessibilityManagerCompat.isAccessibilityEnabled; 21 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType; 22 import static com.android.launcher3.testing.shared.TestProtocol.SCROLL_FINISHED_MESSAGE; 23 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR; 24 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY; 25 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO; 26 27 import android.animation.LayoutTransition; 28 import android.annotation.SuppressLint; 29 import android.content.Context; 30 import android.content.res.Configuration; 31 import android.content.res.Resources; 32 import android.content.res.TypedArray; 33 import android.graphics.Canvas; 34 import android.graphics.Rect; 35 import android.os.Bundle; 36 import android.provider.Settings; 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 * Queues the given callback to be run once {@code mPageScrolls} has been initialized. 722 */ runOnPageScrollsInitialized(Runnable callback)723 public void runOnPageScrollsInitialized(Runnable callback) { 724 mOnPageScrollsInitializedCallbacks.add(callback); 725 if (isPageScrollsInitialized()) { 726 onPageScrollsInitialized(); 727 } 728 } 729 onPageScrollsInitialized()730 protected void onPageScrollsInitialized() { 731 for (Runnable callback : mOnPageScrollsInitializedCallbacks) { 732 callback.run(); 733 } 734 mOnPageScrollsInitializedCallbacks.clear(); 735 } 736 737 @SuppressLint("DrawAllocation") 738 @Override onLayout(boolean changed, int left, int top, int right, int bottom)739 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 740 mIsLayoutValid = true; 741 final int childCount = getChildCount(); 742 int[] pageScrolls = mPageScrolls; 743 boolean pageScrollChanged = false; 744 if (!isPageScrollsInitialized()) { 745 pageScrolls = new int[childCount]; 746 pageScrollChanged = true; 747 } 748 749 if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); 750 751 pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC); 752 mPageScrolls = pageScrolls; 753 754 if (childCount == 0) { 755 onPageScrollsInitialized(); 756 return; 757 } 758 759 final LayoutTransition transition = getLayoutTransition(); 760 // If the transition is running defer updating max scroll, as some empty pages could 761 // still be present, and a max scroll change could cause sudden jumps in scroll. 762 if (transition != null && transition.isRunning()) { 763 transition.addTransitionListener(new LayoutTransition.TransitionListener() { 764 765 @Override 766 public void startTransition(LayoutTransition transition, ViewGroup container, 767 View view, int transitionType) { } 768 769 @Override 770 public void endTransition(LayoutTransition transition, ViewGroup container, 771 View view, int transitionType) { 772 // Wait until all transitions are complete. 773 if (!transition.isRunning()) { 774 transition.removeTransitionListener(this); 775 updateMinAndMaxScrollX(); 776 } 777 } 778 }); 779 } else { 780 updateMinAndMaxScrollX(); 781 } 782 783 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { 784 updateCurrentPageScroll(); 785 mFirstLayout = false; 786 } 787 788 if (mScroller.isFinished() && pageScrollChanged) { 789 // TODO(b/246283207): Remove logging once root cause of flake detected. 790 if (Utilities.isRunningInTestHarness() && !(this instanceof Workspace)) { 791 Log.d("b/246283207", TAG + "#onLayout() -> " 792 + "if(mScroller.isFinished() && pageScrollChanged) -> getNextPage(): " 793 + getNextPage() + ", getScrollForPage(getNextPage()): " 794 + getScrollForPage(getNextPage())); 795 } 796 setCurrentPage(getNextPage()); 797 } 798 onPageScrollsInitialized(); 799 } 800 801 /** 802 * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length 803 * of {@code outPageScrolls} should be same as the the childCount 804 */ getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)805 protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, 806 ComputePageScrollsLogic scrollLogic) { 807 final int childCount = getChildCount(); 808 809 final int startIndex = mIsRtl ? childCount - 1 : 0; 810 final int endIndex = mIsRtl ? -1 : childCount; 811 final int delta = mIsRtl ? -1 : 1; 812 813 final int pageCenter = mOrientationHandler.getCenterForPage(this, mInsets); 814 815 final int scrollOffsetStart = mOrientationHandler.getScrollOffsetStart(this, mInsets); 816 final int scrollOffsetEnd = mOrientationHandler.getScrollOffsetEnd(this, mInsets); 817 boolean pageScrollChanged = false; 818 int panelCount = getPanelCount(); 819 820 for (int i = startIndex, childStart = scrollOffsetStart; i != endIndex; i += delta) { 821 final View child = getPageAt(i); 822 if (scrollLogic.shouldIncludeView(child)) { 823 ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart, 824 pageCenter, layoutChildren); 825 final int primaryDimension = bounds.primaryDimension; 826 final int childPrimaryEnd = bounds.childPrimaryEnd; 827 828 // In case the pages are of different width, align the page to left edge for non-RTL 829 // or right edge for RTL. 830 final int pageScroll = 831 mIsRtl ? childPrimaryEnd - scrollOffsetEnd : childStart - scrollOffsetStart; 832 // If there's more than one panel, only update scroll on leftmost panel. 833 if (outPageScrolls[i] != pageScroll 834 && (panelCount <= 1 || i == getLeftmostVisiblePageForIndex(i))) { 835 pageScrollChanged = true; 836 outPageScrolls[i] = pageScroll; 837 } 838 childStart += primaryDimension + getChildGap(i, i + delta); 839 840 // This makes sure that the space is added after the page, not after each panel 841 int lastPanel = mIsRtl ? 0 : panelCount - 1; 842 if (i % panelCount == lastPanel) { 843 childStart += mPageSpacing; 844 } 845 } 846 } 847 848 if (panelCount > 1) { 849 for (int i = 0; i < childCount; i++) { 850 // In case we have multiple panels, always use leftmost panel's page scroll for all 851 // panels on the screen. 852 int adjustedScroll = outPageScrolls[getLeftmostVisiblePageForIndex(i)]; 853 if (outPageScrolls[i] != adjustedScroll) { 854 outPageScrolls[i] = adjustedScroll; 855 pageScrollChanged = true; 856 } 857 } 858 } 859 return pageScrollChanged; 860 } 861 getChildGap(int fromIndex, int toIndex)862 protected int getChildGap(int fromIndex, int toIndex) { 863 return 0; 864 } 865 updateMinAndMaxScrollX()866 protected void updateMinAndMaxScrollX() { 867 mMinScroll = computeMinScroll(); 868 mMaxScroll = computeMaxScroll(); 869 } 870 computeMinScroll()871 protected int computeMinScroll() { 872 return 0; 873 } 874 computeMaxScroll()875 protected int computeMaxScroll() { 876 int childCount = getChildCount(); 877 if (childCount > 0) { 878 final int index = mIsRtl ? 0 : childCount - 1; 879 return getScrollForPage(index); 880 } else { 881 return 0; 882 } 883 } 884 setPageSpacing(int pageSpacing)885 public void setPageSpacing(int pageSpacing) { 886 mPageSpacing = pageSpacing; 887 requestLayout(); 888 } 889 getPageSpacing()890 public int getPageSpacing() { 891 return mPageSpacing; 892 } 893 dispatchPageCountChanged()894 private void dispatchPageCountChanged() { 895 if (mPageIndicator != null) { 896 mPageIndicator.setMarkersCount(getChildCount() / getPanelCount()); 897 } 898 // This ensures that when children are added, they get the correct transforms / alphas 899 // in accordance with any scroll effects. 900 invalidate(); 901 } 902 903 @Override onViewAdded(View child)904 public void onViewAdded(View child) { 905 super.onViewAdded(child); 906 dispatchPageCountChanged(); 907 } 908 909 @Override onViewRemoved(View child)910 public void onViewRemoved(View child) { 911 super.onViewRemoved(child); 912 runOnPageScrollsInitialized(() -> { 913 mCurrentPage = validateNewPage(mCurrentPage); 914 mCurrentScrollOverPage = mCurrentPage; 915 }); 916 dispatchPageCountChanged(); 917 } 918 getChildOffset(int index)919 protected int getChildOffset(int index) { 920 if (index < 0 || index > getChildCount() - 1) return 0; 921 View pageAtIndex = getPageAt(index); 922 return mOrientationHandler.getChildStart(pageAtIndex); 923 } 924 getChildVisibleSize(int index)925 protected int getChildVisibleSize(int index) { 926 View layout = getPageAt(index); 927 return mOrientationHandler.getMeasuredSize(layout); 928 } 929 930 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)931 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 932 int page = indexOfChild(child); 933 if (!isVisible(page) || !mScroller.isFinished()) { 934 if (immediate) { 935 setCurrentPage(page); 936 } else { 937 snapToPage(page); 938 } 939 return true; 940 } 941 return false; 942 } 943 944 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)945 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 946 int focusablePage; 947 if (mNextPage != INVALID_PAGE) { 948 focusablePage = mNextPage; 949 } else { 950 focusablePage = mCurrentPage; 951 } 952 View v = getPageAt(focusablePage); 953 if (v != null) { 954 return v.requestFocus(direction, previouslyFocusedRect); 955 } 956 return false; 957 } 958 959 @Override dispatchUnhandledMove(View focused, int direction)960 public boolean dispatchUnhandledMove(View focused, int direction) { 961 if (super.dispatchUnhandledMove(focused, direction)) { 962 return true; 963 } 964 965 if (mIsRtl) { 966 if (direction == View.FOCUS_LEFT) { 967 direction = View.FOCUS_RIGHT; 968 } else if (direction == View.FOCUS_RIGHT) { 969 direction = View.FOCUS_LEFT; 970 } 971 } 972 973 int currentPage = getNextPage(); 974 int closestNeighbourIndex = -1; 975 int closestNeighbourDistance = Integer.MAX_VALUE; 976 // Find the closest neighbour page 977 for (int neighbourPageIndex : getNeighbourPageIndices(direction)) { 978 int distance = Math.abs(neighbourPageIndex - currentPage); 979 if (closestNeighbourDistance > distance) { 980 closestNeighbourDistance = distance; 981 closestNeighbourIndex = neighbourPageIndex; 982 } 983 } 984 if (closestNeighbourIndex != -1) { 985 View page = getPageAt(closestNeighbourIndex); 986 snapToPage(closestNeighbourIndex); 987 page.requestFocus(direction); 988 return true; 989 } 990 991 return false; 992 } 993 994 @Override addFocusables(ArrayList<View> views, int direction, int focusableMode)995 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 996 if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { 997 return; 998 } 999 1000 // nextPage is more reliable when multiple control movements have been done in a short 1001 // period of time 1002 getPageIndices(getNextPage()) 1003 .addAll(getNeighbourPageIndices(direction)) 1004 .forEach(pageIndex -> 1005 getPageAt(pageIndex).addFocusables(views, direction, focusableMode)); 1006 } 1007 1008 /** 1009 * If one of our descendant views decides that it could be focused now, only 1010 * pass that along if it's on the current page. 1011 * 1012 * This happens when live folders requery, and if they're off page, they 1013 * end up calling requestFocus, which pulls it on page. 1014 */ 1015 @Override focusableViewAvailable(View focused)1016 public void focusableViewAvailable(View focused) { 1017 View current = getPageAt(mCurrentPage); 1018 View v = focused; 1019 while (true) { 1020 if (v == current) { 1021 super.focusableViewAvailable(focused); 1022 return; 1023 } 1024 if (v == this) { 1025 return; 1026 } 1027 ViewParent parent = v.getParent(); 1028 if (parent instanceof View) { 1029 v = (View)v.getParent(); 1030 } else { 1031 return; 1032 } 1033 } 1034 } 1035 1036 /** 1037 * {@inheritDoc} 1038 */ 1039 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)1040 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1041 if (disallowIntercept) { 1042 // We need to make sure to cancel our long press if 1043 // a scrollable widget takes over touch events 1044 cancelCurrentPageLongPress(); 1045 } 1046 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1047 } 1048 1049 @Override onInterceptTouchEvent(MotionEvent ev)1050 public boolean onInterceptTouchEvent(MotionEvent ev) { 1051 /* 1052 * This method JUST determines whether we want to intercept the motion. 1053 * If we return true, onTouchEvent will be called and we do the actual 1054 * scrolling there. 1055 */ 1056 1057 // Skip touch handling if there are no pages to swipe 1058 if (getChildCount() <= 0) return false; 1059 1060 acquireVelocityTrackerAndAddMovement(ev); 1061 1062 /* 1063 * Shortcut the most recurring case: the user is in the dragging 1064 * state and he is moving his finger. We want to intercept this 1065 * motion. 1066 */ 1067 final int action = ev.getAction(); 1068 if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) { 1069 return true; 1070 } 1071 1072 switch (action & MotionEvent.ACTION_MASK) { 1073 case MotionEvent.ACTION_MOVE: { 1074 /* 1075 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1076 * whether the user has moved far enough from their original down touch. 1077 */ 1078 if (mActivePointerId != INVALID_POINTER) { 1079 determineScrollingStart(ev); 1080 } 1081 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 1082 // event. in that case, treat the first occurrence of a move event as a ACTION_DOWN 1083 // i.e. fall through to the next case (don't break) 1084 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 1085 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 1086 break; 1087 } 1088 1089 case MotionEvent.ACTION_DOWN: { 1090 final float x = ev.getX(); 1091 final float y = ev.getY(); 1092 // Remember location of down touch 1093 mDownMotionX = x; 1094 mDownMotionY = y; 1095 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0); 1096 mLastMotion = (int) mDownMotionPrimary; 1097 mTotalMotion = 0; 1098 mAllowEasyFling = false; 1099 mActivePointerId = ev.getPointerId(0); 1100 updateIsBeingDraggedOnTouchDown(ev); 1101 break; 1102 } 1103 1104 case MotionEvent.ACTION_UP: 1105 case MotionEvent.ACTION_CANCEL: 1106 resetTouchState(); 1107 break; 1108 1109 case MotionEvent.ACTION_POINTER_UP: 1110 onSecondaryPointerUp(ev); 1111 releaseVelocityTracker(); 1112 break; 1113 } 1114 1115 /* 1116 * The only time we want to intercept motion events is if we are in the 1117 * drag mode. 1118 */ 1119 return mIsBeingDragged; 1120 } 1121 1122 /** 1123 * If being flinged and user touches the screen, initiate drag; otherwise don't. 1124 */ updateIsBeingDraggedOnTouchDown(MotionEvent ev)1125 protected void updateIsBeingDraggedOnTouchDown(MotionEvent ev) { 1126 // mScroller.isFinished should be false when being flinged. 1127 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); 1128 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3); 1129 1130 if (finishedScrolling) { 1131 mIsBeingDragged = false; 1132 if (!mScroller.isFinished() && !mFreeScroll) { 1133 setCurrentPage(getNextPage()); 1134 pageEndTransition(); 1135 } 1136 mIsBeingDragged = !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished(); 1137 } else { 1138 mIsBeingDragged = true; 1139 } 1140 1141 // Catch the edge effect if it is active. 1142 float displacement = mOrientationHandler.getSecondaryValue(ev.getX(), ev.getY()) 1143 / mOrientationHandler.getSecondaryValue(getWidth(), getHeight()); 1144 if (!mEdgeGlowLeft.isFinished()) { 1145 mEdgeGlowLeft.onPullDistance(0f, 1f - displacement); 1146 } 1147 if (!mEdgeGlowRight.isFinished()) { 1148 mEdgeGlowRight.onPullDistance(0f, displacement, ev); 1149 } 1150 } 1151 isHandlingTouch()1152 public boolean isHandlingTouch() { 1153 return mIsBeingDragged; 1154 } 1155 determineScrollingStart(MotionEvent ev)1156 protected void determineScrollingStart(MotionEvent ev) { 1157 determineScrollingStart(ev, 1.0f); 1158 } 1159 1160 /* 1161 * Determines if we should change the touch state to start scrolling after the 1162 * user moves their touch point too far. 1163 */ determineScrollingStart(MotionEvent ev, float touchSlopScale)1164 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 1165 // Disallow scrolling if we don't have a valid pointer index 1166 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1167 if (pointerIndex == -1) return; 1168 1169 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex); 1170 final int diff = (int) Math.abs(primaryDirection - mLastMotion); 1171 final int touchSlop = Math.round(touchSlopScale * mTouchSlop); 1172 boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING; 1173 1174 if (moved) { 1175 // Scroll if the user moved far enough along the X axis 1176 mIsBeingDragged = true; 1177 mTotalMotion += Math.abs(mLastMotion - primaryDirection); 1178 mLastMotion = (int) primaryDirection; 1179 pageBeginTransition(); 1180 // Stop listening for things like pinches. 1181 requestDisallowInterceptTouchEvent(true); 1182 } 1183 } 1184 cancelCurrentPageLongPress()1185 protected void cancelCurrentPageLongPress() { 1186 // Try canceling the long press. It could also have been scheduled 1187 // by a distant descendant, so use the mAllowLongPress flag to block 1188 // everything 1189 forEachVisiblePage(View::cancelLongPress); 1190 } 1191 getScrollProgress(int screenCenter, View v, int page)1192 protected float getScrollProgress(int screenCenter, View v, int page) { 1193 final int halfScreenSize = getMeasuredWidth() / 2; 1194 int delta = screenCenter - (getScrollForPage(page) + halfScreenSize); 1195 int panelCount = getPanelCount(); 1196 int pageCount = getChildCount(); 1197 1198 int adjacentPage = page + panelCount; 1199 if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { 1200 adjacentPage = page - panelCount; 1201 } 1202 1203 final int totalDistance; 1204 if (adjacentPage < 0 || adjacentPage > pageCount - 1) { 1205 totalDistance = (v.getMeasuredWidth() + mPageSpacing) * panelCount; 1206 } else { 1207 totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); 1208 } 1209 1210 float scrollProgress = delta / (totalDistance * 1.0f); 1211 scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); 1212 scrollProgress = Math.max(scrollProgress, -MAX_SCROLL_PROGRESS); 1213 return scrollProgress; 1214 } 1215 getScrollForPage(int index)1216 public int getScrollForPage(int index) { 1217 if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) { 1218 return 0; 1219 } else { 1220 return mPageScrolls[index]; 1221 } 1222 } 1223 1224 // While layout transitions are occurring, a child's position may stray from its baseline 1225 // position. This method returns the magnitude of this stray at any given time. getLayoutTransitionOffsetForPage(int index)1226 public int getLayoutTransitionOffsetForPage(int index) { 1227 if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) { 1228 return 0; 1229 } else { 1230 View child = getChildAt(index); 1231 1232 int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); 1233 int baselineX = mPageScrolls[index] + scrollOffset; 1234 return (int) (child.getX() - baselineX); 1235 } 1236 } 1237 setEnableFreeScroll(boolean freeScroll)1238 public void setEnableFreeScroll(boolean freeScroll) { 1239 if (mFreeScroll == freeScroll) { 1240 return; 1241 } 1242 1243 boolean wasFreeScroll = mFreeScroll; 1244 mFreeScroll = freeScroll; 1245 1246 if (mFreeScroll) { 1247 setCurrentPage(getNextPage()); 1248 } else if (wasFreeScroll) { 1249 if (getScrollForPage(getNextPage()) != getScrollX()) { 1250 snapToPage(getNextPage()); 1251 } 1252 } 1253 } 1254 setEnableOverscroll(boolean enable)1255 protected void setEnableOverscroll(boolean enable) { 1256 mAllowOverScroll = enable; 1257 } 1258 isSignificantMove(float absoluteDelta, int pageOrientedSize)1259 protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) { 1260 return absoluteDelta > pageOrientedSize * SIGNIFICANT_MOVE_THRESHOLD; 1261 } 1262 1263 @Override onTouchEvent(MotionEvent ev)1264 public boolean onTouchEvent(MotionEvent ev) { 1265 // Skip touch handling if there are no pages to swipe 1266 if (getChildCount() <= 0) return false; 1267 1268 acquireVelocityTrackerAndAddMovement(ev); 1269 1270 final int action = ev.getAction(); 1271 1272 switch (action & MotionEvent.ACTION_MASK) { 1273 case MotionEvent.ACTION_DOWN: 1274 updateIsBeingDraggedOnTouchDown(ev); 1275 1276 /* 1277 * If being flinged and user touches, stop the fling. isFinished 1278 * will be false if being flinged. 1279 */ 1280 if (!mScroller.isFinished()) { 1281 abortScrollerAnimation(false); 1282 } 1283 1284 // Remember where the motion event started 1285 mDownMotionX = ev.getX(); 1286 mDownMotionY = ev.getY(); 1287 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0); 1288 mLastMotion = (int) mDownMotionPrimary; 1289 mTotalMotion = 0; 1290 mAllowEasyFling = false; 1291 mActivePointerId = ev.getPointerId(0); 1292 if (mIsBeingDragged) { 1293 pageBeginTransition(); 1294 } 1295 break; 1296 1297 case ACTION_MOVE_ALLOW_EASY_FLING: 1298 // Start scrolling immediately 1299 determineScrollingStart(ev); 1300 mAllowEasyFling = true; 1301 break; 1302 1303 case MotionEvent.ACTION_MOVE: 1304 if (mIsBeingDragged) { 1305 // Scroll to follow the motion event 1306 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1307 1308 if (pointerIndex == -1) return true; 1309 int oldScroll = mOrientationHandler.getPrimaryScroll(this); 1310 int dx = (int) ev.getX(pointerIndex); 1311 int dy = (int) ev.getY(pointerIndex); 1312 1313 int direction = mOrientationHandler.getPrimaryValue(dx, dy); 1314 int delta = mLastMotion - direction; 1315 1316 int width = getWidth(); 1317 int height = getHeight(); 1318 float size = mOrientationHandler.getPrimaryValue(width, height); 1319 float displacement = (width == 0 || height == 0) ? 0 1320 : (float) mOrientationHandler.getSecondaryValue(dx, dy) 1321 / mOrientationHandler.getSecondaryValue(width, height); 1322 mTotalMotion += Math.abs(delta); 1323 1324 if (mAllowOverScroll) { 1325 int consumed = 0; 1326 if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) { 1327 consumed = Math.round(size * 1328 mEdgeGlowRight.onPullDistance(delta / size, displacement, ev)); 1329 } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) { 1330 consumed = Math.round(-size * 1331 mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement, ev)); 1332 } 1333 delta -= consumed; 1334 } 1335 delta /= mOrientationHandler.getPrimaryScale(this); 1336 1337 // Only scroll and update mLastMotionX if we have moved some discrete amount. We 1338 // keep the remainder because we are actually testing if we've moved from the last 1339 // scrolled position (which is discrete). 1340 mLastMotion = direction; 1341 1342 if (delta != 0) { 1343 mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, delta); 1344 1345 if (mAllowOverScroll) { 1346 final float pulledToX = oldScroll + delta; 1347 1348 if (pulledToX < mMinScroll) { 1349 mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement, ev); 1350 if (!mEdgeGlowRight.isFinished()) { 1351 mEdgeGlowRight.onRelease(ev); 1352 } 1353 } else if (pulledToX > mMaxScroll) { 1354 mEdgeGlowRight.onPullDistance(delta / size, displacement, ev); 1355 if (!mEdgeGlowLeft.isFinished()) { 1356 mEdgeGlowLeft.onRelease(ev); 1357 } 1358 } 1359 1360 if (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()) { 1361 postInvalidateOnAnimation(); 1362 } 1363 } 1364 } else { 1365 awakenScrollBars(); 1366 } 1367 } else { 1368 determineScrollingStart(ev); 1369 } 1370 break; 1371 1372 case MotionEvent.ACTION_UP: 1373 if (mIsBeingDragged) { 1374 final int activePointerId = mActivePointerId; 1375 final int pointerIndex = ev.findPointerIndex(activePointerId); 1376 if (pointerIndex == -1) return true; 1377 1378 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, 1379 pointerIndex); 1380 final VelocityTracker velocityTracker = mVelocityTracker; 1381 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1382 1383 int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker, 1384 mActivePointerId); 1385 float delta = primaryDirection - mDownMotionPrimary; 1386 1387 View current = getPageAt(mCurrentPage); 1388 if (current == null) { 1389 Log.e(TAG, "current page was null. this should not happen."); 1390 return true; 1391 } 1392 1393 int pageOrientedSize = (int) (mOrientationHandler.getMeasuredSize(current) 1394 * mOrientationHandler.getPrimaryScale(this)); 1395 boolean isSignificantMove = isSignificantMove(Math.abs(delta), pageOrientedSize); 1396 1397 mTotalMotion += Math.abs(mLastMotion - primaryDirection); 1398 boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop; 1399 boolean isFling = passedSlop && shouldFlingForVelocity(velocity); 1400 boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0; 1401 boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0; 1402 if (DEBUG_FAILED_QUICKSWITCH && !isFling && mAllowEasyFling) { 1403 Log.d("Quickswitch", "isFling=false vel=" + velocity 1404 + " threshold=" + mEasyFlingThresholdVelocity); 1405 } 1406 1407 if (!mFreeScroll) { 1408 // In the case that the page is moved far to one direction and then is flung 1409 // in the opposite direction, we use a threshold to determine whether we should 1410 // just return to the starting page, or if we should skip one further. 1411 boolean returnToOriginalPage = false; 1412 if (Math.abs(delta) > pageOrientedSize * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1413 Math.signum(velocity) != Math.signum(delta) && isFling) { 1414 returnToOriginalPage = true; 1415 } 1416 1417 int finalPage; 1418 // We give flings precedence over large moves, which is why we short-circuit our 1419 // test for a large move if a fling has been registered. That is, a large 1420 // move to the left and fling to the right will register as a fling to the right. 1421 1422 if (((isSignificantMove && !isDeltaLeft && !isFling) || 1423 (isFling && !isVelocityLeft)) && mCurrentPage > 0) { 1424 finalPage = returnToOriginalPage 1425 ? mCurrentPage : mCurrentPage - getPanelCount(); 1426 runOnPageScrollsInitialized( 1427 () -> snapToPageWithVelocity(finalPage, velocity)); 1428 } else if (((isSignificantMove && isDeltaLeft && !isFling) || 1429 (isFling && isVelocityLeft)) && 1430 mCurrentPage < getChildCount() - 1) { 1431 finalPage = returnToOriginalPage 1432 ? mCurrentPage : mCurrentPage + getPanelCount(); 1433 runOnPageScrollsInitialized( 1434 () -> snapToPageWithVelocity(finalPage, velocity)); 1435 } else { 1436 runOnPageScrollsInitialized(this::snapToDestination); 1437 } 1438 } else { 1439 if (!mScroller.isFinished()) { 1440 abortScrollerAnimation(true); 1441 } 1442 1443 int initialScroll = mOrientationHandler.getPrimaryScroll(this); 1444 int maxScroll = mMaxScroll; 1445 int minScroll = mMinScroll; 1446 1447 if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) || 1448 ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) { 1449 mScroller.springBack(initialScroll, 0, minScroll, maxScroll, 0, 0); 1450 mNextPage = getDestinationPage(); 1451 } else { 1452 int velocity1 = -velocity; 1453 // Continue a scroll or fling in progress 1454 mScroller.fling(initialScroll, 0, velocity1, 0, minScroll, maxScroll, 0, 0, 1455 Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR), 0); 1456 1457 int finalPos = mScroller.getFinalX(); 1458 mNextPage = getDestinationPage(finalPos); 1459 runOnPageScrollsInitialized(this::onNotSnappingToPageInFreeScroll); 1460 } 1461 invalidate(); 1462 } 1463 mEdgeGlowLeft.onFlingVelocity(velocity); 1464 mEdgeGlowRight.onFlingVelocity(velocity); 1465 } 1466 mEdgeGlowLeft.onRelease(ev); 1467 mEdgeGlowRight.onRelease(ev); 1468 // End any intermediate reordering states 1469 resetTouchState(); 1470 break; 1471 1472 case MotionEvent.ACTION_CANCEL: 1473 if (mIsBeingDragged) { 1474 runOnPageScrollsInitialized(this::snapToDestination); 1475 } 1476 mEdgeGlowLeft.onRelease(ev); 1477 mEdgeGlowRight.onRelease(ev); 1478 resetTouchState(); 1479 break; 1480 1481 case MotionEvent.ACTION_POINTER_UP: 1482 onSecondaryPointerUp(ev); 1483 releaseVelocityTracker(); 1484 break; 1485 } 1486 1487 return true; 1488 } 1489 onNotSnappingToPageInFreeScroll()1490 protected void onNotSnappingToPageInFreeScroll() { } 1491 1492 /** 1493 * Called when the view edges absorb part of the scroll. Subclasses can override this 1494 * to provide custom behavior during animation. 1495 */ onEdgeAbsorbingScroll()1496 protected void onEdgeAbsorbingScroll() { 1497 } 1498 1499 /** 1500 * Called when the current page closest to the center of the screen changes as part of the 1501 * scroll. Subclasses can override this to provide custom behavior during scroll. 1502 */ onScrollOverPageChanged()1503 protected void onScrollOverPageChanged() { 1504 } 1505 shouldFlingForVelocity(int velocity)1506 protected boolean shouldFlingForVelocity(int velocity) { 1507 float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity; 1508 return Math.abs(velocity) > threshold; 1509 } 1510 resetTouchState()1511 protected void resetTouchState() { 1512 releaseVelocityTracker(); 1513 mIsBeingDragged = false; 1514 mActivePointerId = INVALID_POINTER; 1515 } 1516 1517 @Override onGenericMotionEvent(MotionEvent event)1518 public boolean onGenericMotionEvent(MotionEvent event) { 1519 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 1520 switch (event.getAction()) { 1521 case MotionEvent.ACTION_SCROLL: { 1522 // Handle mouse (or ext. device) by shifting the page depending on the scroll 1523 final float vscroll; 1524 final float hscroll; 1525 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 1526 vscroll = 0; 1527 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1528 } else { 1529 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1530 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1531 } 1532 if (!canScroll(Math.abs(vscroll), Math.abs(hscroll))) { 1533 return false; 1534 } 1535 if (hscroll != 0 || vscroll != 0) { 1536 boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) 1537 : (hscroll > 0 || vscroll > 0); 1538 if (isForwardScroll) { 1539 scrollRight(); 1540 } else { 1541 scrollLeft(); 1542 } 1543 return true; 1544 } 1545 } 1546 } 1547 } 1548 return super.onGenericMotionEvent(event); 1549 } 1550 1551 /** 1552 * Returns true if the paged view can scroll for the provided vertical and horizontal 1553 * scroll values 1554 */ canScroll(float absVScroll, float absHScroll)1555 protected boolean canScroll(float absVScroll, float absHScroll) { 1556 ActivityContext ac = ActivityContext.lookupContext(getContext()); 1557 return (ac == null || AbstractFloatingView.getTopOpenView(ac) == null); 1558 } 1559 acquireVelocityTrackerAndAddMovement(MotionEvent ev)1560 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 1561 if (mVelocityTracker == null) { 1562 mVelocityTracker = VelocityTracker.obtain(); 1563 } 1564 mVelocityTracker.addMovement(ev); 1565 } 1566 releaseVelocityTracker()1567 private void releaseVelocityTracker() { 1568 if (mVelocityTracker != null) { 1569 mVelocityTracker.clear(); 1570 mVelocityTracker.recycle(); 1571 mVelocityTracker = null; 1572 } 1573 } 1574 onSecondaryPointerUp(MotionEvent ev)1575 private void onSecondaryPointerUp(MotionEvent ev) { 1576 final int pointerIndex = ev.getActionIndex(); 1577 final int pointerId = ev.getPointerId(pointerIndex); 1578 if (pointerId == mActivePointerId) { 1579 // This was our active pointer going up. Choose a new 1580 // active pointer and adjust accordingly. 1581 // TODO: Make this decision more intelligent. 1582 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1583 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, newPointerIndex); 1584 mLastMotion = (int) mDownMotionPrimary; 1585 mActivePointerId = ev.getPointerId(newPointerIndex); 1586 if (mVelocityTracker != null) { 1587 mVelocityTracker.clear(); 1588 } 1589 } 1590 } 1591 1592 @Override requestChildFocus(View child, View focused)1593 public void requestChildFocus(View child, View focused) { 1594 super.requestChildFocus(child, focused); 1595 if (!shouldHandleRequestChildFocus(child)) { 1596 return; 1597 } 1598 // In case the device is controlled by a controller, mCurrentPage isn't updated properly 1599 // which results in incorrect navigation 1600 int nextPage = getNextPage(); 1601 if (nextPage != mCurrentPage) { 1602 setCurrentPage(nextPage); 1603 } 1604 1605 int page = indexOfChild(child); 1606 if (page >= 0 && !isVisible(page) && !isInTouchMode()) { 1607 snapToPage(page); 1608 } 1609 } 1610 shouldHandleRequestChildFocus(View child)1611 protected boolean shouldHandleRequestChildFocus(View child) { 1612 return true; 1613 } 1614 getDestinationPage()1615 public int getDestinationPage() { 1616 return getDestinationPage(mOrientationHandler.getPrimaryScroll(this)); 1617 } 1618 getDestinationPage(int primaryScroll)1619 protected int getDestinationPage(int primaryScroll) { 1620 return getPageNearestToCenterOfScreen(primaryScroll); 1621 } 1622 getPageNearestToCenterOfScreen()1623 public int getPageNearestToCenterOfScreen() { 1624 return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this)); 1625 } 1626 getPageNearestToCenterOfScreen(int primaryScroll)1627 private int getPageNearestToCenterOfScreen(int primaryScroll) { 1628 int screenCenter = getScreenCenter(primaryScroll); 1629 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 1630 int minDistanceFromScreenCenterIndex = -1; 1631 final int childCount = getChildCount(); 1632 for (int i = 0; i < childCount; ++i) { 1633 int distanceFromScreenCenter = Math.abs( 1634 getDisplacementFromScreenCenter(i, screenCenter)); 1635 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 1636 minDistanceFromScreenCenter = distanceFromScreenCenter; 1637 minDistanceFromScreenCenterIndex = i; 1638 } 1639 } 1640 return minDistanceFromScreenCenterIndex; 1641 } 1642 getDisplacementFromScreenCenter(int childIndex, int screenCenter)1643 private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) { 1644 int childSize = getChildVisibleSize(childIndex); 1645 int halfChildSize = (childSize / 2); 1646 int childCenter = getChildOffset(childIndex) + halfChildSize; 1647 return childCenter - screenCenter; 1648 } 1649 getDisplacementFromScreenCenter(int childIndex)1650 protected int getDisplacementFromScreenCenter(int childIndex) { 1651 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 1652 int screenCenter = getScreenCenter(primaryScroll); 1653 return getDisplacementFromScreenCenter(childIndex, screenCenter); 1654 } 1655 getScreenCenter(int primaryScroll)1656 protected int getScreenCenter(int primaryScroll) { 1657 float primaryScale = mOrientationHandler.getPrimaryScale(this); 1658 float primaryPivot = mOrientationHandler.getPrimaryValue(getPivotX(), getPivotY()); 1659 int pageOrientationSize = mOrientationHandler.getMeasuredSize(this); 1660 return Math.round(primaryScroll + (pageOrientationSize / 2f - primaryPivot) / primaryScale 1661 + primaryPivot); 1662 } 1663 snapToDestination()1664 protected void snapToDestination() { 1665 snapToPage(getDestinationPage(), getSnapAnimationDuration()); 1666 } 1667 1668 // We want the duration of the page snap animation to be influenced by the distance that 1669 // the screen has to travel, however, we don't want this duration to be effected in a 1670 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 1671 // of travel has on the overall snap duration. distanceInfluenceForSnapDuration(float f)1672 private float distanceInfluenceForSnapDuration(float f) { 1673 f -= 0.5f; // center the values about 0. 1674 f *= 0.3f * Math.PI / 2.0f; 1675 return (float) Math.sin(f); 1676 } 1677 snapToPageWithVelocity(int whichPage, int velocity)1678 protected boolean snapToPageWithVelocity(int whichPage, int velocity) { 1679 whichPage = validateNewPage(whichPage); 1680 int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2; 1681 1682 final int newLoc = getScrollForPage(whichPage); 1683 int delta = newLoc - mOrientationHandler.getPrimaryScroll(this); 1684 int duration = 0; 1685 1686 if (Math.abs(velocity) < mMinFlingVelocity) { 1687 // If the velocity is low enough, then treat this more as an automatic page advance 1688 // as opposed to an apparent physical response to flinging 1689 return snapToPage(whichPage, getSnapAnimationDuration()); 1690 } 1691 1692 // Here we compute a "distance" that will be used in the computation of the overall 1693 // snap duration. This is a function of the actual distance that needs to be traveled; 1694 // we keep this value close to half screen size in order to reduce the variance in snap 1695 // duration as a function of the distance the page needs to travel. 1696 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 1697 float distance = halfScreenSize + halfScreenSize * 1698 distanceInfluenceForSnapDuration(distanceRatio); 1699 1700 velocity = Math.abs(velocity); 1701 velocity = Math.max(mMinSnapVelocity, velocity); 1702 1703 // we want the page's snap velocity to approximately match the velocity at which the 1704 // user flings, so we scale the duration by a value near to the derivative of the scroll 1705 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 1706 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 1707 1708 return snapToPage(whichPage, delta, duration); 1709 } 1710 getSnapAnimationDuration()1711 protected int getSnapAnimationDuration() { 1712 return mPageSnapAnimationDuration; 1713 } 1714 snapToPage(int whichPage)1715 public boolean snapToPage(int whichPage) { 1716 return snapToPage(whichPage, getSnapAnimationDuration()); 1717 } 1718 snapToPageImmediately(int whichPage)1719 public boolean snapToPageImmediately(int whichPage) { 1720 return snapToPage(whichPage, getSnapAnimationDuration(), true); 1721 } 1722 snapToPage(int whichPage, int duration)1723 public boolean snapToPage(int whichPage, int duration) { 1724 return snapToPage(whichPage, duration, false); 1725 } 1726 snapToPage(int whichPage, int duration, boolean immediate)1727 protected boolean snapToPage(int whichPage, int duration, boolean immediate) { 1728 whichPage = validateNewPage(whichPage); 1729 1730 int newLoc = getScrollForPage(whichPage); 1731 final int delta = newLoc - mOrientationHandler.getPrimaryScroll(this); 1732 return snapToPage(whichPage, delta, duration, immediate); 1733 } 1734 snapToPage(int whichPage, int delta, int duration)1735 protected boolean snapToPage(int whichPage, int delta, int duration) { 1736 return snapToPage(whichPage, delta, duration, false); 1737 } 1738 snapToPage(int whichPage, int delta, int duration, boolean immediate)1739 protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate) { 1740 if (mFirstLayout) { 1741 setCurrentPage(whichPage); 1742 return false; 1743 } 1744 1745 if (FeatureFlags.IS_STUDIO_BUILD && !Utilities.isRunningInTestHarness()) { 1746 duration *= Settings.Global.getFloat(getContext().getContentResolver(), 1747 Settings.Global.WINDOW_ANIMATION_SCALE, 1); 1748 } 1749 1750 whichPage = validateNewPage(whichPage); 1751 1752 mNextPage = whichPage; 1753 1754 awakenScrollBars(duration); 1755 if (immediate) { 1756 duration = 0; 1757 } else if (duration == 0) { 1758 duration = Math.abs(delta); 1759 } 1760 1761 if (duration != 0) { 1762 pageBeginTransition(); 1763 } 1764 1765 if (!mScroller.isFinished()) { 1766 abortScrollerAnimation(false); 1767 } 1768 1769 mScroller.startScroll(mOrientationHandler.getPrimaryScroll(this), 0, delta, 0, duration); 1770 updatePageIndicator(); 1771 1772 // Trigger a compute() to finish switching pages if necessary 1773 if (immediate) { 1774 computeScroll(); 1775 pageEndTransition(); 1776 } 1777 1778 invalidate(); 1779 return Math.abs(delta) > 0; 1780 } 1781 scrollLeft()1782 public boolean scrollLeft() { 1783 if (getNextPage() > 0) { 1784 snapToPage(getNextPage() - getPanelCount()); 1785 return true; 1786 } 1787 return mAllowOverScroll; 1788 } 1789 scrollRight()1790 public boolean scrollRight() { 1791 if (getNextPage() < getChildCount() - 1) { 1792 snapToPage(getNextPage() + getPanelCount()); 1793 return true; 1794 } 1795 return mAllowOverScroll; 1796 } 1797 1798 @Override onScrollChanged(int l, int t, int oldl, int oldt)1799 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 1800 if (mScroller.isFinished()) { 1801 // This was not caused by the scroller, skip it. 1802 return; 1803 } 1804 int newDestinationPage = getDestinationPage(); 1805 if (newDestinationPage >= 0 && newDestinationPage != mCurrentScrollOverPage) { 1806 mCurrentScrollOverPage = newDestinationPage; 1807 onScrollOverPageChanged(); 1808 } 1809 } 1810 1811 @Override getAccessibilityClassName()1812 public CharSequence getAccessibilityClassName() { 1813 // Some accessibility services have special logic for ScrollView. Since we provide same 1814 // accessibility info as ScrollView, inform the service to handle use the same way. 1815 return ScrollView.class.getName(); 1816 } 1817 isPageOrderFlipped()1818 protected boolean isPageOrderFlipped() { 1819 return false; 1820 } 1821 1822 /* Accessibility */ 1823 @SuppressWarnings("deprecation") 1824 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1825 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1826 super.onInitializeAccessibilityNodeInfo(info); 1827 final boolean pagesFlipped = isPageOrderFlipped(); 1828 info.setScrollable(getPageCount() > 0); 1829 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 1830 if (getCurrentPage() < getPageCount() - getPanelCount() 1831 || (getCurrentPage() == getPageCount() - getPanelCount() 1832 && primaryScroll != getScrollForPage(getPageCount() - getPanelCount()))) { 1833 info.addAction(pagesFlipped ? 1834 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD 1835 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1836 info.addAction(mIsRtl ? 1837 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT 1838 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT); 1839 } 1840 if (getCurrentPage() > 0 1841 || (getCurrentPage() == 0 && primaryScroll != getScrollForPage(0))) { 1842 info.addAction(pagesFlipped ? 1843 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD 1844 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1845 info.addAction(mIsRtl ? 1846 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT 1847 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT); 1848 } 1849 // Accessibility-wise, PagedView doesn't support long click, so disabling it. 1850 // Besides disabling the accessibility long-click, this also prevents this view from getting 1851 // accessibility focus. 1852 info.setLongClickable(false); 1853 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 1854 } 1855 1856 @Override sendAccessibilityEvent(int eventType)1857 public void sendAccessibilityEvent(int eventType) { 1858 // Don't let the view send real scroll events. 1859 if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { 1860 super.sendAccessibilityEvent(eventType); 1861 } 1862 } 1863 1864 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1865 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1866 super.onInitializeAccessibilityEvent(event); 1867 event.setScrollable(mAllowOverScroll || getPageCount() > 1); 1868 } 1869 1870 @Override performAccessibilityAction(int action, Bundle arguments)1871 public boolean performAccessibilityAction(int action, Bundle arguments) { 1872 if (super.performAccessibilityAction(action, arguments)) { 1873 return true; 1874 } 1875 final boolean pagesFlipped = isPageOrderFlipped(); 1876 switch (action) { 1877 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1878 if (pagesFlipped ? scrollLeft() : scrollRight()) { 1879 return true; 1880 } 1881 } break; 1882 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1883 if (pagesFlipped ? scrollRight() : scrollLeft()) { 1884 return true; 1885 } 1886 } break; 1887 case android.R.id.accessibilityActionPageRight: { 1888 if (!mIsRtl) { 1889 return scrollRight(); 1890 } else { 1891 return scrollLeft(); 1892 } 1893 } 1894 case android.R.id.accessibilityActionPageLeft: { 1895 if (!mIsRtl) { 1896 return scrollLeft(); 1897 } else { 1898 return scrollRight(); 1899 } 1900 } 1901 } 1902 return false; 1903 } 1904 canAnnouncePageDescription()1905 protected boolean canAnnouncePageDescription() { 1906 return true; 1907 } 1908 getCurrentPageDescription()1909 protected String getCurrentPageDescription() { 1910 return getContext().getString(R.string.default_scroll_format, 1911 getNextPage() + 1, getChildCount()); 1912 } 1913 getDownMotionX()1914 protected float getDownMotionX() { 1915 return mDownMotionX; 1916 } 1917 getDownMotionY()1918 protected float getDownMotionY() { 1919 return mDownMotionY; 1920 } 1921 1922 protected interface ComputePageScrollsLogic { 1923 shouldIncludeView(View view)1924 boolean shouldIncludeView(View view); 1925 } 1926 getVisibleChildrenRange()1927 public int[] getVisibleChildrenRange() { 1928 float visibleLeft = 0; 1929 float visibleRight = visibleLeft + getMeasuredWidth(); 1930 float scaleX = getScaleX(); 1931 if (scaleX < 1 && scaleX > 0) { 1932 float mid = getMeasuredWidth() / 2; 1933 visibleLeft = mid - ((mid - visibleLeft) / scaleX); 1934 visibleRight = mid + ((visibleRight - mid) / scaleX); 1935 } 1936 1937 int leftChild = -1; 1938 int rightChild = -1; 1939 final int childCount = getChildCount(); 1940 for (int i = 0; i < childCount; i++) { 1941 final View child = getPageAt(i); 1942 1943 float left = child.getLeft() + child.getTranslationX() - getScrollX(); 1944 if (left <= visibleRight && (left + child.getMeasuredWidth()) >= visibleLeft) { 1945 if (leftChild == -1) { 1946 leftChild = i; 1947 } 1948 rightChild = i; 1949 } 1950 } 1951 mTmpIntPair[0] = leftChild; 1952 mTmpIntPair[1] = rightChild; 1953 return mTmpIntPair; 1954 } 1955 1956 @Override draw(Canvas canvas)1957 public void draw(Canvas canvas) { 1958 super.draw(canvas); 1959 drawEdgeEffect(canvas); 1960 pageEndTransition(); 1961 } 1962 drawEdgeEffect(Canvas canvas)1963 protected void drawEdgeEffect(Canvas canvas) { 1964 if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) { 1965 final int width = getWidth(); 1966 final int height = getHeight(); 1967 if (!mEdgeGlowLeft.isFinished()) { 1968 final int restoreCount = canvas.save(); 1969 canvas.rotate(-90); 1970 canvas.translate(-height, Math.min(mMinScroll, getScrollX())); 1971 mEdgeGlowLeft.setSize(height, width); 1972 if (mEdgeGlowLeft.draw(canvas)) { 1973 postInvalidateOnAnimation(); 1974 } 1975 canvas.restoreToCount(restoreCount); 1976 } 1977 if (!mEdgeGlowRight.isFinished()) { 1978 final int restoreCount = canvas.save(); 1979 canvas.rotate(90, width, 0); 1980 canvas.translate(width, -(Math.max(mMaxScroll, getScrollX()))); 1981 1982 mEdgeGlowRight.setSize(height, width); 1983 if (mEdgeGlowRight.draw(canvas)) { 1984 postInvalidateOnAnimation(); 1985 } 1986 canvas.restoreToCount(restoreCount); 1987 } 1988 } 1989 } 1990 } 1991