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