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