• 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.app.animation.Interpolators.SCROLL;
20 import static com.android.launcher3.RemoveAnimationSettingsTracker.WINDOW_ANIMATION_SCALE_URI;
21 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
22 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
23 import static com.android.launcher3.testing.shared.TestProtocol.SCROLL_FINISHED_MESSAGE;
24 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
25 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_BY;
26 import static com.android.launcher3.touch.PagedOrientationHandler.VIEW_SCROLL_TO;
27 
28 import android.animation.LayoutTransition;
29 import android.annotation.SuppressLint;
30 import android.content.Context;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.content.res.TypedArray;
34 import android.graphics.Canvas;
35 import android.graphics.Rect;
36 import android.os.Bundle;
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      * Run the given `callback` immediately once {@code mPageScrolls} has been initialized,
722      * otherwise queue the callback to `mOnPageScrollsInitializedCallbacks`.
723      */
runOnPageScrollsInitialized(Runnable callback)724     public void runOnPageScrollsInitialized(Runnable callback) {
725         if (isPageScrollsInitialized()) {
726             callback.run();
727         } else {
728             mOnPageScrollsInitializedCallbacks.add(callback);
729         }
730     }
731 
onPageScrollsInitialized()732     protected void onPageScrollsInitialized() {
733         for (Runnable callback : mOnPageScrollsInitializedCallbacks) {
734             callback.run();
735         }
736         mOnPageScrollsInitializedCallbacks.clear();
737     }
738 
739     @SuppressLint("DrawAllocation")
740     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)741     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
742         mIsLayoutValid = true;
743         final int childCount = getChildCount();
744         int[] pageScrolls = mPageScrolls;
745         boolean pageScrollChanged = false;
746         if (!isPageScrollsInitialized()) {
747             pageScrolls = new int[childCount];
748             pageScrollChanged = true;
749         }
750 
751         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
752 
753         pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC);
754         mPageScrolls = pageScrolls;
755 
756         if (childCount == 0) {
757             onPageScrollsInitialized();
758             return;
759         }
760 
761         final LayoutTransition transition = getLayoutTransition();
762         // If the transition is running defer updating max scroll, as some empty pages could
763         // still be present, and a max scroll change could cause sudden jumps in scroll.
764         if (transition != null && transition.isRunning()) {
765             transition.addTransitionListener(new LayoutTransition.TransitionListener() {
766 
767                 @Override
768                 public void startTransition(LayoutTransition transition, ViewGroup container,
769                         View view, int transitionType) { }
770 
771                 @Override
772                 public void endTransition(LayoutTransition transition, ViewGroup container,
773                         View view, int transitionType) {
774                     // Wait until all transitions are complete.
775                     if (!transition.isRunning()) {
776                         transition.removeTransitionListener(this);
777                         updateMinAndMaxScrollX();
778                     }
779                 }
780             });
781         } else {
782             updateMinAndMaxScrollX();
783         }
784 
785         if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) {
786             updateCurrentPageScroll();
787             mFirstLayout = false;
788         }
789 
790         if (mScroller.isFinished() && pageScrollChanged) {
791             // TODO(b/246283207): Remove logging once root cause of flake detected.
792             if (Utilities.isRunningInTestHarness() && !(this instanceof Workspace)) {
793                 Log.d("b/246283207", TAG + "#onLayout() -> "
794                         + "if(mScroller.isFinished() && pageScrollChanged) -> getNextPage(): "
795                         + getNextPage() + ", getScrollForPage(getNextPage()): "
796                         + getScrollForPage(getNextPage()));
797             }
798             setCurrentPage(getNextPage());
799         }
800         onPageScrollsInitialized();
801     }
802 
803     /**
804      * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length
805      * of {@code outPageScrolls} should be same as the the childCount
806      */
getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)807     protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
808             ComputePageScrollsLogic scrollLogic) {
809         final int childCount = getChildCount();
810 
811         final int startIndex = mIsRtl ? childCount - 1 : 0;
812         final int endIndex = mIsRtl ? -1 : childCount;
813         final int delta = mIsRtl ? -1 : 1;
814 
815         final int pageCenter = mOrientationHandler.getCenterForPage(this, mInsets);
816 
817         final int scrollOffsetStart = mOrientationHandler.getScrollOffsetStart(this, mInsets);
818         final int scrollOffsetEnd = mOrientationHandler.getScrollOffsetEnd(this, mInsets);
819         boolean pageScrollChanged = false;
820         int panelCount = getPanelCount();
821 
822         for (int i = startIndex, childStart = scrollOffsetStart; i != endIndex; i += delta) {
823             final View child = getPageAt(i);
824             if (scrollLogic.shouldIncludeView(child)) {
825                 ChildBounds bounds = mOrientationHandler.getChildBounds(child, childStart,
826                     pageCenter, layoutChildren);
827                 final int primaryDimension = bounds.primaryDimension;
828                 final int childPrimaryEnd = bounds.childPrimaryEnd;
829 
830                 // In case the pages are of different width, align the page to left edge for non-RTL
831                 // or right edge for RTL.
832                 final int pageScroll =
833                         mIsRtl ? childPrimaryEnd - scrollOffsetEnd : childStart - scrollOffsetStart;
834                 // If there's more than one panel, only update scroll on leftmost panel.
835                 if (outPageScrolls[i] != pageScroll
836                         && (panelCount <= 1 || i == getLeftmostVisiblePageForIndex(i))) {
837                     pageScrollChanged = true;
838                     outPageScrolls[i] = pageScroll;
839                 }
840                 childStart += primaryDimension + getChildGap(i, i + delta);
841 
842                 // This makes sure that the space is added after the page, not after each panel
843                 int lastPanel = mIsRtl ? 0 : panelCount - 1;
844                 if (i % panelCount == lastPanel) {
845                     childStart += mPageSpacing;
846                 }
847             }
848         }
849 
850         if (panelCount > 1) {
851             for (int i = 0; i < childCount; i++) {
852                 // In case we have multiple panels, always use leftmost panel's page scroll for all
853                 // panels on the screen.
854                 int adjustedScroll = outPageScrolls[getLeftmostVisiblePageForIndex(i)];
855                 if (outPageScrolls[i] != adjustedScroll) {
856                     outPageScrolls[i] = adjustedScroll;
857                     pageScrollChanged = true;
858                 }
859             }
860         }
861         return pageScrollChanged;
862     }
863 
getChildGap(int fromIndex, int toIndex)864     protected int getChildGap(int fromIndex, int toIndex) {
865         return 0;
866     }
867 
updateMinAndMaxScrollX()868     protected void updateMinAndMaxScrollX() {
869         mMinScroll = computeMinScroll();
870         mMaxScroll = computeMaxScroll();
871     }
872 
computeMinScroll()873     protected int computeMinScroll() {
874         return 0;
875     }
876 
computeMaxScroll()877     protected int computeMaxScroll() {
878         int childCount = getChildCount();
879         if (childCount > 0) {
880             final int index = mIsRtl ? 0 : childCount - 1;
881             return getScrollForPage(index);
882         } else {
883             return 0;
884         }
885     }
886 
setPageSpacing(int pageSpacing)887     public void setPageSpacing(int pageSpacing) {
888         mPageSpacing = pageSpacing;
889         requestLayout();
890     }
891 
getPageSpacing()892     public int getPageSpacing() {
893         return mPageSpacing;
894     }
895 
dispatchPageCountChanged()896     private void dispatchPageCountChanged() {
897         if (mPageIndicator != null) {
898             mPageIndicator.setMarkersCount(getChildCount() / getPanelCount());
899         }
900         // This ensures that when children are added, they get the correct transforms / alphas
901         // in accordance with any scroll effects.
902         invalidate();
903     }
904 
905     @Override
onViewAdded(View child)906     public void onViewAdded(View child) {
907         super.onViewAdded(child);
908         dispatchPageCountChanged();
909     }
910 
911     @Override
onViewRemoved(View child)912     public void onViewRemoved(View child) {
913         super.onViewRemoved(child);
914         runOnPageScrollsInitialized(() -> {
915             mCurrentPage = validateNewPage(mCurrentPage);
916             mCurrentScrollOverPage = mCurrentPage;
917         });
918         dispatchPageCountChanged();
919     }
920 
getChildOffset(int index)921     protected int getChildOffset(int index) {
922         if (index < 0 || index > getChildCount() - 1) return 0;
923         View pageAtIndex = getPageAt(index);
924         return mOrientationHandler.getChildStart(pageAtIndex);
925     }
926 
getChildVisibleSize(int index)927     protected int getChildVisibleSize(int index) {
928         View layout = getPageAt(index);
929         return mOrientationHandler.getMeasuredSize(layout);
930     }
931 
932     @Override
requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)933     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
934         int page = indexOfChild(child);
935         if (!isVisible(page) || !mScroller.isFinished()) {
936             if (immediate) {
937                 setCurrentPage(page);
938             } else {
939                 snapToPage(page);
940             }
941             return true;
942         }
943         return false;
944     }
945 
946     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)947     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
948         int focusablePage;
949         if (mNextPage != INVALID_PAGE) {
950             focusablePage = mNextPage;
951         } else {
952             focusablePage = mCurrentPage;
953         }
954         View v = getPageAt(focusablePage);
955         if (v != null) {
956             return v.requestFocus(direction, previouslyFocusedRect);
957         }
958         return false;
959     }
960 
961     @Override
dispatchUnhandledMove(View focused, int direction)962     public boolean dispatchUnhandledMove(View focused, int direction) {
963         if (super.dispatchUnhandledMove(focused, direction)) {
964             return true;
965         }
966 
967         if (mIsRtl) {
968             if (direction == View.FOCUS_LEFT) {
969                 direction = View.FOCUS_RIGHT;
970             } else if (direction == View.FOCUS_RIGHT) {
971                 direction = View.FOCUS_LEFT;
972             }
973         }
974 
975         int currentPage = getNextPage();
976         int closestNeighbourIndex = -1;
977         int closestNeighbourDistance = Integer.MAX_VALUE;
978         // Find the closest neighbour page
979         for (int neighbourPageIndex : getNeighbourPageIndices(direction)) {
980             int distance = Math.abs(neighbourPageIndex - currentPage);
981             if (closestNeighbourDistance > distance) {
982                 closestNeighbourDistance = distance;
983                 closestNeighbourIndex = neighbourPageIndex;
984             }
985         }
986         if (closestNeighbourIndex != -1) {
987             View page = getPageAt(closestNeighbourIndex);
988             snapToPage(closestNeighbourIndex);
989             page.requestFocus(direction);
990             return true;
991         }
992 
993         return false;
994     }
995 
996     @Override
addFocusables(ArrayList<View> views, int direction, int focusableMode)997     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
998         if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
999             return;
1000         }
1001 
1002         // nextPage is more reliable when multiple control movements have been done in a short
1003         // period of time
1004         getPageIndices(getNextPage())
1005                 .addAll(getNeighbourPageIndices(direction))
1006                 .forEach(pageIndex ->
1007                         getPageAt(pageIndex).addFocusables(views, direction, focusableMode));
1008     }
1009 
1010     /**
1011      * If one of our descendant views decides that it could be focused now, only
1012      * pass that along if it's on the current page.
1013      *
1014      * This happens when live folders requery, and if they're off page, they
1015      * end up calling requestFocus, which pulls it on page.
1016      */
1017     @Override
focusableViewAvailable(View focused)1018     public void focusableViewAvailable(View focused) {
1019         View current = getPageAt(mCurrentPage);
1020         View v = focused;
1021         while (true) {
1022             if (v == current) {
1023                 super.focusableViewAvailable(focused);
1024                 return;
1025             }
1026             if (v == this) {
1027                 return;
1028             }
1029             ViewParent parent = v.getParent();
1030             if (parent instanceof View) {
1031                 v = (View)v.getParent();
1032             } else {
1033                 return;
1034             }
1035         }
1036     }
1037 
1038     /**
1039      * {@inheritDoc}
1040      */
1041     @Override
requestDisallowInterceptTouchEvent(boolean disallowIntercept)1042     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
1043         if (disallowIntercept) {
1044             // We need to make sure to cancel our long press if
1045             // a scrollable widget takes over touch events
1046             cancelCurrentPageLongPress();
1047         }
1048         super.requestDisallowInterceptTouchEvent(disallowIntercept);
1049     }
1050 
1051     @Override
onInterceptTouchEvent(MotionEvent ev)1052     public boolean onInterceptTouchEvent(MotionEvent ev) {
1053         /*
1054          * This method JUST determines whether we want to intercept the motion.
1055          * If we return true, onTouchEvent will be called and we do the actual
1056          * scrolling there.
1057          */
1058 
1059         // Skip touch handling if there are no pages to swipe
1060         if (getChildCount() <= 0) return false;
1061 
1062         acquireVelocityTrackerAndAddMovement(ev);
1063 
1064         /*
1065          * Shortcut the most recurring case: the user is in the dragging
1066          * state and he is moving his finger.  We want to intercept this
1067          * motion.
1068          */
1069         final int action = ev.getAction();
1070         if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) {
1071             return true;
1072         }
1073 
1074         switch (action & MotionEvent.ACTION_MASK) {
1075             case MotionEvent.ACTION_MOVE: {
1076                 /*
1077                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1078                  * whether the user has moved far enough from their original down touch.
1079                  */
1080                 if (mActivePointerId != INVALID_POINTER) {
1081                     determineScrollingStart(ev);
1082                 }
1083                 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
1084                 // event. in that case, treat the first occurrence of a move event as a ACTION_DOWN
1085                 // i.e. fall through to the next case (don't break)
1086                 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
1087                 // while it's small- this was causing a crash before we checked for INVALID_POINTER)
1088                 break;
1089             }
1090 
1091             case MotionEvent.ACTION_DOWN: {
1092                 final float x = ev.getX();
1093                 final float y = ev.getY();
1094                 // Remember location of down touch
1095                 mDownMotionX = x;
1096                 mDownMotionY = y;
1097                 mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0);
1098                 mLastMotion = (int) mDownMotionPrimary;
1099                 mTotalMotion = 0;
1100                 mAllowEasyFling = false;
1101                 mActivePointerId = ev.getPointerId(0);
1102                 updateIsBeingDraggedOnTouchDown(ev);
1103                 break;
1104             }
1105 
1106             case MotionEvent.ACTION_UP:
1107             case MotionEvent.ACTION_CANCEL:
1108                 resetTouchState();
1109                 break;
1110 
1111             case MotionEvent.ACTION_POINTER_UP:
1112                 onSecondaryPointerUp(ev);
1113                 releaseVelocityTracker();
1114                 break;
1115         }
1116 
1117         /*
1118          * The only time we want to intercept motion events is if we are in the
1119          * drag mode.
1120          */
1121         return mIsBeingDragged;
1122     }
1123 
1124     /**
1125      * If being flinged and user touches the screen, initiate drag; otherwise don't.
1126      */
updateIsBeingDraggedOnTouchDown(MotionEvent ev)1127     protected void updateIsBeingDraggedOnTouchDown(MotionEvent ev) {
1128         // mScroller.isFinished should be false when being flinged.
1129         final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
1130         final boolean finishedScrolling = (mScroller.isFinished() || xDist < mPageSlop / 3);
1131 
1132         if (finishedScrolling) {
1133             mIsBeingDragged = false;
1134             if (!mScroller.isFinished() && !mFreeScroll) {
1135                 setCurrentPage(getNextPage());
1136                 pageEndTransition();
1137             }
1138             mIsBeingDragged = !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished();
1139         } else {
1140             mIsBeingDragged = true;
1141         }
1142 
1143         // Catch the edge effect if it is active.
1144         float displacement = mOrientationHandler.getSecondaryValue(ev.getX(), ev.getY())
1145                 / mOrientationHandler.getSecondaryValue(getWidth(), getHeight());
1146         if (!mEdgeGlowLeft.isFinished()) {
1147             mEdgeGlowLeft.onPullDistance(0f, 1f - displacement);
1148         }
1149         if (!mEdgeGlowRight.isFinished()) {
1150             mEdgeGlowRight.onPullDistance(0f, displacement, ev);
1151         }
1152     }
1153 
isHandlingTouch()1154     public boolean isHandlingTouch() {
1155         return mIsBeingDragged;
1156     }
1157 
determineScrollingStart(MotionEvent ev)1158     protected void determineScrollingStart(MotionEvent ev) {
1159         determineScrollingStart(ev, 1.0f);
1160     }
1161 
1162     /*
1163      * Determines if we should change the touch state to start scrolling after the
1164      * user moves their touch point too far.
1165      */
determineScrollingStart(MotionEvent ev, float touchSlopScale)1166     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
1167         // Disallow scrolling if we don't have a valid pointer index
1168         final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1169         if (pointerIndex == -1) return;
1170 
1171         final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev, pointerIndex);
1172         final int diff = (int) Math.abs(primaryDirection - mLastMotion);
1173         final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
1174         boolean moved = diff > touchSlop || ev.getAction() == ACTION_MOVE_ALLOW_EASY_FLING;
1175 
1176         if (moved) {
1177             // Scroll if the user moved far enough along the X axis
1178             mIsBeingDragged = true;
1179             mTotalMotion += Math.abs(mLastMotion - primaryDirection);
1180             mLastMotion = (int) primaryDirection;
1181             pageBeginTransition();
1182             // Stop listening for things like pinches.
1183             requestDisallowInterceptTouchEvent(true);
1184         }
1185     }
1186 
cancelCurrentPageLongPress()1187     protected void cancelCurrentPageLongPress() {
1188         // Try canceling the long press. It could also have been scheduled
1189         // by a distant descendant, so use the mAllowLongPress flag to block
1190         // everything
1191         forEachVisiblePage(View::cancelLongPress);
1192     }
1193 
getScrollProgress(int screenCenter, View v, int page)1194     protected float getScrollProgress(int screenCenter, View v, int page) {
1195         final int halfScreenSize = getMeasuredWidth() / 2;
1196         int delta = screenCenter - (getScrollForPage(page) + halfScreenSize);
1197         int panelCount = getPanelCount();
1198         int pageCount = getChildCount();
1199 
1200         int adjacentPage = page + panelCount;
1201         if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) {
1202             adjacentPage = page - panelCount;
1203         }
1204 
1205         final int totalDistance;
1206         if (adjacentPage < 0 || adjacentPage > pageCount - 1) {
1207             totalDistance = (v.getMeasuredWidth() + mPageSpacing) * panelCount;
1208         } else {
1209             totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page));
1210         }
1211 
1212         float scrollProgress = delta / (totalDistance * 1.0f);
1213         scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS);
1214         scrollProgress = Math.max(scrollProgress, -MAX_SCROLL_PROGRESS);
1215         return scrollProgress;
1216     }
1217 
getScrollForPage(int index)1218     public int getScrollForPage(int index) {
1219         if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
1220             return 0;
1221         } else {
1222             return mPageScrolls[index];
1223         }
1224     }
1225 
1226     // While layout transitions are occurring, a child's position may stray from its baseline
1227     // position. This method returns the magnitude of this stray at any given time.
getLayoutTransitionOffsetForPage(int index)1228     public int getLayoutTransitionOffsetForPage(int index) {
1229         if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
1230             return 0;
1231         } else {
1232             View child = getChildAt(index);
1233 
1234             int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft();
1235             int baselineX = mPageScrolls[index] + scrollOffset;
1236             return (int) (child.getX() - baselineX);
1237         }
1238     }
1239 
setEnableFreeScroll(boolean freeScroll)1240     public void setEnableFreeScroll(boolean freeScroll) {
1241         if (mFreeScroll == freeScroll) {
1242             return;
1243         }
1244 
1245         boolean wasFreeScroll = mFreeScroll;
1246         mFreeScroll = freeScroll;
1247 
1248         if (mFreeScroll) {
1249             setCurrentPage(getNextPage());
1250         } else if (wasFreeScroll) {
1251             if (getScrollForPage(getNextPage()) != getScrollX()) {
1252                 snapToPage(getNextPage());
1253             }
1254         }
1255     }
1256 
setEnableOverscroll(boolean enable)1257     protected void setEnableOverscroll(boolean enable) {
1258         mAllowOverScroll = enable;
1259     }
1260 
isSignificantMove(float absoluteDelta, int pageOrientedSize)1261     protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) {
1262         return absoluteDelta > pageOrientedSize * SIGNIFICANT_MOVE_THRESHOLD;
1263     }
1264 
1265     @Override
onTouchEvent(MotionEvent ev)1266     public boolean onTouchEvent(MotionEvent ev) {
1267         // Skip touch handling if there are no pages to swipe
1268         if (getChildCount() <= 0) return false;
1269 
1270         acquireVelocityTrackerAndAddMovement(ev);
1271 
1272         final int action = ev.getAction();
1273 
1274         switch (action & MotionEvent.ACTION_MASK) {
1275         case MotionEvent.ACTION_DOWN:
1276             updateIsBeingDraggedOnTouchDown(ev);
1277 
1278             /*
1279              * If being flinged and user touches, stop the fling. isFinished
1280              * will be false if being flinged.
1281              */
1282             if (!mScroller.isFinished()) {
1283                 abortScrollerAnimation(false);
1284             }
1285 
1286             // Remember where the motion event started
1287             mDownMotionX = ev.getX();
1288             mDownMotionY = ev.getY();
1289             mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, 0);
1290             mLastMotion = (int) mDownMotionPrimary;
1291             mTotalMotion = 0;
1292             mAllowEasyFling = false;
1293             mActivePointerId = ev.getPointerId(0);
1294             if (mIsBeingDragged) {
1295                 pageBeginTransition();
1296             }
1297             break;
1298 
1299         case ACTION_MOVE_ALLOW_EASY_FLING:
1300             // Start scrolling immediately
1301             determineScrollingStart(ev);
1302             mAllowEasyFling = true;
1303             break;
1304 
1305         case MotionEvent.ACTION_MOVE:
1306             if (mIsBeingDragged) {
1307                 // Scroll to follow the motion event
1308                 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1309 
1310                 if (pointerIndex == -1) return true;
1311                 int oldScroll = mOrientationHandler.getPrimaryScroll(this);
1312                 int dx = (int) ev.getX(pointerIndex);
1313                 int dy = (int) ev.getY(pointerIndex);
1314 
1315                 int direction = mOrientationHandler.getPrimaryValue(dx, dy);
1316                 int delta = mLastMotion - direction;
1317 
1318                 int width = getWidth();
1319                 int height = getHeight();
1320                 float size = mOrientationHandler.getPrimaryValue(width, height);
1321                 float displacement = (width == 0 || height == 0) ? 0
1322                         : (float) mOrientationHandler.getSecondaryValue(dx, dy)
1323                                 / mOrientationHandler.getSecondaryValue(width, height);
1324                 mTotalMotion += Math.abs(delta);
1325 
1326                 if (mAllowOverScroll) {
1327                     int consumed = 0;
1328                     if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) {
1329                         consumed = Math.round(size *
1330                                 mEdgeGlowRight.onPullDistance(delta / size, displacement, ev));
1331                     } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) {
1332                         consumed = Math.round(-size *
1333                                 mEdgeGlowLeft.onPullDistance(-delta / size, 1 - displacement, ev));
1334                     }
1335                     delta -= consumed;
1336                 }
1337                 delta /= mOrientationHandler.getPrimaryScale(this);
1338 
1339                 // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
1340                 // keep the remainder because we are actually testing if we've moved from the last
1341                 // scrolled position (which is discrete).
1342                 mLastMotion = direction;
1343 
1344                 if (delta != 0) {
1345                     mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, delta);
1346 
1347                     if (mAllowOverScroll) {
1348                         final float pulledToX = oldScroll + delta;
1349 
1350                         if (pulledToX < mMinScroll) {
1351                             mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement, ev);
1352                             if (!mEdgeGlowRight.isFinished()) {
1353                                 mEdgeGlowRight.onRelease(ev);
1354                             }
1355                         } else if (pulledToX > mMaxScroll) {
1356                             mEdgeGlowRight.onPullDistance(delta / size, displacement, ev);
1357                             if (!mEdgeGlowLeft.isFinished()) {
1358                                 mEdgeGlowLeft.onRelease(ev);
1359                             }
1360                         }
1361 
1362                         if (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()) {
1363                             postInvalidateOnAnimation();
1364                         }
1365                     }
1366                 } else {
1367                     awakenScrollBars();
1368                 }
1369             } else {
1370                 determineScrollingStart(ev);
1371             }
1372             break;
1373 
1374         case MotionEvent.ACTION_UP:
1375             if (mIsBeingDragged) {
1376                 final int activePointerId = mActivePointerId;
1377                 final int pointerIndex = ev.findPointerIndex(activePointerId);
1378                 if (pointerIndex == -1) return true;
1379 
1380                 final float primaryDirection = mOrientationHandler.getPrimaryDirection(ev,
1381                     pointerIndex);
1382                 final VelocityTracker velocityTracker = mVelocityTracker;
1383                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1384 
1385                 int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker,
1386                         mActivePointerId);
1387                 float delta = primaryDirection - mDownMotionPrimary;
1388 
1389                 View current = getPageAt(mCurrentPage);
1390                 if (current == null) {
1391                     Log.e(TAG, "current page was null. this should not happen.");
1392                     return true;
1393                 }
1394 
1395                 int pageOrientedSize = (int) (mOrientationHandler.getMeasuredSize(current)
1396                         * mOrientationHandler.getPrimaryScale(this));
1397                 boolean isSignificantMove = isSignificantMove(Math.abs(delta), pageOrientedSize);
1398 
1399                 mTotalMotion += Math.abs(mLastMotion - primaryDirection);
1400                 boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop;
1401                 boolean isFling = passedSlop && shouldFlingForVelocity(velocity);
1402                 boolean isDeltaLeft = mIsRtl ? delta > 0 : delta < 0;
1403                 boolean isVelocityLeft = mIsRtl ? velocity > 0 : velocity < 0;
1404                 if (DEBUG_FAILED_QUICKSWITCH && !isFling && mAllowEasyFling) {
1405                     Log.d("Quickswitch", "isFling=false vel=" + velocity
1406                             + " threshold=" + mEasyFlingThresholdVelocity);
1407                 }
1408 
1409                 if (!mFreeScroll) {
1410                     // In the case that the page is moved far to one direction and then is flung
1411                     // in the opposite direction, we use a threshold to determine whether we should
1412                     // just return to the starting page, or if we should skip one further.
1413                     boolean returnToOriginalPage = false;
1414                     if (Math.abs(delta) > pageOrientedSize * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
1415                             Math.signum(velocity) != Math.signum(delta) && isFling) {
1416                         returnToOriginalPage = true;
1417                     }
1418 
1419                     int finalPage;
1420                     // We give flings precedence over large moves, which is why we short-circuit our
1421                     // test for a large move if a fling has been registered. That is, a large
1422                     // move to the left and fling to the right will register as a fling to the right.
1423 
1424                     if (((isSignificantMove && !isDeltaLeft && !isFling) ||
1425                             (isFling && !isVelocityLeft)) && mCurrentPage > 0) {
1426                         finalPage = returnToOriginalPage
1427                                 ? mCurrentPage : mCurrentPage - getPanelCount();
1428                         runOnPageScrollsInitialized(
1429                                 () -> snapToPageWithVelocity(finalPage, velocity));
1430                     } else if (((isSignificantMove && isDeltaLeft && !isFling) ||
1431                             (isFling && isVelocityLeft)) &&
1432                             mCurrentPage < getChildCount() - 1) {
1433                         finalPage = returnToOriginalPage
1434                                 ? mCurrentPage : mCurrentPage + getPanelCount();
1435                         runOnPageScrollsInitialized(
1436                                 () -> snapToPageWithVelocity(finalPage, velocity));
1437                     } else {
1438                         runOnPageScrollsInitialized(this::snapToDestination);
1439                     }
1440                 } else {
1441                     if (!mScroller.isFinished()) {
1442                         abortScrollerAnimation(true);
1443                     }
1444 
1445                     int initialScroll = mOrientationHandler.getPrimaryScroll(this);
1446                     int maxScroll = mMaxScroll;
1447                     int minScroll = mMinScroll;
1448 
1449                     if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
1450                         ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
1451                         mScroller.springBack(initialScroll, 0, minScroll, maxScroll, 0, 0);
1452                         mNextPage = getDestinationPage();
1453                     } else {
1454                         int velocity1 = -velocity;
1455                         // Continue a scroll or fling in progress
1456                         mScroller.fling(initialScroll, 0, velocity1, 0, minScroll, maxScroll, 0, 0,
1457                                 Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR), 0);
1458 
1459                         int finalPos = mScroller.getFinalX();
1460                         mNextPage = getDestinationPage(finalPos);
1461                         runOnPageScrollsInitialized(this::onNotSnappingToPageInFreeScroll);
1462                     }
1463                     invalidate();
1464                 }
1465                 mEdgeGlowLeft.onFlingVelocity(velocity);
1466                 mEdgeGlowRight.onFlingVelocity(velocity);
1467             }
1468 
1469             // Detect if user tries to swipe to -1 page but gets disallowed by checking if there was
1470             // left-over values in mEdgeGlowLeft (or mEdgeGlowRight in RLT).
1471             final int layoutDir = getLayoutDirection();
1472             if ((mEdgeGlowLeft.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_LTR)
1473                     || (mEdgeGlowRight.getDistance() > 0 && layoutDir == LAYOUT_DIRECTION_RTL)) {
1474                 onDisallowSwipeToMinusOnePage();
1475             }
1476 
1477             mEdgeGlowLeft.onRelease(ev);
1478             mEdgeGlowRight.onRelease(ev);
1479             // End any intermediate reordering states
1480             resetTouchState();
1481             break;
1482 
1483         case MotionEvent.ACTION_CANCEL:
1484             if (mIsBeingDragged) {
1485                 runOnPageScrollsInitialized(this::snapToDestination);
1486             }
1487             mEdgeGlowLeft.onRelease(ev);
1488             mEdgeGlowRight.onRelease(ev);
1489             resetTouchState();
1490             break;
1491 
1492         case MotionEvent.ACTION_POINTER_UP:
1493             onSecondaryPointerUp(ev);
1494             releaseVelocityTracker();
1495             break;
1496         }
1497 
1498         return true;
1499     }
1500 
onDisallowSwipeToMinusOnePage()1501     protected void onDisallowSwipeToMinusOnePage() {}
1502 
onNotSnappingToPageInFreeScroll()1503     protected void onNotSnappingToPageInFreeScroll() { }
1504 
1505     /**
1506      * Called when the view edges absorb part of the scroll. Subclasses can override this
1507      * to provide custom behavior during animation.
1508      */
onEdgeAbsorbingScroll()1509     protected void onEdgeAbsorbingScroll() {
1510     }
1511 
1512     /**
1513      * Called when the current page closest to the center of the screen changes as part of the
1514      * scroll. Subclasses can override this to provide custom behavior during scroll.
1515      */
onScrollOverPageChanged()1516     protected void onScrollOverPageChanged() {
1517     }
1518 
shouldFlingForVelocity(int velocity)1519     protected boolean shouldFlingForVelocity(int velocity) {
1520         float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
1521         return Math.abs(velocity) > threshold;
1522     }
1523 
resetTouchState()1524     protected void resetTouchState() {
1525         releaseVelocityTracker();
1526         mIsBeingDragged = false;
1527         mActivePointerId = INVALID_POINTER;
1528     }
1529 
1530     @Override
onGenericMotionEvent(MotionEvent event)1531     public boolean onGenericMotionEvent(MotionEvent event) {
1532         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
1533             switch (event.getAction()) {
1534                 case MotionEvent.ACTION_SCROLL: {
1535                     // Handle mouse (or ext. device) by shifting the page depending on the scroll
1536                     final float vscroll;
1537                     final float hscroll;
1538                     if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
1539                         vscroll = 0;
1540                         hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1541                     } else {
1542                         vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1543                         hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
1544                     }
1545                     if (!canScroll(Math.abs(vscroll), Math.abs(hscroll))) {
1546                         return false;
1547                     }
1548                     if (hscroll != 0 || vscroll != 0) {
1549                         boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0)
1550                                                          : (hscroll > 0 || vscroll > 0);
1551                         if (isForwardScroll) {
1552                             scrollRight();
1553                         } else {
1554                             scrollLeft();
1555                         }
1556                         return true;
1557                     }
1558                 }
1559             }
1560         }
1561         return super.onGenericMotionEvent(event);
1562     }
1563 
1564     /**
1565      * Returns true if the paged view can scroll for the provided vertical and horizontal
1566      * scroll values
1567      */
canScroll(float absVScroll, float absHScroll)1568     protected boolean canScroll(float absVScroll, float absHScroll) {
1569         ActivityContext ac = ActivityContext.lookupContext(getContext());
1570         return (ac == null || AbstractFloatingView.getTopOpenView(ac) == null);
1571     }
1572 
acquireVelocityTrackerAndAddMovement(MotionEvent ev)1573     private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
1574         if (mVelocityTracker == null) {
1575             mVelocityTracker = VelocityTracker.obtain();
1576         }
1577         mVelocityTracker.addMovement(ev);
1578     }
1579 
releaseVelocityTracker()1580     private void releaseVelocityTracker() {
1581         if (mVelocityTracker != null) {
1582             mVelocityTracker.clear();
1583             mVelocityTracker.recycle();
1584             mVelocityTracker = null;
1585         }
1586     }
1587 
onSecondaryPointerUp(MotionEvent ev)1588     private void onSecondaryPointerUp(MotionEvent ev) {
1589         final int pointerIndex = ev.getActionIndex();
1590         final int pointerId = ev.getPointerId(pointerIndex);
1591         if (pointerId == mActivePointerId) {
1592             // This was our active pointer going up. Choose a new
1593             // active pointer and adjust accordingly.
1594             // TODO: Make this decision more intelligent.
1595             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1596             mDownMotionPrimary = mOrientationHandler.getPrimaryDirection(ev, newPointerIndex);
1597             mLastMotion = (int) mDownMotionPrimary;
1598             mActivePointerId = ev.getPointerId(newPointerIndex);
1599             if (mVelocityTracker != null) {
1600                 mVelocityTracker.clear();
1601             }
1602         }
1603     }
1604 
1605     @Override
requestChildFocus(View child, View focused)1606     public void requestChildFocus(View child, View focused) {
1607         super.requestChildFocus(child, focused);
1608         if (!shouldHandleRequestChildFocus(child)) {
1609             return;
1610         }
1611         // In case the device is controlled by a controller, mCurrentPage isn't updated properly
1612         // which results in incorrect navigation
1613         int nextPage = getNextPage();
1614         if (nextPage != mCurrentPage) {
1615             setCurrentPage(nextPage);
1616         }
1617 
1618         int page = indexOfChild(child);
1619         if (page >= 0 && !isVisible(page) && !isInTouchMode()) {
1620             snapToPage(page);
1621         }
1622     }
1623 
shouldHandleRequestChildFocus(View child)1624     protected boolean shouldHandleRequestChildFocus(View child) {
1625         return true;
1626     }
1627 
getDestinationPage()1628     public int getDestinationPage() {
1629         return getDestinationPage(mOrientationHandler.getPrimaryScroll(this));
1630     }
1631 
getDestinationPage(int primaryScroll)1632     protected int getDestinationPage(int primaryScroll) {
1633         return getPageNearestToCenterOfScreen(primaryScroll);
1634     }
1635 
getPageNearestToCenterOfScreen()1636     public int getPageNearestToCenterOfScreen() {
1637         return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
1638     }
1639 
getPageNearestToCenterOfScreen(int primaryScroll)1640     private int getPageNearestToCenterOfScreen(int primaryScroll) {
1641         int screenCenter = getScreenCenter(primaryScroll);
1642         int minDistanceFromScreenCenter = Integer.MAX_VALUE;
1643         int minDistanceFromScreenCenterIndex = -1;
1644         final int childCount = getChildCount();
1645         for (int i = 0; i < childCount; ++i) {
1646             int distanceFromScreenCenter = Math.abs(
1647                     getDisplacementFromScreenCenter(i, screenCenter));
1648             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
1649                 minDistanceFromScreenCenter = distanceFromScreenCenter;
1650                 minDistanceFromScreenCenterIndex = i;
1651             }
1652         }
1653         return minDistanceFromScreenCenterIndex;
1654     }
1655 
getDisplacementFromScreenCenter(int childIndex, int screenCenter)1656     private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
1657         int childSize = getChildVisibleSize(childIndex);
1658         int halfChildSize = (childSize / 2);
1659         int childCenter = getChildOffset(childIndex) + halfChildSize;
1660         return childCenter - screenCenter;
1661     }
1662 
getDisplacementFromScreenCenter(int childIndex)1663     protected int getDisplacementFromScreenCenter(int childIndex) {
1664         int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
1665         int screenCenter = getScreenCenter(primaryScroll);
1666         return getDisplacementFromScreenCenter(childIndex, screenCenter);
1667     }
1668 
getScreenCenter(int primaryScroll)1669     protected int getScreenCenter(int primaryScroll) {
1670         float primaryScale = mOrientationHandler.getPrimaryScale(this);
1671         float primaryPivot =  mOrientationHandler.getPrimaryValue(getPivotX(), getPivotY());
1672         int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
1673         return Math.round(primaryScroll + (pageOrientationSize / 2f - primaryPivot) / primaryScale
1674                 + primaryPivot);
1675     }
1676 
snapToDestination()1677     protected void snapToDestination() {
1678         snapToPage(getDestinationPage(), getSnapAnimationDuration());
1679     }
1680 
1681     // We want the duration of the page snap animation to be influenced by the distance that
1682     // the screen has to travel, however, we don't want this duration to be effected in a
1683     // purely linear fashion. Instead, we use this method to moderate the effect that the distance
1684     // of travel has on the overall snap duration.
distanceInfluenceForSnapDuration(float f)1685     private float distanceInfluenceForSnapDuration(float f) {
1686         f -= 0.5f; // center the values about 0.
1687         f *= 0.3f * Math.PI / 2.0f;
1688         return (float) Math.sin(f);
1689     }
1690 
snapToPageWithVelocity(int whichPage, int velocity)1691     protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
1692         whichPage = validateNewPage(whichPage);
1693         int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
1694 
1695         final int newLoc = getScrollForPage(whichPage);
1696         int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);
1697         int duration = 0;
1698 
1699         if (Math.abs(velocity) < mMinFlingVelocity) {
1700             // If the velocity is low enough, then treat this more as an automatic page advance
1701             // as opposed to an apparent physical response to flinging
1702             return snapToPage(whichPage, getSnapAnimationDuration());
1703         }
1704 
1705         // Here we compute a "distance" that will be used in the computation of the overall
1706         // snap duration. This is a function of the actual distance that needs to be traveled;
1707         // we keep this value close to half screen size in order to reduce the variance in snap
1708         // duration as a function of the distance the page needs to travel.
1709         float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize));
1710         float distance = halfScreenSize + halfScreenSize *
1711                 distanceInfluenceForSnapDuration(distanceRatio);
1712 
1713         velocity = Math.abs(velocity);
1714         velocity = Math.max(mMinSnapVelocity, velocity);
1715 
1716         // we want the page's snap velocity to approximately match the velocity at which the
1717         // user flings, so we scale the duration by a value near to the derivative of the scroll
1718         // interpolator at zero, ie. 5. We use 4 to make it a little slower.
1719         duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
1720 
1721         return snapToPage(whichPage, delta, duration);
1722     }
1723 
getSnapAnimationDuration()1724     protected int getSnapAnimationDuration() {
1725         return mPageSnapAnimationDuration;
1726     }
1727 
snapToPage(int whichPage)1728     public boolean snapToPage(int whichPage) {
1729         return snapToPage(whichPage, getSnapAnimationDuration());
1730     }
1731 
snapToPageImmediately(int whichPage)1732     public boolean snapToPageImmediately(int whichPage) {
1733         return snapToPage(whichPage, getSnapAnimationDuration(), true);
1734     }
1735 
snapToPage(int whichPage, int duration)1736     public boolean snapToPage(int whichPage, int duration) {
1737         return snapToPage(whichPage, duration, false);
1738     }
1739 
snapToPage(int whichPage, int duration, boolean immediate)1740     protected boolean snapToPage(int whichPage, int duration, boolean immediate) {
1741         whichPage = validateNewPage(whichPage);
1742 
1743         int newLoc = getScrollForPage(whichPage);
1744         final int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);
1745         return snapToPage(whichPage, delta, duration, immediate);
1746     }
1747 
snapToPage(int whichPage, int delta, int duration)1748     protected boolean snapToPage(int whichPage, int delta, int duration) {
1749         return snapToPage(whichPage, delta, duration, false);
1750     }
1751 
snapToPage(int whichPage, int delta, int duration, boolean immediate)1752     protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate) {
1753         if (mFirstLayout) {
1754             setCurrentPage(whichPage);
1755             return false;
1756         }
1757 
1758         if (FeatureFlags.IS_STUDIO_BUILD && !Utilities.isRunningInTestHarness()) {
1759             duration *= RemoveAnimationSettingsTracker.INSTANCE.get(getContext()).getValue(
1760                     WINDOW_ANIMATION_SCALE_URI);
1761         }
1762 
1763         whichPage = validateNewPage(whichPage);
1764 
1765         mNextPage = whichPage;
1766 
1767         awakenScrollBars(duration);
1768         if (immediate) {
1769             duration = 0;
1770         } else if (duration == 0) {
1771             duration = Math.abs(delta);
1772         }
1773 
1774         if (duration != 0) {
1775             pageBeginTransition();
1776         }
1777 
1778         if (!mScroller.isFinished()) {
1779             abortScrollerAnimation(false);
1780         }
1781 
1782         mScroller.startScroll(mOrientationHandler.getPrimaryScroll(this), 0, delta, 0, duration);
1783         updatePageIndicator();
1784 
1785         // Trigger a compute() to finish switching pages if necessary
1786         if (immediate) {
1787             computeScroll();
1788             pageEndTransition();
1789         }
1790 
1791         invalidate();
1792         return Math.abs(delta) > 0;
1793     }
1794 
scrollLeft()1795     public boolean scrollLeft() {
1796         if (getNextPage() > 0) {
1797             snapToPage(getNextPage() - getPanelCount());
1798             return true;
1799         }
1800         return mAllowOverScroll;
1801     }
1802 
scrollRight()1803     public boolean scrollRight() {
1804         if (getNextPage() < getChildCount() - 1) {
1805             snapToPage(getNextPage() + getPanelCount());
1806             return true;
1807         }
1808         return mAllowOverScroll;
1809     }
1810 
1811     @Override
onScrollChanged(int l, int t, int oldl, int oldt)1812     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
1813         if (mScroller.isFinished()) {
1814             // This was not caused by the scroller, skip it.
1815             return;
1816         }
1817         int newDestinationPage = getDestinationPage();
1818         if (newDestinationPage >= 0 && newDestinationPage != mCurrentScrollOverPage) {
1819             mCurrentScrollOverPage = newDestinationPage;
1820             onScrollOverPageChanged();
1821         }
1822     }
1823 
1824     @Override
getAccessibilityClassName()1825     public CharSequence getAccessibilityClassName() {
1826         // Some accessibility services have special logic for ScrollView. Since we provide same
1827         // accessibility info as ScrollView, inform the service to handle use the same way.
1828         return ScrollView.class.getName();
1829     }
1830 
isPageOrderFlipped()1831     protected boolean isPageOrderFlipped() {
1832         return false;
1833     }
1834 
1835     /* Accessibility */
1836     @SuppressWarnings("deprecation")
1837     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1838     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1839         super.onInitializeAccessibilityNodeInfo(info);
1840         final boolean pagesFlipped = isPageOrderFlipped();
1841         info.setScrollable(getPageCount() > 0);
1842         int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
1843         if (getCurrentPage() < getPageCount() - getPanelCount()
1844                 || (getCurrentPage() == getPageCount() - getPanelCount()
1845                 && primaryScroll != getScrollForPage(getPageCount() - getPanelCount()))) {
1846             info.addAction(pagesFlipped ?
1847                     AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD
1848                     : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
1849             info.addAction(mIsRtl ?
1850                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT
1851                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
1852         }
1853         if (getCurrentPage() > 0
1854                 || (getCurrentPage() == 0 && primaryScroll != getScrollForPage(0))) {
1855             info.addAction(pagesFlipped ?
1856                     AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD
1857                     : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
1858             info.addAction(mIsRtl ?
1859                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT
1860                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
1861         }
1862         // Accessibility-wise, PagedView doesn't support long click, so disabling it.
1863         // Besides disabling the accessibility long-click, this also prevents this view from getting
1864         // accessibility focus.
1865         info.setLongClickable(false);
1866         info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
1867     }
1868 
1869     @Override
sendAccessibilityEvent(int eventType)1870     public void sendAccessibilityEvent(int eventType) {
1871         // Don't let the view send real scroll events.
1872         if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
1873             super.sendAccessibilityEvent(eventType);
1874         }
1875     }
1876 
1877     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)1878     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1879         super.onInitializeAccessibilityEvent(event);
1880         event.setScrollable(mAllowOverScroll || getPageCount() > 1);
1881     }
1882 
1883     @Override
performAccessibilityAction(int action, Bundle arguments)1884     public boolean performAccessibilityAction(int action, Bundle arguments) {
1885         if (super.performAccessibilityAction(action, arguments)) {
1886             return true;
1887         }
1888         final boolean pagesFlipped = isPageOrderFlipped();
1889         switch (action) {
1890             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1891                 if (pagesFlipped ? scrollLeft() : scrollRight()) {
1892                     return true;
1893                 }
1894             } break;
1895             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1896                 if (pagesFlipped ? scrollRight() : scrollLeft()) {
1897                     return true;
1898                 }
1899             } break;
1900             case android.R.id.accessibilityActionPageRight: {
1901                 if (!mIsRtl) {
1902                     return scrollRight();
1903                 } else {
1904                     return scrollLeft();
1905                 }
1906             }
1907             case android.R.id.accessibilityActionPageLeft: {
1908                 if (!mIsRtl) {
1909                     return scrollLeft();
1910                 } else {
1911                     return scrollRight();
1912                 }
1913             }
1914         }
1915         return false;
1916     }
1917 
canAnnouncePageDescription()1918     protected boolean canAnnouncePageDescription() {
1919         return true;
1920     }
1921 
getCurrentPageDescription()1922     protected String getCurrentPageDescription() {
1923         return getContext().getString(R.string.default_scroll_format,
1924                 getNextPage() + 1, getChildCount());
1925     }
1926 
getDownMotionX()1927     protected float getDownMotionX() {
1928         return mDownMotionX;
1929     }
1930 
getDownMotionY()1931     protected float getDownMotionY() {
1932         return mDownMotionY;
1933     }
1934 
1935     protected interface ComputePageScrollsLogic {
1936 
shouldIncludeView(View view)1937         boolean shouldIncludeView(View view);
1938     }
1939 
getVisibleChildrenRange()1940     public int[] getVisibleChildrenRange() {
1941         float visibleLeft = 0;
1942         float visibleRight = visibleLeft + getMeasuredWidth();
1943         float scaleX = getScaleX();
1944         if (scaleX < 1 && scaleX > 0) {
1945             float mid = getMeasuredWidth() / 2;
1946             visibleLeft = mid - ((mid - visibleLeft) / scaleX);
1947             visibleRight = mid + ((visibleRight - mid) / scaleX);
1948         }
1949 
1950         int leftChild = -1;
1951         int rightChild = -1;
1952         final int childCount = getChildCount();
1953         for (int i = 0; i < childCount; i++) {
1954             final View child = getPageAt(i);
1955 
1956             float left = child.getLeft() + child.getTranslationX() - getScrollX();
1957             if (left <= visibleRight && (left + child.getMeasuredWidth()) >= visibleLeft) {
1958                 if (leftChild == -1) {
1959                     leftChild = i;
1960                 }
1961                 rightChild = i;
1962             }
1963         }
1964         mTmpIntPair[0] = leftChild;
1965         mTmpIntPair[1] = rightChild;
1966         return mTmpIntPair;
1967     }
1968 
1969     @Override
draw(Canvas canvas)1970     public void draw(Canvas canvas) {
1971         super.draw(canvas);
1972         drawEdgeEffect(canvas);
1973         pageEndTransition();
1974     }
1975 
drawEdgeEffect(Canvas canvas)1976     protected void drawEdgeEffect(Canvas canvas) {
1977         if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) {
1978             final int width = getWidth();
1979             final int height = getHeight();
1980             if (!mEdgeGlowLeft.isFinished()) {
1981                 final int restoreCount = canvas.save();
1982                 canvas.rotate(-90);
1983                 canvas.translate(-height, Math.min(mMinScroll, getScrollX()));
1984                 mEdgeGlowLeft.setSize(height, width);
1985                 if (mEdgeGlowLeft.draw(canvas)) {
1986                     postInvalidateOnAnimation();
1987                 }
1988                 canvas.restoreToCount(restoreCount);
1989             }
1990             if (!mEdgeGlowRight.isFinished()) {
1991                 final int restoreCount = canvas.save();
1992                 canvas.rotate(90, width, 0);
1993                 canvas.translate(width, -(Math.max(mMaxScroll, getScrollX())));
1994 
1995                 mEdgeGlowRight.setSize(height, width);
1996                 if (mEdgeGlowRight.draw(canvas)) {
1997                     postInvalidateOnAnimation();
1998                 }
1999                 canvas.restoreToCount(restoreCount);
2000             }
2001         }
2002     }
2003 }
2004