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