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