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