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