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