PagedView.java revision d6523da1ad7bae805a3dbea8d7b398d86cd3c181
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
17package com.android.internal.policy.impl.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.PropertyValuesHolder;
24import android.animation.TimeInterpolator;
25import android.animation.ValueAnimator;
26import android.animation.ValueAnimator.AnimatorUpdateListener;
27import android.content.Context;
28import android.content.res.TypedArray;
29import android.graphics.Canvas;
30import android.graphics.Matrix;
31import android.graphics.PointF;
32import android.graphics.Rect;
33import android.os.Bundle;
34import android.os.Parcel;
35import android.os.Parcelable;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.InputDevice;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.VelocityTracker;
42import android.view.View;
43import android.view.ViewConfiguration;
44import android.view.ViewGroup;
45import android.view.ViewParent;
46import android.view.View.MeasureSpec;
47import android.view.ViewGroup.LayoutParams;
48import android.view.accessibility.AccessibilityEvent;
49import android.view.accessibility.AccessibilityManager;
50import android.view.accessibility.AccessibilityNodeInfo;
51import android.view.animation.AnimationUtils;
52import android.view.animation.DecelerateInterpolator;
53import android.view.animation.Interpolator;
54import android.widget.Scroller;
55
56import com.android.internal.R;
57
58import java.util.ArrayList;
59
60/**
61 * An abstraction of the original Workspace which supports browsing through a
62 * sequential list of "pages"
63 */
64public class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener {
65    private static final String TAG = "WidgetPagedView";
66    private static final boolean DEBUG = false;
67    protected static final int INVALID_PAGE = -1;
68
69    // the min drag distance for a fling to register, to prevent random page shifts
70    private static final int MIN_LENGTH_FOR_FLING = 25;
71
72    protected static final int PAGE_SNAP_ANIMATION_DURATION = 750;
73    protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950;
74    protected static final float NANOTIME_DIV = 1000000000.0f;
75
76    private static final float OVERSCROLL_ACCELERATE_FACTOR = 2;
77    private static final float OVERSCROLL_DAMP_FACTOR = 0.14f;
78
79    private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f;
80    // The page is moved more than halfway, automatically move to the next page on touch up.
81    private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f;
82
83    // The following constants need to be scaled based on density. The scaled versions will be
84    // assigned to the corresponding member variables below.
85    private static final int FLING_THRESHOLD_VELOCITY = 500;
86    private static final int MIN_SNAP_VELOCITY = 1500;
87    private static final int MIN_FLING_VELOCITY = 250;
88
89    // We are disabling touch interaction of the widget region for factory ROM.
90    private static final boolean DISABLE_TOUCH_INTERACTION = false;
91    private static final boolean DISABLE_TOUCH_SIDE_PAGES = true;
92    private static final boolean DISABLE_FLING_TO_DELETE = false;
93
94    static final int AUTOMATIC_PAGE_SPACING = -1;
95
96    protected int mFlingThresholdVelocity;
97    protected int mMinFlingVelocity;
98    protected int mMinSnapVelocity;
99
100    protected float mDensity;
101    protected float mSmoothingTime;
102    protected float mTouchX;
103
104    protected boolean mFirstLayout = true;
105
106    protected int mCurrentPage;
107    protected int mChildCountOnLastMeasure;
108
109    protected int mNextPage = INVALID_PAGE;
110    protected int mMaxScrollX;
111    protected Scroller mScroller;
112    private VelocityTracker mVelocityTracker;
113
114    private float mParentDownMotionX;
115    private float mParentDownMotionY;
116    private float mDownMotionX;
117    private float mDownMotionY;
118    private float mDownScrollX;
119    protected float mLastMotionX;
120    protected float mLastMotionXRemainder;
121    protected float mLastMotionY;
122    protected float mTotalMotionX;
123    private int mLastScreenCenter = -1;
124    private int[] mChildOffsets;
125    private int[] mChildRelativeOffsets;
126    private int[] mChildOffsetsWithLayoutScale;
127
128    protected final static int TOUCH_STATE_REST = 0;
129    protected final static int TOUCH_STATE_SCROLLING = 1;
130    protected final static int TOUCH_STATE_PREV_PAGE = 2;
131    protected final static int TOUCH_STATE_NEXT_PAGE = 3;
132    protected final static int TOUCH_STATE_REORDERING = 4;
133
134    protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f;
135
136    protected int mTouchState = TOUCH_STATE_REST;
137    protected boolean mForceScreenScrolled = false;
138
139    protected OnLongClickListener mLongClickListener;
140
141    protected boolean mAllowLongPress = true;
142
143    protected int mTouchSlop;
144    private int mPagingTouchSlop;
145    private int mMaximumVelocity;
146    private int mMinimumWidth;
147    protected int mPageSpacing;
148    protected int mCellCountX = 0;
149    protected int mCellCountY = 0;
150    protected boolean mAllowOverScroll = true;
151    protected int mUnboundedScrollX;
152    protected int[] mTempVisiblePagesRange = new int[2];
153    protected boolean mForceDrawAllChildrenNextFrame;
154
155    // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise
156    // it is equal to the scaled overscroll position. We use a separate value so as to prevent
157    // the screens from continuing to translate beyond the normal bounds.
158    protected int mOverScrollX;
159
160    // parameter that adjusts the layout to be optimized for pages with that scale factor
161    protected float mLayoutScale = 1.0f;
162
163    protected static final int INVALID_POINTER = -1;
164
165    protected int mActivePointerId = INVALID_POINTER;
166
167    private PageSwitchListener mPageSwitchListener;
168
169    protected ArrayList<Boolean> mDirtyPageContent;
170
171    // If true, syncPages and syncPageItems will be called to refresh pages
172    protected boolean mContentIsRefreshable = true;
173
174    // If true, modify alpha of neighboring pages as user scrolls left/right
175    protected boolean mFadeInAdjacentScreens = false;
176
177    // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding
178    // to switch to a new page
179    protected boolean mUsePagingTouchSlop = true;
180
181    // If true, the subclass should directly update scrollX itself in its computeScroll method
182    // (SmoothPagedView does this)
183    protected boolean mDeferScrollUpdate = false;
184
185    protected boolean mIsPageMoving = false;
186
187    // All syncs and layout passes are deferred until data is ready.
188    protected boolean mIsDataReady = true;
189
190    // Scrolling indicator
191    private ValueAnimator mScrollIndicatorAnimator;
192    private View mScrollIndicator;
193    private int mScrollIndicatorPaddingLeft;
194    private int mScrollIndicatorPaddingRight;
195    private boolean mShouldShowScrollIndicator = false;
196    private boolean mShouldShowScrollIndicatorImmediately = false;
197    protected static final int sScrollIndicatorFadeInDuration = 150;
198    protected static final int sScrollIndicatorFadeOutDuration = 650;
199    protected static final int sScrollIndicatorFlashDuration = 650;
200
201    // Reordering
202    // We use the min scale to determine how much to expand the actually PagedView measured
203    // dimensions such that when we are zoomed out, the view is not clipped
204    private int REORDERING_DROP_REPOSITION_DURATION = 200;
205    private int REORDERING_REORDER_REPOSITION_DURATION = 350;
206    private int REORDERING_ZOOM_IN_OUT_DURATION = 250;
207    private int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 500;
208    private float REORDERING_SIDE_PAGE_BUFFER_PERCENTAGE = 0.1f;
209    private float mMinScale = 1f;
210    protected View mDragView;
211    private AnimatorSet mZoomInOutAnim;
212    private Runnable mSidePageHoverRunnable;
213    private int mSidePageHoverIndex = -1;
214
215    // Edge swiping
216    private boolean mOnlyAllowEdgeSwipes = false;
217    private boolean mDownEventOnEdge = false;
218    private int mEdgeSwipeRegionSize = 0;
219
220    // Convenience/caching
221    private Matrix mTmpInvMatrix = new Matrix();
222    private float[] mTmpPoint = new float[2];
223
224    // Fling to delete
225    private int FLING_TO_DELETE_FADE_OUT_DURATION = 350;
226    private float FLING_TO_DELETE_FRICTION = 0.035f;
227    // The degrees specifies how much deviation from the up vector to still consider a fling "up"
228    private float FLING_TO_DELETE_MAX_FLING_DEGREES = 35f;
229    protected int mFlingToDeleteThresholdVelocity = -1400; // TEMPORARY
230    private boolean mIsFlingingToDelete = false;
231
232    public interface PageSwitchListener {
233        void onPageSwitch(View newPage, int newPageIndex);
234    }
235
236    public PagedView(Context context) {
237        this(context, null);
238    }
239
240    public PagedView(Context context, AttributeSet attrs) {
241        this(context, attrs, 0);
242    }
243
244    public PagedView(Context context, AttributeSet attrs, int defStyle) {
245        super(context, attrs, defStyle);
246        TypedArray a = context.obtainStyledAttributes(attrs,
247                R.styleable.PagedView, defStyle, 0);
248        setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0));
249        mScrollIndicatorPaddingLeft =
250            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0);
251        mScrollIndicatorPaddingRight =
252            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0);
253        mEdgeSwipeRegionSize =
254                a.getDimensionPixelSize(R.styleable.PagedView_edgeSwipeRegionSize, 0);
255        a.recycle();
256
257        setHapticFeedbackEnabled(false);
258        init();
259    }
260
261    /**
262     * Initializes various states for this workspace.
263     */
264    protected void init() {
265        mDirtyPageContent = new ArrayList<Boolean>();
266        mDirtyPageContent.ensureCapacity(32);
267        mScroller = new Scroller(getContext(), new ScrollInterpolator());
268        mCurrentPage = 0;
269
270        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
271        mTouchSlop = configuration.getScaledTouchSlop();
272        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
273        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
274        mDensity = getResources().getDisplayMetrics().density;
275
276        mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
277        mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity);
278        mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity);
279        setOnHierarchyChangeListener(this);
280    }
281
282    // Convenience methods to map points from self to parent and vice versa
283    float[] mapPointFromSelfToParent(float x, float y) {
284        mTmpPoint[0] = x;
285        mTmpPoint[1] = y;
286        getMatrix().mapPoints(mTmpPoint);
287        mTmpPoint[0] += getLeft();
288        mTmpPoint[1] += getTop();
289        return mTmpPoint;
290    }
291    float[] mapPointFromParentToSelf(float x, float y) {
292        mTmpPoint[0] = x - getLeft();
293        mTmpPoint[1] = y - getTop();
294        getMatrix().invert(mTmpInvMatrix);
295        mTmpInvMatrix.mapPoints(mTmpPoint);
296        return mTmpPoint;
297    }
298
299    void updateDragViewTranslationDuringDrag() {
300        float x = mLastMotionX - mDownMotionX + getScrollX() - mDownScrollX;
301        float y = mLastMotionY - mDownMotionY;
302        mDragView.setTranslationX(x);
303        mDragView.setTranslationY(y);
304
305        if (DEBUG) Log.d(TAG, "PagedView.updateDragViewTranslationDuringDrag(): " + x + ", " + y);
306    }
307
308    public void setMinScale(float f) {
309        mMinScale = f;
310        requestLayout();
311    }
312
313    @Override
314    public void setScaleX(float scaleX) {
315        super.setScaleX(scaleX);
316        if (mTouchState == TOUCH_STATE_REORDERING) {
317            float[] p = mapPointFromParentToSelf(mParentDownMotionX, mParentDownMotionY);
318            mLastMotionX = p[0];
319            mLastMotionY = p[1];
320            updateDragViewTranslationDuringDrag();
321        }
322    }
323
324    // Convenience methods to get the actual width/height of the PagedView (since it is measured
325    // to be larger to account for the minimum possible scale)
326    int getMinScaledMeasuredWidth() {
327        return (int) (getMeasuredWidth() * mMinScale);
328    }
329    int getMinScaledMeasuredHeight() {
330        return (int) (getMeasuredHeight() * mMinScale);
331    }
332
333    // Convenience methods to get the offset ASSUMING that we are centering the pages in the
334    // PagedView both horizontally and vertically
335    int getOffsetX() {
336        return (getMeasuredWidth() - getMinScaledMeasuredWidth()) / 2;
337    }
338    int getOffsetY() {
339        return (getMeasuredHeight() - getMinScaledMeasuredHeight()) / 2;
340    }
341
342    public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
343        mPageSwitchListener = pageSwitchListener;
344        if (mPageSwitchListener != null) {
345            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
346        }
347    }
348
349    /**
350     * Called by subclasses to mark that data is ready, and that we can begin loading and laying
351     * out pages.
352     */
353    protected void setDataIsReady() {
354        mIsDataReady = true;
355    }
356
357    protected boolean isDataReady() {
358        return mIsDataReady;
359    }
360
361    /**
362     * Returns the index of the currently displayed page.
363     *
364     * @return The index of the currently displayed page.
365     */
366    int getCurrentPage() {
367        return mCurrentPage;
368    }
369
370    int getNextPage() {
371        return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
372    }
373
374    int getPageCount() {
375        return getChildCount();
376    }
377
378    View getPageAt(int index) {
379        return getChildAt(index);
380    }
381
382    protected int indexToPage(int index) {
383        return index;
384    }
385
386    /**
387     * Updates the scroll of the current page immediately to its final scroll position.  We use this
388     * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of
389     * the previous tab page.
390     */
391    protected void updateCurrentPageScroll() {
392        int offset = getChildOffset(mCurrentPage);
393        int relOffset = getRelativeChildOffset(mCurrentPage);
394        int newX = offset - relOffset;
395        scrollTo(newX, 0);
396        mScroller.setFinalX(newX);
397        mScroller.forceFinished(true);
398    }
399
400    /**
401     * Sets the current page.
402     */
403    void setCurrentPage(int currentPage) {
404        if (!mScroller.isFinished()) {
405            mScroller.abortAnimation();
406        }
407        // don't introduce any checks like mCurrentPage == currentPage here-- if we change the
408        // the default
409        if (getChildCount() == 0) {
410            return;
411        }
412
413        mForceScreenScrolled = true;
414        mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
415        updateCurrentPageScroll();
416        updateScrollingIndicator();
417        notifyPageSwitchListener();
418        invalidate();
419    }
420
421    public void setOnlyAllowEdgeSwipes(boolean enable) {
422        mOnlyAllowEdgeSwipes = enable;
423    }
424
425    protected void notifyPageSwitchListener() {
426        if (mPageSwitchListener != null) {
427            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
428        }
429    }
430
431    protected void pageBeginMoving() {
432        if (!mIsPageMoving) {
433            mIsPageMoving = true;
434            onPageBeginMoving();
435        }
436    }
437
438    protected void pageEndMoving() {
439        if (mIsPageMoving) {
440            mIsPageMoving = false;
441            onPageEndMoving();
442        }
443    }
444
445    protected boolean isPageMoving() {
446        return mIsPageMoving;
447    }
448
449    // a method that subclasses can override to add behavior
450    protected void onPageBeginMoving() {
451    }
452
453    // a method that subclasses can override to add behavior
454    protected void onPageEndMoving() {
455    }
456
457    /**
458     * Registers the specified listener on each page contained in this workspace.
459     *
460     * @param l The listener used to respond to long clicks.
461     */
462    @Override
463    public void setOnLongClickListener(OnLongClickListener l) {
464        mLongClickListener = l;
465        final int count = getPageCount();
466        for (int i = 0; i < count; i++) {
467            getPageAt(i).setOnLongClickListener(l);
468        }
469    }
470
471    @Override
472    public void scrollBy(int x, int y) {
473        scrollTo(mUnboundedScrollX + x, getScrollY() + y);
474    }
475
476    @Override
477    public void scrollTo(int x, int y) {
478        mUnboundedScrollX = x;
479
480        if (x < 0) {
481            super.scrollTo(0, y);
482            if (mAllowOverScroll) {
483                overScroll(x);
484            }
485        } else if (x > mMaxScrollX) {
486            super.scrollTo(mMaxScrollX, y);
487            if (mAllowOverScroll) {
488                overScroll(x - mMaxScrollX);
489            }
490        } else {
491            mOverScrollX = x;
492            super.scrollTo(x, y);
493        }
494
495        mTouchX = x;
496        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
497
498        // Update the last motion events when scrolling
499        if (mTouchState == TOUCH_STATE_REORDERING) {
500            float[] p = mapPointFromParentToSelf(mParentDownMotionX, mParentDownMotionY);
501            mLastMotionX = p[0];
502            mLastMotionY = p[1];
503            updateDragViewTranslationDuringDrag();
504        }
505    }
506
507    // we moved this functionality to a helper function so SmoothPagedView can reuse it
508    protected boolean computeScrollHelper() {
509        if (mScroller.computeScrollOffset()) {
510            // Don't bother scrolling if the page does not need to be moved
511            if (getScrollX() != mScroller.getCurrX()
512                || getScrollY() != mScroller.getCurrY()
513                || mOverScrollX != mScroller.getCurrX()) {
514                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
515            }
516            invalidate();
517            return true;
518        } else if (mNextPage != INVALID_PAGE) {
519            mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
520            mNextPage = INVALID_PAGE;
521            notifyPageSwitchListener();
522
523            // We don't want to trigger a page end moving unless the page has settled
524            // and the user has stopped scrolling
525            if (mTouchState == TOUCH_STATE_REST) {
526                pageEndMoving();
527            }
528
529            // Notify the user when the page changes
530            AccessibilityManager accessibilityManager = (AccessibilityManager)
531                    getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
532            if (accessibilityManager.isEnabled()) {
533                AccessibilityEvent ev =
534                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
535                ev.getText().add(getCurrentPageDescription());
536                sendAccessibilityEventUnchecked(ev);
537            }
538            return true;
539        }
540        return false;
541    }
542
543    public String getCurrentPageDescription() {
544        return "";
545    }
546
547    @Override
548    public void computeScroll() {
549        computeScrollHelper();
550    }
551
552    @Override
553    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
554        if (!mIsDataReady || getChildCount() == 0) {
555            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
556            return;
557        }
558
559        // We measure the dimensions of the PagedView to be larger than the pages so that when we
560        // zoom out (and scale down), the view is still contained in the parent
561        int scaledWidthSize = (int) (MeasureSpec.getSize(widthMeasureSpec) / mMinScale);
562        int scaledHeightSize = (int) (MeasureSpec.getSize(heightMeasureSpec) / mMinScale);
563        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
564        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
565        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
566        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
567
568        if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
569            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
570            return;
571        }
572
573        // Return early if we aren't given a proper dimension
574        if (widthSize <= 0 || heightSize <= 0) {
575            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
576            return;
577        }
578
579        /* Allow the height to be set as WRAP_CONTENT. This allows the particular case
580         * of the All apps view on XLarge displays to not take up more space then it needs. Width
581         * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect
582         * each page to have the same width.
583         */
584        final int verticalPadding = getPaddingTop() + getPaddingBottom();
585        final int horizontalPadding = getPaddingLeft() + getPaddingRight();
586
587        // The children are given the same width and height as the workspace
588        // unless they were set to WRAP_CONTENT
589        if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize);
590        if (DEBUG) Log.d(TAG, "PagedView.horizontalPadding: " + horizontalPadding);
591        final int childCount = getChildCount();
592        for (int i = 0; i < childCount; i++) {
593            // disallowing padding in paged view (just pass 0)
594            final View child = getPageAt(i);
595            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
596
597            int childWidthMode;
598            if (lp.width == LayoutParams.WRAP_CONTENT) {
599                childWidthMode = MeasureSpec.AT_MOST;
600            } else {
601                childWidthMode = MeasureSpec.EXACTLY;
602            }
603
604            int childHeightMode;
605            if (lp.height == LayoutParams.WRAP_CONTENT) {
606                childHeightMode = MeasureSpec.AT_MOST;
607            } else {
608                childHeightMode = MeasureSpec.EXACTLY;
609            }
610
611            final int childWidthMeasureSpec =
612                MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode);
613            final int childHeightMeasureSpec =
614                MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode);
615
616            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
617        }
618        setMeasuredDimension(scaledWidthSize, scaledHeightSize);
619
620        // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions.
621        // We also wait until we set the measured dimensions before flushing the cache as well, to
622        // ensure that the cache is filled with good values.
623        invalidateCachedOffsets();
624
625        if (mChildCountOnLastMeasure != getChildCount()) {
626            setCurrentPage(mCurrentPage);
627        }
628        mChildCountOnLastMeasure = getChildCount();
629
630        if (childCount > 0) {
631            if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMinScaledMeasuredWidth() + ", "
632                    + getChildWidth(0));
633
634            // Calculate the variable page spacing if necessary
635            if (mPageSpacing == AUTOMATIC_PAGE_SPACING) {
636                // The gap between pages in the PagedView should be equal to the gap from the page
637                // to the edge of the screen (so it is not visible in the current screen).  To
638                // account for unequal padding on each side of the paged view, we take the maximum
639                // of the left/right gap and use that as the gap between each page.
640                int offset = getRelativeChildOffset(0);
641                int spacing = Math.max(offset, widthSize - offset -
642                        getChildAt(0).getMeasuredWidth());
643                setPageSpacing(spacing);
644            }
645        }
646
647        updateScrollingIndicatorPosition();
648
649        if (childCount > 0) {
650            mMaxScrollX = getChildOffset(childCount - 1) - getRelativeChildOffset(childCount - 1);
651        } else {
652            mMaxScrollX = 0;
653        }
654    }
655
656    public void setPageSpacing(int pageSpacing) {
657        mPageSpacing = pageSpacing;
658        invalidateCachedOffsets();
659    }
660
661    @Override
662    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
663        if (!mIsDataReady || getChildCount() == 0) {
664            return;
665        }
666
667        if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
668        final int childCount = getChildCount();
669
670        int scaleOffsetX = getOffsetX();
671        int scaleOffsetY = getOffsetY();
672
673        int childLeft = scaleOffsetX + getRelativeChildOffset(0);
674        for (int i = 0; i < childCount; i++) {
675            final View child = getPageAt(i);
676            int childTop = scaleOffsetY + getPaddingTop();
677            if (child.getVisibility() != View.GONE) {
678                final int childWidth = getScaledMeasuredWidth(child);
679                final int childHeight = child.getMeasuredHeight();
680
681                if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop);
682                child.layout(childLeft, childTop,
683                        childLeft + child.getMeasuredWidth(), childTop + childHeight);
684                childLeft += childWidth + mPageSpacing;
685            }
686        }
687
688        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
689            setHorizontalScrollBarEnabled(false);
690            updateCurrentPageScroll();
691            setHorizontalScrollBarEnabled(true);
692            mFirstLayout = false;
693        }
694    }
695
696    protected void screenScrolled(int screenCenter) {
697        if (isScrollingIndicatorEnabled()) {
698            updateScrollingIndicator();
699        }
700        boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX;
701
702        if (mFadeInAdjacentScreens && !isInOverscroll) {
703            for (int i = 0; i < getChildCount(); i++) {
704                View child = getChildAt(i);
705                if (child != null) {
706                    float scrollProgress = getScrollProgress(screenCenter, child, i);
707                    float alpha = 1 - Math.abs(scrollProgress);
708                    child.setAlpha(alpha);
709                }
710            }
711            invalidate();
712        }
713    }
714
715    @Override
716    public void onChildViewAdded(View parent, View child) {
717        // This ensures that when children are added, they get the correct transforms / alphas
718        // in accordance with any scroll effects.
719        mForceScreenScrolled = true;
720        invalidate();
721        invalidateCachedOffsets();
722    }
723
724    @Override
725    public void onChildViewRemoved(View parent, View child) {
726        mForceScreenScrolled = true;
727    }
728
729    protected void invalidateCachedOffsets() {
730        int count = getChildCount();
731        if (count == 0) {
732            mChildOffsets = null;
733            mChildRelativeOffsets = null;
734            mChildOffsetsWithLayoutScale = null;
735            return;
736        }
737
738        mChildOffsets = new int[count];
739        mChildRelativeOffsets = new int[count];
740        mChildOffsetsWithLayoutScale = new int[count];
741        for (int i = 0; i < count; i++) {
742            mChildOffsets[i] = -1;
743            mChildRelativeOffsets[i] = -1;
744            mChildOffsetsWithLayoutScale[i] = -1;
745        }
746    }
747
748    protected int getChildOffset(int index) {
749        if (index < 0 || index > getChildCount() - 1) return 0;
750
751        int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ?
752                mChildOffsets : mChildOffsetsWithLayoutScale;
753
754        if (childOffsets != null && childOffsets[index] != -1) {
755            return childOffsets[index];
756        } else {
757            if (getChildCount() == 0)
758                return 0;
759
760            int offset = getRelativeChildOffset(0);
761            for (int i = 0; i < index; ++i) {
762                offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing;
763            }
764            if (childOffsets != null) {
765                childOffsets[index] = offset;
766            }
767            return offset;
768        }
769    }
770
771    protected int getRelativeChildOffset(int index) {
772        if (index < 0 || index > getChildCount() - 1) return 0;
773
774        if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) {
775            return mChildRelativeOffsets[index];
776        } else {
777            final int padding = getPaddingLeft() + getPaddingRight();
778            final int offset = getPaddingLeft() +
779                    (getMinScaledMeasuredWidth() - padding - getChildWidth(index)) / 2;
780            if (mChildRelativeOffsets != null) {
781                mChildRelativeOffsets[index] = offset;
782            }
783            return offset;
784        }
785    }
786
787    protected int getScaledMeasuredWidth(View child) {
788        // This functions are called enough times that it actually makes a difference in the
789        // profiler -- so just inline the max() here
790        final int measuredWidth = child.getMeasuredWidth();
791        final int minWidth = mMinimumWidth;
792        final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth;
793        return (int) (maxWidth * mLayoutScale + 0.5f);
794    }
795
796    // TODO: Fix this
797    protected void getVisiblePages(int[] range) {
798        range[0] = 0;
799        range[1] = getPageCount() - 1;
800        /*
801        final int pageCount = getChildCount();
802
803        if (pageCount > 0) {
804            final int screenWidth = getMinScaledMeasuredWidth();
805            int leftScreen = 0;
806            int rightScreen = 0;
807            int offsetX = getOffsetX() + getScrollX();
808            View currPage = getPageAt(leftScreen);
809            while (leftScreen < pageCount - 1 &&
810                    currPage.getX() + currPage.getWidth() -
811                    currPage.getPaddingRight() < offsetX) {
812                leftScreen++;
813                currPage = getPageAt(leftScreen);
814            }
815            rightScreen = leftScreen;
816            currPage = getPageAt(rightScreen + 1);
817            while (rightScreen < pageCount - 1 &&
818                    currPage.getX() - currPage.getPaddingLeft() < offsetX + screenWidth) {
819                rightScreen++;
820                currPage = getPageAt(rightScreen + 1);
821            }
822
823            // TEMP: this is a hacky way to ensure that animations to new pages are not clipped
824            // because we don't draw them while scrolling?
825            range[0] = Math.max(0, leftScreen - 1);
826            range[1] = Math.min(rightScreen + 1, getChildCount() - 1);
827        } else {
828            range[0] = -1;
829            range[1] = -1;
830        }
831        */
832    }
833
834    protected boolean shouldDrawChild(View child) {
835        return child.getAlpha() > 0;
836    }
837
838    @Override
839    protected void dispatchDraw(Canvas canvas) {
840        int halfScreenSize = getMinScaledMeasuredWidth() / 2;
841        // mOverScrollX is equal to getScrollX() when we're within the normal scroll range.
842        // Otherwise it is equal to the scaled overscroll position.
843        int screenCenter = mOverScrollX + halfScreenSize;
844
845        if (screenCenter != mLastScreenCenter || mForceScreenScrolled) {
846            // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can
847            // set it for the next frame
848            mForceScreenScrolled = false;
849            screenScrolled(screenCenter);
850            mLastScreenCenter = screenCenter;
851        }
852
853        // Find out which screens are visible; as an optimization we only call draw on them
854        final int pageCount = getChildCount();
855        if (pageCount > 0) {
856            getVisiblePages(mTempVisiblePagesRange);
857            final int leftScreen = mTempVisiblePagesRange[0];
858            final int rightScreen = mTempVisiblePagesRange[1];
859            if (leftScreen != -1 && rightScreen != -1) {
860                final long drawingTime = getDrawingTime();
861                // Clip to the bounds
862                canvas.save();
863                canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(),
864                        getScrollY() + getBottom() - getTop());
865
866                // Draw all the children, leaving the drag view for last
867                for (int i = pageCount - 1; i >= 0; i--) {
868                    final View v = getPageAt(i);
869                    if (v == mDragView) continue;
870                    if (mForceDrawAllChildrenNextFrame ||
871                               (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) {
872                        drawChild(canvas, v, drawingTime);
873                    }
874                }
875                // Draw the drag view on top (if there is one)
876                if (mDragView != null) {
877                    drawChild(canvas, mDragView, drawingTime);
878                }
879
880                mForceDrawAllChildrenNextFrame = false;
881                canvas.restore();
882            }
883        }
884    }
885
886    @Override
887    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
888        int page = indexToPage(indexOfChild(child));
889        if (page != mCurrentPage || !mScroller.isFinished()) {
890            snapToPage(page);
891            return true;
892        }
893        return false;
894    }
895
896    @Override
897    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
898        int focusablePage;
899        if (mNextPage != INVALID_PAGE) {
900            focusablePage = mNextPage;
901        } else {
902            focusablePage = mCurrentPage;
903        }
904        View v = getPageAt(focusablePage);
905        if (v != null) {
906            return v.requestFocus(direction, previouslyFocusedRect);
907        }
908        return false;
909    }
910
911    @Override
912    public boolean dispatchUnhandledMove(View focused, int direction) {
913        if (direction == View.FOCUS_LEFT) {
914            if (getCurrentPage() > 0) {
915                snapToPage(getCurrentPage() - 1);
916                return true;
917            }
918        } else if (direction == View.FOCUS_RIGHT) {
919            if (getCurrentPage() < getPageCount() - 1) {
920                snapToPage(getCurrentPage() + 1);
921                return true;
922            }
923        }
924        return super.dispatchUnhandledMove(focused, direction);
925    }
926
927    @Override
928    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
929        if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
930            getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode);
931        }
932        if (direction == View.FOCUS_LEFT) {
933            if (mCurrentPage > 0) {
934                getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode);
935            }
936        } else if (direction == View.FOCUS_RIGHT){
937            if (mCurrentPage < getPageCount() - 1) {
938                getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode);
939            }
940        }
941    }
942
943    /**
944     * If one of our descendant views decides that it could be focused now, only
945     * pass that along if it's on the current page.
946     *
947     * This happens when live folders requery, and if they're off page, they
948     * end up calling requestFocus, which pulls it on page.
949     */
950    @Override
951    public void focusableViewAvailable(View focused) {
952        View current = getPageAt(mCurrentPage);
953        View v = focused;
954        while (true) {
955            if (v == current) {
956                super.focusableViewAvailable(focused);
957                return;
958            }
959            if (v == this) {
960                return;
961            }
962            ViewParent parent = v.getParent();
963            if (parent instanceof View) {
964                v = (View)v.getParent();
965            } else {
966                return;
967            }
968        }
969    }
970
971    /**
972     * {@inheritDoc}
973     */
974    @Override
975    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
976        if (disallowIntercept) {
977            // We need to make sure to cancel our long press if
978            // a scrollable widget takes over touch events
979            final View currentPage = getPageAt(mCurrentPage);
980            currentPage.cancelLongPress();
981        }
982        super.requestDisallowInterceptTouchEvent(disallowIntercept);
983    }
984
985    /**
986     * Return true if a tap at (x, y) should trigger a flip to the previous page.
987     */
988    protected boolean hitsPreviousPage(float x, float y) {
989        return (x < getOffsetX() + getRelativeChildOffset(mCurrentPage) - mPageSpacing);
990    }
991
992    /**
993     * Return true if a tap at (x, y) should trigger a flip to the next page.
994     */
995    protected boolean hitsNextPage(float x, float y) {
996        return  (x > (getOffsetX() + getMinScaledMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing));
997    }
998
999    @Override
1000    public boolean onInterceptTouchEvent(MotionEvent ev) {
1001        if (DISABLE_TOUCH_INTERACTION) {
1002            return false;
1003        }
1004
1005        /*
1006         * This method JUST determines whether we want to intercept the motion.
1007         * If we return true, onTouchEvent will be called and we do the actual
1008         * scrolling there.
1009         */
1010        acquireVelocityTrackerAndAddMovement(ev);
1011
1012        // Skip touch handling if there are no pages to swipe
1013        if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev);
1014
1015        /*
1016         * Shortcut the most recurring case: the user is in the dragging
1017         * state and he is moving his finger.  We want to intercept this
1018         * motion.
1019         */
1020        final int action = ev.getAction();
1021        if ((action == MotionEvent.ACTION_MOVE) &&
1022                (mTouchState == TOUCH_STATE_SCROLLING)) {
1023            return true;
1024        }
1025
1026        switch (action & MotionEvent.ACTION_MASK) {
1027            case MotionEvent.ACTION_MOVE: {
1028                /*
1029                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1030                 * whether the user has moved far enough from his original down touch.
1031                 */
1032                if (mActivePointerId != INVALID_POINTER) {
1033                    determineScrollingStart(ev);
1034                    break;
1035                }
1036                // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
1037                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
1038                // i.e. fall through to the next case (don't break)
1039                // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
1040                // while it's small- this was causing a crash before we checked for INVALID_POINTER)
1041            }
1042
1043            case MotionEvent.ACTION_DOWN: {
1044                final float x = ev.getX();
1045                final float y = ev.getY();
1046                // Remember location of down touch
1047                mDownMotionX = x;
1048                mDownMotionY = y;
1049                mDownScrollX = getScrollX();
1050                mLastMotionX = x;
1051                mLastMotionY = y;
1052                float[] p = mapPointFromSelfToParent(x, y);
1053                mParentDownMotionX = p[0];
1054                mParentDownMotionY = p[1];
1055                mLastMotionXRemainder = 0;
1056                mTotalMotionX = 0;
1057                mActivePointerId = ev.getPointerId(0);
1058
1059                // Determine if the down event is within the threshold to be an edge swipe
1060                int leftEdgeBoundary = getOffsetX() + mEdgeSwipeRegionSize;
1061                int rightEdgeBoundary = getMeasuredWidth() - getOffsetX() - mEdgeSwipeRegionSize;
1062                if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) {
1063                    mDownEventOnEdge = true;
1064                }
1065
1066                /*
1067                 * If being flinged and user touches the screen, initiate drag;
1068                 * otherwise don't.  mScroller.isFinished should be false when
1069                 * being flinged.
1070                 */
1071                final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
1072                final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
1073                if (finishedScrolling) {
1074                    mTouchState = TOUCH_STATE_REST;
1075                    mScroller.abortAnimation();
1076                } else {
1077                    mTouchState = TOUCH_STATE_SCROLLING;
1078                }
1079
1080                // check if this can be the beginning of a tap on the side of the pages
1081                // to scroll the current page
1082                if (!DISABLE_TOUCH_SIDE_PAGES) {
1083                    if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) {
1084                        if (getChildCount() > 0) {
1085                            if (hitsPreviousPage(x, y)) {
1086                                mTouchState = TOUCH_STATE_PREV_PAGE;
1087                            } else if (hitsNextPage(x, y)) {
1088                                mTouchState = TOUCH_STATE_NEXT_PAGE;
1089                            }
1090                        }
1091                    }
1092                }
1093                break;
1094            }
1095
1096            case MotionEvent.ACTION_UP:
1097            case MotionEvent.ACTION_CANCEL:
1098                resetTouchState();
1099                break;
1100
1101            case MotionEvent.ACTION_POINTER_UP:
1102                onSecondaryPointerUp(ev);
1103                releaseVelocityTracker();
1104                break;
1105        }
1106
1107        /*
1108         * The only time we want to intercept motion events is if we are in the
1109         * drag mode.
1110         */
1111        return mTouchState != TOUCH_STATE_REST;
1112    }
1113
1114    protected void determineScrollingStart(MotionEvent ev) {
1115        determineScrollingStart(ev, 1.0f);
1116    }
1117
1118    /*
1119     * Determines if we should change the touch state to start scrolling after the
1120     * user moves their touch point too far.
1121     */
1122    protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
1123        /*
1124         * Locally do absolute value. mLastMotionX is set to the y value
1125         * of the down event.
1126         */
1127        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1128
1129        if (pointerIndex == -1) {
1130            return;
1131        }
1132
1133        // If we're only allowing edge swipes, we break out early if the down event wasn't
1134        // at the edge.
1135        if (mOnlyAllowEdgeSwipes && !mDownEventOnEdge) {
1136            return;
1137        }
1138
1139        final float x = ev.getX(pointerIndex);
1140        final float y = ev.getY(pointerIndex);
1141        final int xDiff = (int) Math.abs(x - mLastMotionX);
1142        final int yDiff = (int) Math.abs(y - mLastMotionY);
1143
1144        final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
1145        boolean xPaged = xDiff > mPagingTouchSlop;
1146        boolean xMoved = xDiff > touchSlop;
1147        boolean yMoved = yDiff > touchSlop;
1148
1149        if (xMoved || xPaged || yMoved) {
1150            if (mUsePagingTouchSlop ? xPaged : xMoved) {
1151                // Scroll if the user moved far enough along the X axis
1152                mTouchState = TOUCH_STATE_SCROLLING;
1153                mTotalMotionX += Math.abs(mLastMotionX - x);
1154                mLastMotionX = x;
1155                mLastMotionXRemainder = 0;
1156                mTouchX = getOffsetX() + getScrollX();
1157                mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
1158                pageBeginMoving();
1159            }
1160        }
1161    }
1162
1163    protected float getScrollProgress(int screenCenter, View v, int page) {
1164        final int halfScreenSize = getMinScaledMeasuredWidth() / 2;
1165
1166        int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing;
1167        int delta = screenCenter - (getChildOffset(page) -
1168                getRelativeChildOffset(page) + halfScreenSize);
1169
1170        float scrollProgress = delta / (totalDistance * 1.0f);
1171        scrollProgress = Math.min(scrollProgress, 1.0f);
1172        scrollProgress = Math.max(scrollProgress, -1.0f);
1173        return scrollProgress;
1174    }
1175
1176    // This curve determines how the effect of scrolling over the limits of the page dimishes
1177    // as the user pulls further and further from the bounds
1178    private float overScrollInfluenceCurve(float f) {
1179        f -= 1.0f;
1180        return f * f * f + 1.0f;
1181    }
1182
1183    protected void acceleratedOverScroll(float amount) {
1184        int screenSize = getMinScaledMeasuredWidth();
1185
1186        // We want to reach the max over scroll effect when the user has
1187        // over scrolled half the size of the screen
1188        float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize);
1189
1190        if (f == 0) return;
1191
1192        // Clamp this factor, f, to -1 < f < 1
1193        if (Math.abs(f) >= 1) {
1194            f /= Math.abs(f);
1195        }
1196
1197        int overScrollAmount = (int) Math.round(f * screenSize);
1198        if (amount < 0) {
1199            mOverScrollX = overScrollAmount;
1200            super.scrollTo(0, getScrollY());
1201        } else {
1202            mOverScrollX = mMaxScrollX + overScrollAmount;
1203            super.scrollTo(mMaxScrollX, getScrollY());
1204        }
1205        invalidate();
1206    }
1207
1208    protected void dampedOverScroll(float amount) {
1209        int screenSize = getMinScaledMeasuredWidth();
1210
1211        float f = (amount / screenSize);
1212
1213        if (f == 0) return;
1214        f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
1215
1216        // Clamp this factor, f, to -1 < f < 1
1217        if (Math.abs(f) >= 1) {
1218            f /= Math.abs(f);
1219        }
1220
1221        int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize);
1222        if (amount < 0) {
1223            mOverScrollX = overScrollAmount;
1224            super.scrollTo(0, getScrollY());
1225        } else {
1226            mOverScrollX = mMaxScrollX + overScrollAmount;
1227            super.scrollTo(mMaxScrollX, getScrollY());
1228        }
1229        invalidate();
1230    }
1231
1232    protected void overScroll(float amount) {
1233        dampedOverScroll(amount);
1234    }
1235
1236    protected float maxOverScroll() {
1237        // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not
1238        // exceed). Used to find out how much extra wallpaper we need for the over scroll effect
1239        float f = 1.0f;
1240        f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
1241        return OVERSCROLL_DAMP_FACTOR * f;
1242    }
1243
1244    @Override
1245    public boolean onTouchEvent(MotionEvent ev) {
1246        if (DISABLE_TOUCH_INTERACTION) {
1247            return false;
1248        }
1249
1250        // Skip touch handling if there are no pages to swipe
1251        if (getChildCount() <= 0) return super.onTouchEvent(ev);
1252
1253        acquireVelocityTrackerAndAddMovement(ev);
1254
1255        final int action = ev.getAction();
1256
1257        switch (action & MotionEvent.ACTION_MASK) {
1258        case MotionEvent.ACTION_DOWN:
1259            /*
1260             * If being flinged and user touches, stop the fling. isFinished
1261             * will be false if being flinged.
1262             */
1263            if (!mScroller.isFinished()) {
1264                mScroller.abortAnimation();
1265            }
1266
1267            // Remember where the motion event started
1268            mDownMotionX = mLastMotionX = ev.getX();
1269            mDownMotionY = mLastMotionY = ev.getY();
1270            mDownScrollX = getScrollX();
1271            float[] p = mapPointFromSelfToParent(mLastMotionX, mLastMotionY);
1272            mParentDownMotionX = p[0];
1273            mParentDownMotionY = p[1];
1274            mLastMotionXRemainder = 0;
1275            mTotalMotionX = 0;
1276            mActivePointerId = ev.getPointerId(0);
1277
1278            // Determine if the down event is within the threshold to be an edge swipe
1279            int leftEdgeBoundary = getOffsetX() + mEdgeSwipeRegionSize;
1280            int rightEdgeBoundary = getMeasuredWidth() - getOffsetX() - mEdgeSwipeRegionSize;
1281            if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) {
1282                mDownEventOnEdge = true;
1283            }
1284
1285            if (mTouchState == TOUCH_STATE_SCROLLING) {
1286                pageBeginMoving();
1287            }
1288            break;
1289
1290        case MotionEvent.ACTION_MOVE:
1291            if (mTouchState == TOUCH_STATE_SCROLLING) {
1292                // Scroll to follow the motion event
1293                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1294                final float x = ev.getX(pointerIndex);
1295                final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
1296
1297                mTotalMotionX += Math.abs(deltaX);
1298
1299                // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
1300                // keep the remainder because we are actually testing if we've moved from the last
1301                // scrolled position (which is discrete).
1302                if (Math.abs(deltaX) >= 1.0f) {
1303                    mTouchX += deltaX;
1304                    mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
1305                    if (!mDeferScrollUpdate) {
1306                        scrollBy((int) deltaX, 0);
1307                        if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX);
1308                    } else {
1309                        invalidate();
1310                    }
1311                    mLastMotionX = x;
1312                    mLastMotionXRemainder = deltaX - (int) deltaX;
1313                } else {
1314                    awakenScrollBars();
1315                }
1316            } else if (mTouchState == TOUCH_STATE_REORDERING) {
1317                // Update the last motion position
1318                mLastMotionX = ev.getX();
1319                mLastMotionY = ev.getY();
1320
1321                // Update the parent down so that our zoom animations take this new movement into
1322                // account
1323                float[] pt = mapPointFromSelfToParent(mLastMotionX, mLastMotionY);
1324                mParentDownMotionX = pt[0];
1325                mParentDownMotionY = pt[1];
1326                updateDragViewTranslationDuringDrag();
1327
1328                // Find the closest page to the touch point
1329                final int dragViewIndex = indexOfChild(mDragView);
1330                int bufferSize = (int) (REORDERING_SIDE_PAGE_BUFFER_PERCENTAGE *
1331                    getMinScaledMeasuredWidth());
1332                int leftBufferEdge = (int) mapPointFromSelfToParent(0, 0)[0] + bufferSize;
1333                int rightBufferEdge = (int) mapPointFromSelfToParent(getMeasuredWidth(), 0)[0]
1334                        - bufferSize;
1335                float parentX = mParentDownMotionX;
1336                int pageIndexToSnapTo = -1;
1337                if (parentX < leftBufferEdge && dragViewIndex > 0) {
1338                    pageIndexToSnapTo = dragViewIndex - 1;
1339                } else if (parentX > rightBufferEdge && dragViewIndex < getChildCount() - 1) {
1340                    pageIndexToSnapTo = dragViewIndex + 1;
1341                }
1342
1343                final int pageUnderPointIndex = pageIndexToSnapTo;
1344                if (pageUnderPointIndex > -1) {
1345                    if (pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) {
1346                        mSidePageHoverIndex = pageUnderPointIndex;
1347                        mSidePageHoverRunnable = new Runnable() {
1348                            @Override
1349                            public void run() {
1350                                // Update the down scroll position to account for the fact that the
1351                                // current page is moved
1352                                mDownScrollX = getChildOffset(pageUnderPointIndex)
1353                                        - getRelativeChildOffset(pageUnderPointIndex);
1354
1355                                // Setup the scroll to the correct page before we swap the views
1356                                snapToPage(pageUnderPointIndex);
1357
1358                                // For each of the pages between the paged view and the drag view,
1359                                // animate them from the previous position to the new position in
1360                                // the layout (as a result of the drag view moving in the layout)
1361                                int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1;
1362                                int lowerIndex = (dragViewIndex < pageUnderPointIndex) ?
1363                                        dragViewIndex + 1 : pageUnderPointIndex;
1364                                int upperIndex = (dragViewIndex > pageUnderPointIndex) ?
1365                                        dragViewIndex - 1 : pageUnderPointIndex;
1366                                for (int i = lowerIndex; i <= upperIndex; ++i) {
1367                                    View v = getChildAt(i);
1368                                    // dragViewIndex < pageUnderPointIndex, so after we remove the
1369                                    // drag view all subsequent views to pageUnderPointIndex will
1370                                    // shift down.
1371                                    int oldX = getOffsetX() + getChildOffset(i);
1372                                    int newX = getOffsetX() + getChildOffset(i + shiftDelta);
1373
1374
1375                                    // Animate the view translation from its old position to its new
1376                                    // position
1377                                    AnimatorSet anim = (AnimatorSet) v.getTag();
1378                                    if (anim != null) {
1379                                        anim.cancel();
1380                                    }
1381
1382                                    v.setTranslationX(oldX - newX);
1383                                    anim = new AnimatorSet();
1384                                    anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION);
1385                                    anim.playTogether(
1386                                            ObjectAnimator.ofFloat(v, "translationX", 0f));
1387                                    anim.start();
1388                                    v.setTag(anim);
1389                                }
1390
1391                                removeView(mDragView);
1392                                addView(mDragView, pageUnderPointIndex);
1393                                mSidePageHoverIndex = -1;
1394                            }
1395                        };
1396                        postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT);
1397                    }
1398                } else {
1399                    removeCallbacks(mSidePageHoverRunnable);
1400                    mSidePageHoverIndex = -1;
1401                }
1402            } else {
1403                determineScrollingStart(ev);
1404            }
1405            break;
1406
1407        case MotionEvent.ACTION_UP:
1408            if (mTouchState == TOUCH_STATE_SCROLLING) {
1409                final int activePointerId = mActivePointerId;
1410                final int pointerIndex = ev.findPointerIndex(activePointerId);
1411                final float x = ev.getX(pointerIndex);
1412                final VelocityTracker velocityTracker = mVelocityTracker;
1413                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1414                int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
1415                final int deltaX = (int) (x - mDownMotionX);
1416                final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage));
1417                boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
1418                        SIGNIFICANT_MOVE_THRESHOLD;
1419
1420                mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
1421
1422                boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
1423                        Math.abs(velocityX) > mFlingThresholdVelocity;
1424
1425                // In the case that the page is moved far to one direction and then is flung
1426                // in the opposite direction, we use a threshold to determine whether we should
1427                // just return to the starting page, or if we should skip one further.
1428                boolean returnToOriginalPage = false;
1429                if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
1430                        Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
1431                    returnToOriginalPage = true;
1432                }
1433
1434                int finalPage;
1435                // We give flings precedence over large moves, which is why we short-circuit our
1436                // test for a large move if a fling has been registered. That is, a large
1437                // move to the left and fling to the right will register as a fling to the right.
1438                if (((isSignificantMove && deltaX > 0 && !isFling) ||
1439                        (isFling && velocityX > 0)) && mCurrentPage > 0) {
1440                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
1441                    snapToPageWithVelocity(finalPage, velocityX);
1442                } else if (((isSignificantMove && deltaX < 0 && !isFling) ||
1443                        (isFling && velocityX < 0)) &&
1444                        mCurrentPage < getChildCount() - 1) {
1445                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
1446                    snapToPageWithVelocity(finalPage, velocityX);
1447                } else {
1448                    snapToDestination();
1449                }
1450            } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
1451                // at this point we have not moved beyond the touch slop
1452                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
1453                // we can just page
1454                int nextPage = Math.max(0, mCurrentPage - 1);
1455                if (nextPage != mCurrentPage) {
1456                    snapToPage(nextPage);
1457                } else {
1458                    snapToDestination();
1459                }
1460            } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
1461                // at this point we have not moved beyond the touch slop
1462                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
1463                // we can just page
1464                int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
1465                if (nextPage != mCurrentPage) {
1466                    snapToPage(nextPage);
1467                } else {
1468                    snapToDestination();
1469                }
1470            } else if (mTouchState == TOUCH_STATE_REORDERING) {
1471                if (!DISABLE_FLING_TO_DELETE) {
1472                    // Check the velocity and see if we are flinging-to-delete
1473                    PointF flingToDeleteVector = isFlingingToDelete();
1474                    if (flingToDeleteVector != null) {
1475                        onFlingToDelete(flingToDeleteVector);
1476                    }
1477                }
1478            } else {
1479                onUnhandledTap(ev);
1480            }
1481
1482            // Remove the callback to wait for the side page hover timeout
1483            removeCallbacks(mSidePageHoverRunnable);
1484            // End any intermediate reordering states
1485            resetTouchState();
1486            break;
1487
1488        case MotionEvent.ACTION_CANCEL:
1489            if (mTouchState == TOUCH_STATE_SCROLLING) {
1490                snapToDestination();
1491            }
1492            resetTouchState();
1493            break;
1494
1495        case MotionEvent.ACTION_POINTER_UP:
1496            onSecondaryPointerUp(ev);
1497            break;
1498        }
1499
1500        return true;
1501    }
1502
1503    private void resetTouchState() {
1504        releaseVelocityTracker();
1505        endReordering();
1506        mTouchState = TOUCH_STATE_REST;
1507        mActivePointerId = INVALID_POINTER;
1508        mDownEventOnEdge = false;
1509    }
1510
1511    protected void onUnhandledTap(MotionEvent ev) {}
1512
1513    @Override
1514    public boolean onGenericMotionEvent(MotionEvent event) {
1515        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
1516            switch (event.getAction()) {
1517                case MotionEvent.ACTION_SCROLL: {
1518                    // Handle mouse (or ext. device) by shifting the page depending on the scroll
1519                    final float vscroll;
1520                    final float hscroll;
1521                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
1522                        vscroll = 0;
1523                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1524                    } else {
1525                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1526                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
1527                    }
1528                    if (hscroll != 0 || vscroll != 0) {
1529                        if (hscroll > 0 || vscroll > 0) {
1530                            scrollRight();
1531                        } else {
1532                            scrollLeft();
1533                        }
1534                        return true;
1535                    }
1536                }
1537            }
1538        }
1539        return super.onGenericMotionEvent(event);
1540    }
1541
1542    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
1543        if (mVelocityTracker == null) {
1544            mVelocityTracker = VelocityTracker.obtain();
1545        }
1546        mVelocityTracker.addMovement(ev);
1547    }
1548
1549    private void releaseVelocityTracker() {
1550        if (mVelocityTracker != null) {
1551            mVelocityTracker.recycle();
1552            mVelocityTracker = null;
1553        }
1554    }
1555
1556    private void onSecondaryPointerUp(MotionEvent ev) {
1557        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
1558                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
1559        final int pointerId = ev.getPointerId(pointerIndex);
1560        if (pointerId == mActivePointerId) {
1561            // This was our active pointer going up. Choose a new
1562            // active pointer and adjust accordingly.
1563            // TODO: Make this decision more intelligent.
1564            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1565            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
1566            mLastMotionY = ev.getY(newPointerIndex);
1567            mLastMotionXRemainder = 0;
1568            mActivePointerId = ev.getPointerId(newPointerIndex);
1569            if (mVelocityTracker != null) {
1570                mVelocityTracker.clear();
1571            }
1572        }
1573    }
1574
1575    @Override
1576    public void requestChildFocus(View child, View focused) {
1577        super.requestChildFocus(child, focused);
1578        int page = indexToPage(indexOfChild(child));
1579        if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) {
1580            snapToPage(page);
1581        }
1582    }
1583
1584    protected int getChildIndexForRelativeOffset(int relativeOffset) {
1585        final int childCount = getChildCount();
1586        int left;
1587        int right;
1588        for (int i = 0; i < childCount; ++i) {
1589            left = getRelativeChildOffset(i);
1590            right = (left + getScaledMeasuredWidth(getPageAt(i)));
1591            if (left <= relativeOffset && relativeOffset <= right) {
1592                return i;
1593            }
1594        }
1595        return -1;
1596    }
1597
1598    protected int getChildWidth(int index) {
1599        // This functions are called enough times that it actually makes a difference in the
1600        // profiler -- so just inline the max() here
1601        final int measuredWidth = getPageAt(index).getMeasuredWidth();
1602        final int minWidth = mMinimumWidth;
1603        return (minWidth > measuredWidth) ? minWidth : measuredWidth;
1604    }
1605
1606    int getPageNearestToPoint(float x) {
1607        int index = 0;
1608        for (int i = 0; i < getChildCount(); ++i) {
1609            if (x < getChildAt(i).getRight() - getScrollX()) {
1610                return index;
1611            } else {
1612                index++;
1613            }
1614        }
1615        return Math.min(index, getChildCount() - 1);
1616    }
1617
1618    int getPageNearestToCenterOfScreen() {
1619        int minDistanceFromScreenCenter = Integer.MAX_VALUE;
1620        int minDistanceFromScreenCenterIndex = -1;
1621        int screenCenter = getOffsetX() + getScrollX() + (getMinScaledMeasuredWidth() / 2);
1622        final int childCount = getChildCount();
1623        for (int i = 0; i < childCount; ++i) {
1624            View layout = (View) getPageAt(i);
1625            int childWidth = getScaledMeasuredWidth(layout);
1626            int halfChildWidth = (childWidth / 2);
1627            int childCenter = getOffsetX() + getChildOffset(i) + halfChildWidth;
1628            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
1629            if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
1630                minDistanceFromScreenCenter = distanceFromScreenCenter;
1631                minDistanceFromScreenCenterIndex = i;
1632            }
1633        }
1634        return minDistanceFromScreenCenterIndex;
1635    }
1636
1637    protected void snapToDestination() {
1638        snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION);
1639    }
1640
1641    private static class ScrollInterpolator implements Interpolator {
1642        public ScrollInterpolator() {
1643        }
1644
1645        public float getInterpolation(float t) {
1646            t -= 1.0f;
1647            return t*t*t*t*t + 1;
1648        }
1649    }
1650
1651    // We want the duration of the page snap animation to be influenced by the distance that
1652    // the screen has to travel, however, we don't want this duration to be effected in a
1653    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
1654    // of travel has on the overall snap duration.
1655    float distanceInfluenceForSnapDuration(float f) {
1656        f -= 0.5f; // center the values about 0.
1657        f *= 0.3f * Math.PI / 2.0f;
1658        return (float) Math.sin(f);
1659    }
1660
1661    protected void snapToPageWithVelocity(int whichPage, int velocity) {
1662        whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1));
1663        int halfScreenSize = getMinScaledMeasuredWidth() / 2;
1664
1665        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
1666        if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): "
1667                + getMinScaledMeasuredWidth() + ", " + getChildWidth(whichPage));
1668        final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
1669        int delta = newX - mUnboundedScrollX;
1670        int duration = 0;
1671
1672        if (Math.abs(velocity) < mMinFlingVelocity) {
1673            // If the velocity is low enough, then treat this more as an automatic page advance
1674            // as opposed to an apparent physical response to flinging
1675            snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
1676            return;
1677        }
1678
1679        // Here we compute a "distance" that will be used in the computation of the overall
1680        // snap duration. This is a function of the actual distance that needs to be traveled;
1681        // we keep this value close to half screen size in order to reduce the variance in snap
1682        // duration as a function of the distance the page needs to travel.
1683        float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize));
1684        float distance = halfScreenSize + halfScreenSize *
1685                distanceInfluenceForSnapDuration(distanceRatio);
1686
1687        velocity = Math.abs(velocity);
1688        velocity = Math.max(mMinSnapVelocity, velocity);
1689
1690        // we want the page's snap velocity to approximately match the velocity at which the
1691        // user flings, so we scale the duration by a value near to the derivative of the scroll
1692        // interpolator at zero, ie. 5. We use 4 to make it a little slower.
1693        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
1694
1695        snapToPage(whichPage, delta, duration);
1696    }
1697
1698    protected void snapToPage(int whichPage) {
1699        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
1700    }
1701
1702    protected void snapToPage(int whichPage, int duration) {
1703        whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
1704
1705        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
1706        if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMinScaledMeasuredWidth() + ", "
1707                + getChildWidth(whichPage));
1708        int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
1709        final int sX = mUnboundedScrollX;
1710        final int delta = newX - sX;
1711        snapToPage(whichPage, delta, duration);
1712    }
1713
1714    protected void snapToPage(int whichPage, int delta, int duration) {
1715        mNextPage = whichPage;
1716
1717        View focusedChild = getFocusedChild();
1718        if (focusedChild != null && whichPage != mCurrentPage &&
1719                focusedChild == getPageAt(mCurrentPage)) {
1720            focusedChild.clearFocus();
1721        }
1722
1723        pageBeginMoving();
1724        awakenScrollBars(duration);
1725        if (duration == 0) {
1726            duration = Math.abs(delta);
1727        }
1728
1729        if (!mScroller.isFinished()) mScroller.abortAnimation();
1730        mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);
1731
1732        notifyPageSwitchListener();
1733        invalidate();
1734    }
1735
1736    public void scrollLeft() {
1737        if (mScroller.isFinished()) {
1738            if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
1739        } else {
1740            if (mNextPage > 0) snapToPage(mNextPage - 1);
1741        }
1742    }
1743
1744    public void scrollRight() {
1745        if (mScroller.isFinished()) {
1746            if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
1747        } else {
1748            if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
1749        }
1750    }
1751
1752    public int getPageForView(View v) {
1753        int result = -1;
1754        if (v != null) {
1755            ViewParent vp = v.getParent();
1756            int count = getChildCount();
1757            for (int i = 0; i < count; i++) {
1758                if (vp == getPageAt(i)) {
1759                    return i;
1760                }
1761            }
1762        }
1763        return result;
1764    }
1765
1766    public static class SavedState extends BaseSavedState {
1767        int currentPage = -1;
1768
1769        SavedState(Parcelable superState) {
1770            super(superState);
1771        }
1772
1773        private SavedState(Parcel in) {
1774            super(in);
1775            currentPage = in.readInt();
1776        }
1777
1778        @Override
1779        public void writeToParcel(Parcel out, int flags) {
1780            super.writeToParcel(out, flags);
1781            out.writeInt(currentPage);
1782        }
1783
1784        public static final Parcelable.Creator<SavedState> CREATOR =
1785                new Parcelable.Creator<SavedState>() {
1786            public SavedState createFromParcel(Parcel in) {
1787                return new SavedState(in);
1788            }
1789
1790            public SavedState[] newArray(int size) {
1791                return new SavedState[size];
1792            }
1793        };
1794    }
1795
1796    protected View getScrollingIndicator() {
1797        return null;
1798    }
1799
1800    protected boolean isScrollingIndicatorEnabled() {
1801        return false;
1802    }
1803
1804    Runnable hideScrollingIndicatorRunnable = new Runnable() {
1805        @Override
1806        public void run() {
1807            hideScrollingIndicator(false);
1808        }
1809    };
1810
1811    protected void flashScrollingIndicator(boolean animated) {
1812        removeCallbacks(hideScrollingIndicatorRunnable);
1813        showScrollingIndicator(!animated);
1814        postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration);
1815    }
1816
1817    protected void showScrollingIndicator(boolean immediately) {
1818        mShouldShowScrollIndicator = true;
1819        mShouldShowScrollIndicatorImmediately = true;
1820        if (getChildCount() <= 1) return;
1821        if (!isScrollingIndicatorEnabled()) return;
1822
1823        mShouldShowScrollIndicator = false;
1824        getScrollingIndicator();
1825        if (mScrollIndicator != null) {
1826            // Fade the indicator in
1827            updateScrollingIndicatorPosition();
1828            mScrollIndicator.setVisibility(View.VISIBLE);
1829            cancelScrollingIndicatorAnimations();
1830            if (immediately) {
1831                mScrollIndicator.setAlpha(1f);
1832            } else {
1833                mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 1f);
1834                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration);
1835                mScrollIndicatorAnimator.start();
1836            }
1837        }
1838    }
1839
1840    protected void cancelScrollingIndicatorAnimations() {
1841        if (mScrollIndicatorAnimator != null) {
1842            mScrollIndicatorAnimator.cancel();
1843        }
1844    }
1845
1846    protected void hideScrollingIndicator(boolean immediately) {
1847        if (getChildCount() <= 1) return;
1848        if (!isScrollingIndicatorEnabled()) return;
1849
1850        getScrollingIndicator();
1851        if (mScrollIndicator != null) {
1852            // Fade the indicator out
1853            updateScrollingIndicatorPosition();
1854            cancelScrollingIndicatorAnimations();
1855            if (immediately) {
1856                mScrollIndicator.setVisibility(View.INVISIBLE);
1857                mScrollIndicator.setAlpha(0f);
1858            } else {
1859                mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 0f);
1860                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration);
1861                mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() {
1862                    private boolean cancelled = false;
1863                    @Override
1864                    public void onAnimationCancel(android.animation.Animator animation) {
1865                        cancelled = true;
1866                    }
1867                    @Override
1868                    public void onAnimationEnd(Animator animation) {
1869                        if (!cancelled) {
1870                            mScrollIndicator.setVisibility(View.INVISIBLE);
1871                        }
1872                    }
1873                });
1874                mScrollIndicatorAnimator.start();
1875            }
1876        }
1877    }
1878
1879    /**
1880     * To be overridden by subclasses to determine whether the scroll indicator should stretch to
1881     * fill its space on the track or not.
1882     */
1883    protected boolean hasElasticScrollIndicator() {
1884        return true;
1885    }
1886
1887    private void updateScrollingIndicator() {
1888        if (getChildCount() <= 1) return;
1889        if (!isScrollingIndicatorEnabled()) return;
1890
1891        getScrollingIndicator();
1892        if (mScrollIndicator != null) {
1893            updateScrollingIndicatorPosition();
1894        }
1895        if (mShouldShowScrollIndicator) {
1896            showScrollingIndicator(mShouldShowScrollIndicatorImmediately);
1897        }
1898    }
1899
1900    private void updateScrollingIndicatorPosition() {
1901        if (!isScrollingIndicatorEnabled()) return;
1902        if (mScrollIndicator == null) return;
1903        int numPages = getChildCount();
1904        int pageWidth = getMinScaledMeasuredWidth();
1905        int lastChildIndex = Math.max(0, getChildCount() - 1);
1906        int maxScrollX = getChildOffset(lastChildIndex) - getRelativeChildOffset(lastChildIndex);
1907        int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight;
1908        int indicatorWidth = mScrollIndicator.getMeasuredWidth() -
1909                mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight();
1910
1911        float offset = Math.max(0f, Math.min(1f, (float) getScrollX() / maxScrollX));
1912        int indicatorSpace = trackWidth / numPages;
1913        int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft;
1914        if (hasElasticScrollIndicator()) {
1915            if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) {
1916                mScrollIndicator.getLayoutParams().width = indicatorSpace;
1917                mScrollIndicator.requestLayout();
1918            }
1919        } else {
1920            int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2;
1921            indicatorPos += indicatorCenterOffset;
1922        }
1923        mScrollIndicator.setTranslationX(indicatorPos);
1924    }
1925
1926    // Animate the drag view back to the original position
1927    void animateChildrenToOriginalPosition() {
1928        if (mDragView != null) {
1929            AnimatorSet anim = new AnimatorSet();
1930            anim.setDuration(REORDERING_DROP_REPOSITION_DURATION);
1931            anim.playTogether(
1932                    ObjectAnimator.ofFloat(mDragView, "translationX", 0f),
1933                    ObjectAnimator.ofFloat(mDragView, "translationY", 0f));
1934            anim.start();
1935        }
1936    }
1937
1938    // "Zooms out" the PagedView to reveal more side pages
1939    boolean zoomOut() {
1940        if (!(getScaleX() < 1f || getScaleY() < 1f)) {
1941            if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) {
1942                mZoomInOutAnim.cancel();
1943            }
1944
1945            mZoomInOutAnim = new AnimatorSet();
1946            mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION);
1947            mZoomInOutAnim.playTogether(
1948                    ObjectAnimator.ofFloat(this, "scaleX", mMinScale),
1949                    ObjectAnimator.ofFloat(this, "scaleY", mMinScale));
1950            mZoomInOutAnim.start();
1951            return true;
1952        }
1953        return false;
1954    }
1955
1956    protected void onStartReordering() {
1957    }
1958
1959    protected void onEndReordering() {
1960    }
1961
1962    public void startReordering() {
1963        if (zoomOut()) {
1964            onStartReordering();
1965
1966            // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.)
1967            mTouchState = TOUCH_STATE_REORDERING;
1968            // Find the drag view under the pointer
1969            mDragView = getChildAt(getPageNearestToCenterOfScreen());
1970
1971            // We must invalidate to trigger a redraw to update the layers such that the drag view
1972            // is always drawn on top
1973            invalidate();
1974        }
1975    }
1976
1977    void endReordering() {
1978        Runnable onCompleteRunnable = new Runnable() {
1979            @Override
1980            public void run() {
1981                onEndReordering();
1982            }
1983        };
1984        if (zoomIn(onCompleteRunnable)) {
1985            // Snap to the current page
1986            snapToDestination();
1987        }
1988        // If we haven't flung-to-delete the current child, then we just animate the drag view
1989        // back into position
1990        if (!mIsFlingingToDelete) {
1991            animateChildrenToOriginalPosition();
1992        }
1993    }
1994
1995    public boolean isReordering() {
1996        return mTouchState == TOUCH_STATE_REORDERING;
1997    }
1998
1999    // "Zooms in" the PagedView to highlight the current page
2000    boolean zoomIn(final Runnable onCompleteRunnable) {
2001        if (getScaleX() < 1f || getScaleY() < 1f) {
2002            if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) {
2003                mZoomInOutAnim.cancel();
2004            }
2005
2006            mZoomInOutAnim = new AnimatorSet();
2007            mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION);
2008            mZoomInOutAnim.playTogether(
2009                    ObjectAnimator.ofFloat(this, "scaleX", 1f),
2010                    ObjectAnimator.ofFloat(this, "scaleY", 1f));
2011            mZoomInOutAnim.addListener(new AnimatorListenerAdapter() {
2012                @Override
2013                public void onAnimationCancel(Animator animation) {
2014                    mDragView = null;
2015                }
2016                @Override
2017                public void onAnimationEnd(Animator animation) {
2018                    mDragView = null;
2019                    if (onCompleteRunnable != null) {
2020                        onCompleteRunnable.run();
2021                    }
2022                }
2023            });
2024            mZoomInOutAnim.start();
2025            return true;
2026        }
2027        return false;
2028    }
2029
2030    /*
2031     * Flinging to delete - IN PROGRESS
2032     */
2033    private PointF isFlingingToDelete() {
2034        ViewConfiguration config = ViewConfiguration.get(getContext());
2035        mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity());
2036
2037        if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) {
2038            // Do a quick dot product test to ensure that we are flinging upwards
2039            PointF vel = new PointF(mVelocityTracker.getXVelocity(),
2040                    mVelocityTracker.getYVelocity());
2041            PointF upVec = new PointF(0f, -1f);
2042            float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) /
2043                    (vel.length() * upVec.length()));
2044            if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) {
2045                return vel;
2046            }
2047        }
2048        return null;
2049    }
2050
2051    /**
2052     * Creates an animation from the current drag view along its current velocity vector.
2053     * For this animation, the alpha runs for a fixed duration and we update the position
2054     * progressively.
2055     */
2056    private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
2057        private View mDragView;
2058        private PointF mVelocity;
2059        private Rect mFrom;
2060        private long mPrevTime;
2061        private float mFriction;
2062
2063        private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
2064
2065        public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from,
2066                long startTime, float friction) {
2067            mDragView = dragView;
2068            mVelocity = vel;
2069            mFrom = from;
2070            mPrevTime = startTime;
2071            mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction);
2072        }
2073
2074        @Override
2075        public void onAnimationUpdate(ValueAnimator animation) {
2076            float t = ((Float) animation.getAnimatedValue()).floatValue();
2077            long curTime = AnimationUtils.currentAnimationTimeMillis();
2078
2079            mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
2080            mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
2081
2082            mDragView.setTranslationX(mFrom.left);
2083            mDragView.setTranslationY(mFrom.top);
2084            mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
2085
2086            mVelocity.x *= mFriction;
2087            mVelocity.y *= mFriction;
2088            mPrevTime = curTime;
2089        }
2090    };
2091
2092    public void onFlingToDelete(PointF vel) {
2093        final ViewConfiguration config = ViewConfiguration.get(getContext());
2094        final long startTime = AnimationUtils.currentAnimationTimeMillis();
2095
2096        // NOTE: Because it takes time for the first frame of animation to actually be
2097        // called and we expect the animation to be a continuation of the fling, we have
2098        // to account for the time that has elapsed since the fling finished.  And since
2099        // we don't have a startDelay, we will always get call to update when we call
2100        // start() (which we want to ignore).
2101        final TimeInterpolator tInterpolator = new TimeInterpolator() {
2102            private int mCount = -1;
2103            private long mStartTime;
2104            private float mOffset;
2105            /* Anonymous inner class ctor */ {
2106                mStartTime = startTime;
2107            }
2108
2109            @Override
2110            public float getInterpolation(float t) {
2111                if (mCount < 0) {
2112                    mCount++;
2113                } else if (mCount == 0) {
2114                    mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
2115                            mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION);
2116                    mCount++;
2117                }
2118                return Math.min(1f, mOffset + t);
2119            }
2120        };
2121
2122        final Rect from = new Rect();
2123        final View dragView = mDragView;
2124        from.left = (int) dragView.getTranslationX();
2125        from.top = (int) dragView.getTranslationY();
2126        AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, from, startTime,
2127                FLING_TO_DELETE_FRICTION);
2128
2129        final Runnable onAnimationEndRunnable = new Runnable() {
2130            @Override
2131            public void run() {
2132                // TEMP - in progress
2133                mIsFlingingToDelete = false;
2134                removeView(dragView);
2135                requestLayout();
2136            }
2137        };
2138
2139        // Create and start the animation
2140        ValueAnimator mDropAnim = new ValueAnimator();
2141        mDropAnim.setInterpolator(tInterpolator);
2142        mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION);
2143        mDropAnim.setFloatValues(0f, 1f);
2144        mDropAnim.addUpdateListener(updateCb);
2145        mDropAnim.addListener(new AnimatorListenerAdapter() {
2146            public void onAnimationEnd(Animator animation) {
2147                onAnimationEndRunnable.run();
2148            }
2149        });
2150        mDropAnim.start();
2151        mIsFlingingToDelete = true;
2152    }
2153
2154    /* Accessibility */
2155    @Override
2156    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
2157        super.onInitializeAccessibilityNodeInfo(info);
2158        info.setScrollable(getPageCount() > 1);
2159        if (getCurrentPage() < getPageCount() - 1) {
2160            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
2161        }
2162        if (getCurrentPage() > 0) {
2163            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
2164        }
2165    }
2166
2167    @Override
2168    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
2169        super.onInitializeAccessibilityEvent(event);
2170        event.setScrollable(true);
2171        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
2172            event.setFromIndex(mCurrentPage);
2173            event.setToIndex(mCurrentPage);
2174            event.setItemCount(getChildCount());
2175        }
2176    }
2177
2178    @Override
2179    public boolean performAccessibilityAction(int action, Bundle arguments) {
2180        if (super.performAccessibilityAction(action, arguments)) {
2181            return true;
2182        }
2183        switch (action) {
2184            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
2185                if (getCurrentPage() < getPageCount() - 1) {
2186                    scrollRight();
2187                    return true;
2188                }
2189            } break;
2190            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
2191                if (getCurrentPage() > 0) {
2192                    scrollLeft();
2193                    return true;
2194                }
2195            } break;
2196        }
2197        return false;
2198    }
2199
2200    @Override
2201    public boolean onHoverEvent(android.view.MotionEvent event) {
2202        return true;
2203    }
2204}
2205