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