PagedView.java revision 0ff7f010f8bfd011f0915031b02739ae3bee401e
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.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.Rect;
28import android.os.Bundle;
29import android.os.Parcel;
30import android.os.Parcelable;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.InputDevice;
34import android.view.KeyEvent;
35import android.view.MotionEvent;
36import android.view.VelocityTracker;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.ViewGroup;
40import android.view.ViewParent;
41import android.view.accessibility.AccessibilityEvent;
42import android.view.accessibility.AccessibilityManager;
43import android.view.accessibility.AccessibilityNodeInfo;
44import android.view.animation.Interpolator;
45import android.widget.Scroller;
46
47import com.android.internal.R;
48
49import java.util.ArrayList;
50
51/**
52 * An abstraction of the original Workspace which supports browsing through a
53 * sequential list of "pages"
54 */
55public class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener {
56    private static final String TAG = "WidgetPagedView";
57    private static final boolean DEBUG = false;
58    protected static final int INVALID_PAGE = -1;
59
60    // the min drag distance for a fling to register, to prevent random page shifts
61    private static final int MIN_LENGTH_FOR_FLING = 25;
62
63    protected static final int PAGE_SNAP_ANIMATION_DURATION = 550;
64    protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950;
65    protected static final float NANOTIME_DIV = 1000000000.0f;
66
67    private static final float OVERSCROLL_ACCELERATE_FACTOR = 2;
68    private static final float OVERSCROLL_DAMP_FACTOR = 0.14f;
69
70    private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f;
71    // The page is moved more than halfway, automatically move to the next page on touch up.
72    private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f;
73
74    // The following constants need to be scaled based on density. The scaled versions will be
75    // assigned to the corresponding member variables below.
76    private static final int FLING_THRESHOLD_VELOCITY = 500;
77    private static final int MIN_SNAP_VELOCITY = 1500;
78    private static final int MIN_FLING_VELOCITY = 250;
79
80    static final int AUTOMATIC_PAGE_SPACING = -1;
81
82    protected int mFlingThresholdVelocity;
83    protected int mMinFlingVelocity;
84    protected int mMinSnapVelocity;
85
86    protected float mDensity;
87    protected float mSmoothingTime;
88    protected float mTouchX;
89
90    protected boolean mFirstLayout = true;
91
92    protected int mCurrentPage;
93    protected int mChildCountOnLastMeasure;
94
95    protected int mNextPage = INVALID_PAGE;
96    protected int mMaxScrollX;
97    protected Scroller mScroller;
98    private VelocityTracker mVelocityTracker;
99
100    private float mDownMotionX;
101    protected float mLastMotionX;
102    protected float mLastMotionXRemainder;
103    protected float mLastMotionY;
104    protected float mTotalMotionX;
105    private int mLastScreenCenter = -1;
106    private int[] mChildOffsets;
107    private int[] mChildRelativeOffsets;
108    private int[] mChildOffsetsWithLayoutScale;
109
110    protected final static int TOUCH_STATE_REST = 0;
111    protected final static int TOUCH_STATE_SCROLLING = 1;
112    protected final static int TOUCH_STATE_PREV_PAGE = 2;
113    protected final static int TOUCH_STATE_NEXT_PAGE = 3;
114    protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f;
115
116    protected int mTouchState = TOUCH_STATE_REST;
117    protected boolean mForceScreenScrolled = false;
118
119    protected OnLongClickListener mLongClickListener;
120
121    protected boolean mAllowLongPress = true;
122
123    protected int mTouchSlop;
124    private int mPagingTouchSlop;
125    private int mMaximumVelocity;
126    private int mMinimumWidth;
127    protected int mPageSpacing;
128    protected int mPageLayoutPaddingTop;
129    protected int mPageLayoutPaddingBottom;
130    protected int mPageLayoutPaddingLeft;
131    protected int mPageLayoutPaddingRight;
132    protected int mPageLayoutWidthGap;
133    protected int mPageLayoutHeightGap;
134    protected int mCellCountX = 0;
135    protected int mCellCountY = 0;
136    protected boolean mCenterPagesVertically;
137    protected boolean mAllowOverScroll = true;
138    protected int mUnboundedScrollX;
139    protected int[] mTempVisiblePagesRange = new int[2];
140    protected boolean mForceDrawAllChildrenNextFrame;
141
142    // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise
143    // it is equal to the scaled overscroll position. We use a separate value so as to prevent
144    // the screens from continuing to translate beyond the normal bounds.
145    protected int mOverScrollX;
146
147    // parameter that adjusts the layout to be optimized for pages with that scale factor
148    protected float mLayoutScale = 1.0f;
149
150    protected static final int INVALID_POINTER = -1;
151
152    protected int mActivePointerId = INVALID_POINTER;
153
154    private PageSwitchListener mPageSwitchListener;
155
156    protected ArrayList<Boolean> mDirtyPageContent;
157
158    // If true, syncPages and syncPageItems will be called to refresh pages
159    protected boolean mContentIsRefreshable = true;
160
161    // If true, modify alpha of neighboring pages as user scrolls left/right
162    protected boolean mFadeInAdjacentScreens = true;
163
164    // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding
165    // to switch to a new page
166    protected boolean mUsePagingTouchSlop = true;
167
168    // If true, the subclass should directly update scrollX itself in its computeScroll method
169    // (SmoothPagedView does this)
170    protected boolean mDeferScrollUpdate = false;
171
172    protected boolean mIsPageMoving = false;
173
174    // All syncs and layout passes are deferred until data is ready.
175    protected boolean mIsDataReady = true;
176
177    // Scrolling indicator
178    private ValueAnimator mScrollIndicatorAnimator;
179    private View mScrollIndicator;
180    private int mScrollIndicatorPaddingLeft;
181    private int mScrollIndicatorPaddingRight;
182    private boolean mShouldShowScrollIndicator = false;
183    private boolean mShouldShowScrollIndicatorImmediately = false;
184    protected static final int sScrollIndicatorFadeInDuration = 150;
185    protected static final int sScrollIndicatorFadeOutDuration = 650;
186    protected static final int sScrollIndicatorFlashDuration = 650;
187
188    public interface PageSwitchListener {
189        void onPageSwitch(View newPage, int newPageIndex);
190    }
191
192    public PagedView(Context context) {
193        this(context, null);
194    }
195
196    public PagedView(Context context, AttributeSet attrs) {
197        this(context, attrs, 0);
198    }
199
200    public PagedView(Context context, AttributeSet attrs, int defStyle) {
201        super(context, attrs, defStyle);
202        TypedArray a = context.obtainStyledAttributes(attrs,
203                R.styleable.PagedView, defStyle, 0);
204        setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0));
205        mPageLayoutPaddingTop = a.getDimensionPixelSize(
206                R.styleable.PagedView_pageLayoutPaddingTop, 0);
207        mPageLayoutPaddingBottom = a.getDimensionPixelSize(
208                R.styleable.PagedView_pageLayoutPaddingBottom, 0);
209        mPageLayoutPaddingLeft = a.getDimensionPixelSize(
210                R.styleable.PagedView_pageLayoutPaddingLeft, 0);
211        mPageLayoutPaddingRight = a.getDimensionPixelSize(
212                R.styleable.PagedView_pageLayoutPaddingRight, 0);
213        mPageLayoutWidthGap = a.getDimensionPixelSize(
214                R.styleable.PagedView_pageLayoutWidthGap, 0);
215        mPageLayoutHeightGap = a.getDimensionPixelSize(
216                R.styleable.PagedView_pageLayoutHeightGap, 0);
217        mScrollIndicatorPaddingLeft =
218            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0);
219        mScrollIndicatorPaddingRight =
220            a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0);
221        a.recycle();
222
223        setHapticFeedbackEnabled(false);
224        init();
225    }
226
227    /**
228     * Initializes various states for this workspace.
229     */
230    protected void init() {
231        mDirtyPageContent = new ArrayList<Boolean>();
232        mDirtyPageContent.ensureCapacity(32);
233        mScroller = new Scroller(getContext(), new ScrollInterpolator());
234        mCurrentPage = 0;
235        mCenterPagesVertically = true;
236
237        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
238        mTouchSlop = configuration.getScaledTouchSlop();
239        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
240        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
241        mDensity = getResources().getDisplayMetrics().density;
242
243        mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
244        mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity);
245        mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity);
246        setOnHierarchyChangeListener(this);
247    }
248
249    public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
250        mPageSwitchListener = pageSwitchListener;
251        if (mPageSwitchListener != null) {
252            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
253        }
254    }
255
256    /**
257     * Called by subclasses to mark that data is ready, and that we can begin loading and laying
258     * out pages.
259     */
260    protected void setDataIsReady() {
261        mIsDataReady = true;
262    }
263
264    protected boolean isDataReady() {
265        return mIsDataReady;
266    }
267
268    /**
269     * Returns the index of the currently displayed page.
270     *
271     * @return The index of the currently displayed page.
272     */
273    int getCurrentPage() {
274        return mCurrentPage;
275    }
276
277    int getNextPage() {
278        return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
279    }
280
281    int getPageCount() {
282        return getChildCount();
283    }
284
285    View getPageAt(int index) {
286        return getChildAt(index);
287    }
288
289    protected int indexToPage(int index) {
290        return index;
291    }
292
293    /**
294     * Updates the scroll of the current page immediately to its final scroll position.  We use this
295     * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of
296     * the previous tab page.
297     */
298    protected void updateCurrentPageScroll() {
299        int offset = getChildOffset(mCurrentPage);
300        int relOffset = getRelativeChildOffset(mCurrentPage);
301        int newX = offset - relOffset;
302        scrollTo(newX, 0);
303        mScroller.setFinalX(newX);
304        mScroller.forceFinished(true);
305    }
306
307    /**
308     * Sets the current page.
309     */
310    void setCurrentPage(int currentPage) {
311        if (!mScroller.isFinished()) {
312            mScroller.abortAnimation();
313        }
314        // don't introduce any checks like mCurrentPage == currentPage here-- if we change the
315        // the default
316        if (getChildCount() == 0) {
317            return;
318        }
319
320        mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
321        updateCurrentPageScroll();
322        updateScrollingIndicator();
323        notifyPageSwitchListener();
324        invalidate();
325    }
326
327    protected void notifyPageSwitchListener() {
328        if (mPageSwitchListener != null) {
329            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
330        }
331    }
332
333    protected void pageBeginMoving() {
334        if (!mIsPageMoving) {
335            mIsPageMoving = true;
336            onPageBeginMoving();
337        }
338    }
339
340    protected void pageEndMoving() {
341        if (mIsPageMoving) {
342            mIsPageMoving = false;
343            onPageEndMoving();
344        }
345    }
346
347    protected boolean isPageMoving() {
348        return mIsPageMoving;
349    }
350
351    // a method that subclasses can override to add behavior
352    protected void onPageBeginMoving() {
353    }
354
355    // a method that subclasses can override to add behavior
356    protected void onPageEndMoving() {
357    }
358
359    /**
360     * Registers the specified listener on each page contained in this workspace.
361     *
362     * @param l The listener used to respond to long clicks.
363     */
364    @Override
365    public void setOnLongClickListener(OnLongClickListener l) {
366        mLongClickListener = l;
367        final int count = getPageCount();
368        for (int i = 0; i < count; i++) {
369            getPageAt(i).setOnLongClickListener(l);
370        }
371    }
372
373    @Override
374    public void scrollBy(int x, int y) {
375        scrollTo(mUnboundedScrollX + x, getScrollY() + y);
376    }
377
378    @Override
379    public void scrollTo(int x, int y) {
380        mUnboundedScrollX = x;
381
382        if (x < 0) {
383            super.scrollTo(0, y);
384            if (mAllowOverScroll) {
385                overScroll(x);
386            }
387        } else if (x > mMaxScrollX) {
388            super.scrollTo(mMaxScrollX, y);
389            if (mAllowOverScroll) {
390                overScroll(x - mMaxScrollX);
391            }
392        } else {
393            mOverScrollX = x;
394            super.scrollTo(x, y);
395        }
396
397        mTouchX = x;
398        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
399    }
400
401    // we moved this functionality to a helper function so SmoothPagedView can reuse it
402    protected boolean computeScrollHelper() {
403        if (mScroller.computeScrollOffset()) {
404            // Don't bother scrolling if the page does not need to be moved
405            if (getScrollX() != mScroller.getCurrX()
406                || getScrollY() != mScroller.getCurrY()
407                || mOverScrollX != mScroller.getCurrX()) {
408                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
409            }
410            invalidate();
411            return true;
412        } else if (mNextPage != INVALID_PAGE) {
413            mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
414            mNextPage = INVALID_PAGE;
415            notifyPageSwitchListener();
416
417            // We don't want to trigger a page end moving unless the page has settled
418            // and the user has stopped scrolling
419            if (mTouchState == TOUCH_STATE_REST) {
420                pageEndMoving();
421            }
422
423            // Notify the user when the page changes
424            AccessibilityManager accessibilityManager = (AccessibilityManager)
425                    getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
426            if (accessibilityManager.isEnabled()) {
427                AccessibilityEvent ev =
428                    AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
429                ev.getText().add(getCurrentPageDescription());
430                sendAccessibilityEventUnchecked(ev);
431            }
432            return true;
433        }
434        return false;
435    }
436
437    public String getCurrentPageDescription() {
438        return "";
439    }
440
441    @Override
442    public void computeScroll() {
443        computeScrollHelper();
444    }
445
446    @Override
447    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
448        if (!mIsDataReady || getChildCount() == 0) {
449            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
450            return;
451        }
452
453        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
454        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
455        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
456        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
457
458        if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
459            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
460            return;
461        }
462
463        // Return early if we aren't given a proper dimension
464        if (widthSize <= 0 || heightSize <= 0) {
465            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
466            return;
467        }
468
469        /* Allow the height to be set as WRAP_CONTENT. This allows the particular case
470         * of the All apps view on XLarge displays to not take up more space then it needs. Width
471         * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect
472         * each page to have the same width.
473         */
474        final int verticalPadding = getPaddingTop() + getPaddingBottom();
475        final int horizontalPadding = getPaddingLeft() + getPaddingRight();
476
477        // The children are given the same width and height as the workspace
478        // unless they were set to WRAP_CONTENT
479        if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize);
480        final int childCount = getChildCount();
481        for (int i = 0; i < childCount; i++) {
482            // disallowing padding in paged view (just pass 0)
483            final View child = getPageAt(i);
484
485            int childWidthMode = MeasureSpec.EXACTLY;
486            int childHeightMode = MeasureSpec.EXACTLY;
487
488            final int childWidthMeasureSpec =
489                MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode);
490            final int childHeightMeasureSpec =
491                MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode);
492
493            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
494        }
495
496        setMeasuredDimension(widthSize, heightSize);
497
498        // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions.
499        // We also wait until we set the measured dimensions before flushing the cache as well, to
500        // ensure that the cache is filled with good values.
501        invalidateCachedOffsets();
502
503        if (mChildCountOnLastMeasure != getChildCount()) {
504            setCurrentPage(mCurrentPage);
505        }
506        mChildCountOnLastMeasure = getChildCount();
507
508        if (childCount > 0) {
509            if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth() + ", "
510                    + getChildWidth(0));
511
512            // Calculate the variable page spacing if necessary
513            if (mPageSpacing == AUTOMATIC_PAGE_SPACING) {
514                // The gap between pages in the PagedView should be equal to the gap from the page
515                // to the edge of the screen (so it is not visible in the current screen).  To
516                // account for unequal padding on each side of the paged view, we take the maximum
517                // of the left/right gap and use that as the gap between each page.
518                int offset = getRelativeChildOffset(0);
519                int spacing = Math.max(offset, widthSize - offset -
520                        getChildAt(0).getMeasuredWidth());
521                setPageSpacing(spacing);
522            }
523        }
524
525        updateScrollingIndicatorPosition();
526
527        if (childCount > 0) {
528            mMaxScrollX = getChildOffset(childCount - 1) - getRelativeChildOffset(childCount - 1);
529        } else {
530            mMaxScrollX = 0;
531        }
532    }
533
534    public void setPageSpacing(int pageSpacing) {
535        mPageSpacing = pageSpacing;
536        invalidateCachedOffsets();
537    }
538
539    @Override
540    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
541        if (!mIsDataReady || getChildCount() == 0) {
542            return;
543        }
544
545        if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
546        final int verticalPadding = getPaddingTop() + getPaddingBottom();
547        final int childCount = getChildCount();
548        int childLeft = getRelativeChildOffset(0);
549
550        for (int i = 0; i < childCount; i++) {
551            final View child = getPageAt(i);
552            if (child.getVisibility() != View.GONE) {
553                final int childWidth = getScaledMeasuredWidth(child);
554                final int childHeight = child.getMeasuredHeight();
555                int childTop = getPaddingTop();
556                if (mCenterPagesVertically) {
557                    childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2;
558                }
559
560                if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop);
561                child.layout(childLeft, childTop,
562                        childLeft + child.getMeasuredWidth(), childTop + childHeight);
563                childLeft += childWidth + mPageSpacing;
564            }
565        }
566
567        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
568            setHorizontalScrollBarEnabled(false);
569            updateCurrentPageScroll();
570            setHorizontalScrollBarEnabled(true);
571            mFirstLayout = false;
572        }
573    }
574
575    protected void screenScrolled(int screenCenter) {
576        if (isScrollingIndicatorEnabled()) {
577            updateScrollingIndicator();
578        }
579        boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX;
580
581        if (mFadeInAdjacentScreens && !isInOverscroll) {
582            for (int i = 0; i < getChildCount(); i++) {
583                View child = getChildAt(i);
584                if (child != null) {
585                    float scrollProgress = getScrollProgress(screenCenter, child, i);
586                    float alpha = 1 - Math.abs(scrollProgress);
587                    child.setAlpha(alpha);
588                }
589            }
590            invalidate();
591        }
592    }
593
594    @Override
595    public void onChildViewAdded(View parent, View child) {
596        // This ensures that when children are added, they get the correct transforms / alphas
597        // in accordance with any scroll effects.
598        mForceScreenScrolled = true;
599        invalidate();
600        invalidateCachedOffsets();
601    }
602
603    @Override
604    public void onChildViewRemoved(View parent, View child) {
605        // TODO Auto-generated method stub
606    }
607
608    protected void invalidateCachedOffsets() {
609        int count = getChildCount();
610        if (count == 0) {
611            mChildOffsets = null;
612            mChildRelativeOffsets = null;
613            mChildOffsetsWithLayoutScale = null;
614            return;
615        }
616
617        mChildOffsets = new int[count];
618        mChildRelativeOffsets = new int[count];
619        mChildOffsetsWithLayoutScale = new int[count];
620        for (int i = 0; i < count; i++) {
621            mChildOffsets[i] = -1;
622            mChildRelativeOffsets[i] = -1;
623            mChildOffsetsWithLayoutScale[i] = -1;
624        }
625    }
626
627    protected int getChildOffset(int index) {
628        if (index < 0 || index > getChildCount() - 1) return 0;
629
630        int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ?
631                mChildOffsets : mChildOffsetsWithLayoutScale;
632
633        if (childOffsets != null && childOffsets[index] != -1) {
634            return childOffsets[index];
635        } else {
636            if (getChildCount() == 0)
637                return 0;
638
639            int offset = getRelativeChildOffset(0);
640            for (int i = 0; i < index; ++i) {
641                offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing;
642            }
643            if (childOffsets != null) {
644                childOffsets[index] = offset;
645            }
646            return offset;
647        }
648    }
649
650    protected int getRelativeChildOffset(int index) {
651        if (index < 0 || index > getChildCount() - 1) return 0;
652
653        if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) {
654            return mChildRelativeOffsets[index];
655        } else {
656            final int padding = getPaddingLeft() + getPaddingRight();
657            final int offset = getPaddingLeft() +
658                    (getMeasuredWidth() - padding - getChildWidth(index)) / 2;
659            if (mChildRelativeOffsets != null) {
660                mChildRelativeOffsets[index] = offset;
661            }
662            return offset;
663        }
664    }
665
666    protected int getScaledMeasuredWidth(View child) {
667        // This functions are called enough times that it actually makes a difference in the
668        // profiler -- so just inline the max() here
669        final int measuredWidth = child.getMeasuredWidth();
670        final int minWidth = mMinimumWidth;
671        final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth;
672        return (int) (maxWidth * mLayoutScale + 0.5f);
673    }
674
675    protected void getVisiblePages(int[] range) {
676        final int pageCount = getChildCount();
677
678        if (pageCount > 0) {
679            final int screenWidth = getMeasuredWidth();
680            int leftScreen = 0;
681            int rightScreen = 0;
682            View currPage = getPageAt(leftScreen);
683            while (leftScreen < pageCount - 1 &&
684                    currPage.getX() + currPage.getWidth() -
685                    currPage.getPaddingRight() < getScrollX()) {
686                leftScreen++;
687                currPage = getPageAt(leftScreen);
688            }
689            rightScreen = leftScreen;
690            currPage = getPageAt(rightScreen + 1);
691            while (rightScreen < pageCount - 1 &&
692                    currPage.getX() - currPage.getPaddingLeft() < getScrollX() + screenWidth) {
693                rightScreen++;
694                currPage = getPageAt(rightScreen + 1);
695            }
696            range[0] = leftScreen;
697            range[1] = rightScreen;
698        } else {
699            range[0] = -1;
700            range[1] = -1;
701        }
702    }
703
704    protected boolean shouldDrawChild(View child) {
705        return child.getAlpha() > 0;
706    }
707
708    @Override
709    protected void dispatchDraw(Canvas canvas) {
710        int halfScreenSize = getMeasuredWidth() / 2;
711        // mOverScrollX is equal to getScrollX() when we're within the normal scroll range.
712        // Otherwise it is equal to the scaled overscroll position.
713        int screenCenter = mOverScrollX + halfScreenSize;
714
715        if (screenCenter != mLastScreenCenter || mForceScreenScrolled) {
716            // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can
717            // set it for the next frame
718            mForceScreenScrolled = false;
719            screenScrolled(screenCenter);
720            mLastScreenCenter = screenCenter;
721        }
722
723        // Find out which screens are visible; as an optimization we only call draw on them
724        final int pageCount = getChildCount();
725        if (pageCount > 0) {
726            getVisiblePages(mTempVisiblePagesRange);
727            final int leftScreen = mTempVisiblePagesRange[0];
728            final int rightScreen = mTempVisiblePagesRange[1];
729            if (leftScreen != -1 && rightScreen != -1) {
730                final long drawingTime = getDrawingTime();
731                // Clip to the bounds
732                canvas.save();
733                canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(),
734                        getScrollY() + getBottom() - getTop());
735
736                for (int i = getChildCount() - 1; i >= 0; i--) {
737                    final View v = getPageAt(i);
738                    if (mForceDrawAllChildrenNextFrame ||
739                               (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) {
740                        drawChild(canvas, v, drawingTime);
741                    }
742                }
743                mForceDrawAllChildrenNextFrame = false;
744                canvas.restore();
745            }
746        }
747    }
748
749    @Override
750    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
751        int page = indexToPage(indexOfChild(child));
752        if (page != mCurrentPage || !mScroller.isFinished()) {
753            snapToPage(page);
754            return true;
755        }
756        return false;
757    }
758
759    @Override
760    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
761        int focusablePage;
762        if (mNextPage != INVALID_PAGE) {
763            focusablePage = mNextPage;
764        } else {
765            focusablePage = mCurrentPage;
766        }
767        View v = getPageAt(focusablePage);
768        if (v != null) {
769            return v.requestFocus(direction, previouslyFocusedRect);
770        }
771        return false;
772    }
773
774    @Override
775    public boolean dispatchUnhandledMove(View focused, int direction) {
776        if (direction == View.FOCUS_LEFT) {
777            if (getCurrentPage() > 0) {
778                snapToPage(getCurrentPage() - 1);
779                return true;
780            }
781        } else if (direction == View.FOCUS_RIGHT) {
782            if (getCurrentPage() < getPageCount() - 1) {
783                snapToPage(getCurrentPage() + 1);
784                return true;
785            }
786        }
787        return super.dispatchUnhandledMove(focused, direction);
788    }
789
790    @Override
791    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
792        if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
793            getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode);
794        }
795        if (direction == View.FOCUS_LEFT) {
796            if (mCurrentPage > 0) {
797                getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode);
798            }
799        } else if (direction == View.FOCUS_RIGHT){
800            if (mCurrentPage < getPageCount() - 1) {
801                getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode);
802            }
803        }
804    }
805
806    /**
807     * If one of our descendant views decides that it could be focused now, only
808     * pass that along if it's on the current page.
809     *
810     * This happens when live folders requery, and if they're off page, they
811     * end up calling requestFocus, which pulls it on page.
812     */
813    @Override
814    public void focusableViewAvailable(View focused) {
815        View current = getPageAt(mCurrentPage);
816        View v = focused;
817        while (true) {
818            if (v == current) {
819                super.focusableViewAvailable(focused);
820                return;
821            }
822            if (v == this) {
823                return;
824            }
825            ViewParent parent = v.getParent();
826            if (parent instanceof View) {
827                v = (View)v.getParent();
828            } else {
829                return;
830            }
831        }
832    }
833
834    /**
835     * {@inheritDoc}
836     */
837    @Override
838    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
839        if (disallowIntercept) {
840            // We need to make sure to cancel our long press if
841            // a scrollable widget takes over touch events
842            final View currentPage = getPageAt(mCurrentPage);
843            currentPage.cancelLongPress();
844        }
845        super.requestDisallowInterceptTouchEvent(disallowIntercept);
846    }
847
848    /**
849     * Return true if a tap at (x, y) should trigger a flip to the previous page.
850     */
851    protected boolean hitsPreviousPage(float x, float y) {
852        return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing);
853    }
854
855    /**
856     * Return true if a tap at (x, y) should trigger a flip to the next page.
857     */
858    protected boolean hitsNextPage(float x, float y) {
859        return  (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing));
860    }
861
862    @Override
863    public boolean onInterceptTouchEvent(MotionEvent ev) {
864        /*
865         * This method JUST determines whether we want to intercept the motion.
866         * If we return true, onTouchEvent will be called and we do the actual
867         * scrolling there.
868         */
869        acquireVelocityTrackerAndAddMovement(ev);
870
871        // Skip touch handling if there are no pages to swipe
872        if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev);
873
874        /*
875         * Shortcut the most recurring case: the user is in the dragging
876         * state and he is moving his finger.  We want to intercept this
877         * motion.
878         */
879        final int action = ev.getAction();
880        if ((action == MotionEvent.ACTION_MOVE) &&
881                (mTouchState == TOUCH_STATE_SCROLLING)) {
882            return true;
883        }
884
885        switch (action & MotionEvent.ACTION_MASK) {
886            case MotionEvent.ACTION_MOVE: {
887                /*
888                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
889                 * whether the user has moved far enough from his original down touch.
890                 */
891                if (mActivePointerId != INVALID_POINTER) {
892                    determineScrollingStart(ev);
893                    break;
894                }
895                // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
896                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
897                // i.e. fall through to the next case (don't break)
898                // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
899                // while it's small- this was causing a crash before we checked for INVALID_POINTER)
900            }
901
902            case MotionEvent.ACTION_DOWN: {
903                final float x = ev.getX();
904                final float y = ev.getY();
905                // Remember location of down touch
906                mDownMotionX = x;
907                mLastMotionX = x;
908                mLastMotionY = y;
909                mLastMotionXRemainder = 0;
910                mTotalMotionX = 0;
911                mActivePointerId = ev.getPointerId(0);
912                mAllowLongPress = true;
913
914                /*
915                 * If being flinged and user touches the screen, initiate drag;
916                 * otherwise don't.  mScroller.isFinished should be false when
917                 * being flinged.
918                 */
919                final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
920                final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
921                if (finishedScrolling) {
922                    mTouchState = TOUCH_STATE_REST;
923                    mScroller.abortAnimation();
924                } else {
925                    mTouchState = TOUCH_STATE_SCROLLING;
926                }
927
928                // check if this can be the beginning of a tap on the side of the pages
929                // to scroll the current page
930                if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) {
931                    if (getChildCount() > 0) {
932                        if (hitsPreviousPage(x, y)) {
933                            mTouchState = TOUCH_STATE_PREV_PAGE;
934                        } else if (hitsNextPage(x, y)) {
935                            mTouchState = TOUCH_STATE_NEXT_PAGE;
936                        }
937                    }
938                }
939                break;
940            }
941
942            case MotionEvent.ACTION_UP:
943            case MotionEvent.ACTION_CANCEL:
944                mTouchState = TOUCH_STATE_REST;
945                mAllowLongPress = false;
946                mActivePointerId = INVALID_POINTER;
947                releaseVelocityTracker();
948                break;
949
950            case MotionEvent.ACTION_POINTER_UP:
951                onSecondaryPointerUp(ev);
952                releaseVelocityTracker();
953                break;
954        }
955
956        /*
957         * The only time we want to intercept motion events is if we are in the
958         * drag mode.
959         */
960        return mTouchState != TOUCH_STATE_REST;
961    }
962
963    protected void determineScrollingStart(MotionEvent ev) {
964        determineScrollingStart(ev, 1.0f);
965    }
966
967    /*
968     * Determines if we should change the touch state to start scrolling after the
969     * user moves their touch point too far.
970     */
971    protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
972        /*
973         * Locally do absolute value. mLastMotionX is set to the y value
974         * of the down event.
975         */
976        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
977        if (pointerIndex == -1) {
978            return;
979        }
980        final float x = ev.getX(pointerIndex);
981        final float y = ev.getY(pointerIndex);
982        final int xDiff = (int) Math.abs(x - mLastMotionX);
983        final int yDiff = (int) Math.abs(y - mLastMotionY);
984
985        final int touchSlop = Math.round(touchSlopScale * mTouchSlop);
986        boolean xPaged = xDiff > mPagingTouchSlop;
987        boolean xMoved = xDiff > touchSlop;
988        boolean yMoved = yDiff > touchSlop;
989
990        if (xMoved || xPaged || yMoved) {
991            if (mUsePagingTouchSlop ? xPaged : xMoved) {
992                // Scroll if the user moved far enough along the X axis
993                mTouchState = TOUCH_STATE_SCROLLING;
994                mTotalMotionX += Math.abs(mLastMotionX - x);
995                mLastMotionX = x;
996                mLastMotionXRemainder = 0;
997                mTouchX = getScrollX();
998                mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
999                pageBeginMoving();
1000            }
1001            // Either way, cancel any pending longpress
1002            cancelCurrentPageLongPress();
1003        }
1004    }
1005
1006    protected void cancelCurrentPageLongPress() {
1007        if (mAllowLongPress) {
1008            mAllowLongPress = false;
1009            // Try canceling the long press. It could also have been scheduled
1010            // by a distant descendant, so use the mAllowLongPress flag to block
1011            // everything
1012            final View currentPage = getPageAt(mCurrentPage);
1013            if (currentPage != null) {
1014                currentPage.cancelLongPress();
1015            }
1016        }
1017    }
1018
1019    protected float getScrollProgress(int screenCenter, View v, int page) {
1020        final int halfScreenSize = getMeasuredWidth() / 2;
1021
1022        int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing;
1023        int delta = screenCenter - (getChildOffset(page) -
1024                getRelativeChildOffset(page) + halfScreenSize);
1025
1026        float scrollProgress = delta / (totalDistance * 1.0f);
1027        scrollProgress = Math.min(scrollProgress, 1.0f);
1028        scrollProgress = Math.max(scrollProgress, -1.0f);
1029        return scrollProgress;
1030    }
1031
1032    // This curve determines how the effect of scrolling over the limits of the page dimishes
1033    // as the user pulls further and further from the bounds
1034    private float overScrollInfluenceCurve(float f) {
1035        f -= 1.0f;
1036        return f * f * f + 1.0f;
1037    }
1038
1039    protected void acceleratedOverScroll(float amount) {
1040        int screenSize = getMeasuredWidth();
1041
1042        // We want to reach the max over scroll effect when the user has
1043        // over scrolled half the size of the screen
1044        float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize);
1045
1046        if (f == 0) return;
1047
1048        // Clamp this factor, f, to -1 < f < 1
1049        if (Math.abs(f) >= 1) {
1050            f /= Math.abs(f);
1051        }
1052
1053        int overScrollAmount = (int) Math.round(f * screenSize);
1054        if (amount < 0) {
1055            mOverScrollX = overScrollAmount;
1056            super.scrollTo(0, getScrollY());
1057        } else {
1058            mOverScrollX = mMaxScrollX + overScrollAmount;
1059            super.scrollTo(mMaxScrollX, getScrollY());
1060        }
1061        invalidate();
1062    }
1063
1064    protected void dampedOverScroll(float amount) {
1065        int screenSize = getMeasuredWidth();
1066
1067        float f = (amount / screenSize);
1068
1069        if (f == 0) return;
1070        f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
1071
1072        // Clamp this factor, f, to -1 < f < 1
1073        if (Math.abs(f) >= 1) {
1074            f /= Math.abs(f);
1075        }
1076
1077        int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize);
1078        if (amount < 0) {
1079            mOverScrollX = overScrollAmount;
1080            super.scrollTo(0, getScrollY());
1081        } else {
1082            mOverScrollX = mMaxScrollX + overScrollAmount;
1083            super.scrollTo(mMaxScrollX, getScrollY());
1084        }
1085        invalidate();
1086    }
1087
1088    protected void overScroll(float amount) {
1089        dampedOverScroll(amount);
1090    }
1091
1092    protected float maxOverScroll() {
1093        // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not
1094        // exceed). Used to find out how much extra wallpaper we need for the over scroll effect
1095        float f = 1.0f;
1096        f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
1097        return OVERSCROLL_DAMP_FACTOR * f;
1098    }
1099
1100    @Override
1101    public boolean onTouchEvent(MotionEvent ev) {
1102        // Skip touch handling if there are no pages to swipe
1103        if (getChildCount() <= 0) return super.onTouchEvent(ev);
1104
1105        acquireVelocityTrackerAndAddMovement(ev);
1106
1107        final int action = ev.getAction();
1108
1109        switch (action & MotionEvent.ACTION_MASK) {
1110        case MotionEvent.ACTION_DOWN:
1111            /*
1112             * If being flinged and user touches, stop the fling. isFinished
1113             * will be false if being flinged.
1114             */
1115            if (!mScroller.isFinished()) {
1116                mScroller.abortAnimation();
1117            }
1118
1119            // Remember where the motion event started
1120            mDownMotionX = mLastMotionX = ev.getX();
1121            mLastMotionXRemainder = 0;
1122            mTotalMotionX = 0;
1123            mActivePointerId = ev.getPointerId(0);
1124            if (mTouchState == TOUCH_STATE_SCROLLING) {
1125                pageBeginMoving();
1126            }
1127            break;
1128
1129        case MotionEvent.ACTION_MOVE:
1130            if (mTouchState == TOUCH_STATE_SCROLLING) {
1131                // Scroll to follow the motion event
1132                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
1133                final float x = ev.getX(pointerIndex);
1134                final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
1135
1136                mTotalMotionX += Math.abs(deltaX);
1137
1138                // Only scroll and update mLastMotionX if we have moved some discrete amount.  We
1139                // keep the remainder because we are actually testing if we've moved from the last
1140                // scrolled position (which is discrete).
1141                if (Math.abs(deltaX) >= 1.0f) {
1142                    mTouchX += deltaX;
1143                    mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
1144                    if (!mDeferScrollUpdate) {
1145                        scrollBy((int) deltaX, 0);
1146                        if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX);
1147                    } else {
1148                        invalidate();
1149                    }
1150                    mLastMotionX = x;
1151                    mLastMotionXRemainder = deltaX - (int) deltaX;
1152                } else {
1153                    awakenScrollBars();
1154                }
1155            } else {
1156                determineScrollingStart(ev);
1157            }
1158            break;
1159
1160        case MotionEvent.ACTION_UP:
1161            if (mTouchState == TOUCH_STATE_SCROLLING) {
1162                final int activePointerId = mActivePointerId;
1163                final int pointerIndex = ev.findPointerIndex(activePointerId);
1164                final float x = ev.getX(pointerIndex);
1165                final VelocityTracker velocityTracker = mVelocityTracker;
1166                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1167                int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
1168                final int deltaX = (int) (x - mDownMotionX);
1169                final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage));
1170                boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
1171                        SIGNIFICANT_MOVE_THRESHOLD;
1172
1173                mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
1174
1175                boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
1176                        Math.abs(velocityX) > mFlingThresholdVelocity;
1177
1178                // In the case that the page is moved far to one direction and then is flung
1179                // in the opposite direction, we use a threshold to determine whether we should
1180                // just return to the starting page, or if we should skip one further.
1181                boolean returnToOriginalPage = false;
1182                if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
1183                        Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
1184                    returnToOriginalPage = true;
1185                }
1186
1187                int finalPage;
1188                // We give flings precedence over large moves, which is why we short-circuit our
1189                // test for a large move if a fling has been registered. That is, a large
1190                // move to the left and fling to the right will register as a fling to the right.
1191                if (((isSignificantMove && deltaX > 0 && !isFling) ||
1192                        (isFling && velocityX > 0)) && mCurrentPage > 0) {
1193                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
1194                    snapToPageWithVelocity(finalPage, velocityX);
1195                } else if (((isSignificantMove && deltaX < 0 && !isFling) ||
1196                        (isFling && velocityX < 0)) &&
1197                        mCurrentPage < getChildCount() - 1) {
1198                    finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
1199                    snapToPageWithVelocity(finalPage, velocityX);
1200                } else {
1201                    snapToDestination();
1202                }
1203            } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
1204                // at this point we have not moved beyond the touch slop
1205                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
1206                // we can just page
1207                int nextPage = Math.max(0, mCurrentPage - 1);
1208                if (nextPage != mCurrentPage) {
1209                    snapToPage(nextPage);
1210                } else {
1211                    snapToDestination();
1212                }
1213            } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
1214                // at this point we have not moved beyond the touch slop
1215                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
1216                // we can just page
1217                int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
1218                if (nextPage != mCurrentPage) {
1219                    snapToPage(nextPage);
1220                } else {
1221                    snapToDestination();
1222                }
1223            } else {
1224                onUnhandledTap(ev);
1225            }
1226            mTouchState = TOUCH_STATE_REST;
1227            mActivePointerId = INVALID_POINTER;
1228            releaseVelocityTracker();
1229            break;
1230
1231        case MotionEvent.ACTION_CANCEL:
1232            if (mTouchState == TOUCH_STATE_SCROLLING) {
1233                snapToDestination();
1234            }
1235            mTouchState = TOUCH_STATE_REST;
1236            mActivePointerId = INVALID_POINTER;
1237            releaseVelocityTracker();
1238            break;
1239
1240        case MotionEvent.ACTION_POINTER_UP:
1241            onSecondaryPointerUp(ev);
1242            break;
1243        }
1244
1245        return true;
1246    }
1247
1248    @Override
1249    public boolean onGenericMotionEvent(MotionEvent event) {
1250        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
1251            switch (event.getAction()) {
1252                case MotionEvent.ACTION_SCROLL: {
1253                    // Handle mouse (or ext. device) by shifting the page depending on the scroll
1254                    final float vscroll;
1255                    final float hscroll;
1256                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
1257                        vscroll = 0;
1258                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1259                    } else {
1260                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1261                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
1262                    }
1263                    if (hscroll != 0 || vscroll != 0) {
1264                        if (hscroll > 0 || vscroll > 0) {
1265                            scrollRight();
1266                        } else {
1267                            scrollLeft();
1268                        }
1269                        return true;
1270                    }
1271                }
1272            }
1273        }
1274        return super.onGenericMotionEvent(event);
1275    }
1276
1277    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
1278        if (mVelocityTracker == null) {
1279            mVelocityTracker = VelocityTracker.obtain();
1280        }
1281        mVelocityTracker.addMovement(ev);
1282    }
1283
1284    private void releaseVelocityTracker() {
1285        if (mVelocityTracker != null) {
1286            mVelocityTracker.recycle();
1287            mVelocityTracker = null;
1288        }
1289    }
1290
1291    private void onSecondaryPointerUp(MotionEvent ev) {
1292        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
1293                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
1294        final int pointerId = ev.getPointerId(pointerIndex);
1295        if (pointerId == mActivePointerId) {
1296            // This was our active pointer going up. Choose a new
1297            // active pointer and adjust accordingly.
1298            // TODO: Make this decision more intelligent.
1299            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1300            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
1301            mLastMotionY = ev.getY(newPointerIndex);
1302            mLastMotionXRemainder = 0;
1303            mActivePointerId = ev.getPointerId(newPointerIndex);
1304            if (mVelocityTracker != null) {
1305                mVelocityTracker.clear();
1306            }
1307        }
1308    }
1309
1310    protected void onUnhandledTap(MotionEvent ev) {}
1311
1312    @Override
1313    public void requestChildFocus(View child, View focused) {
1314        super.requestChildFocus(child, focused);
1315        int page = indexToPage(indexOfChild(child));
1316        if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) {
1317            snapToPage(page);
1318        }
1319    }
1320
1321    protected int getChildIndexForRelativeOffset(int relativeOffset) {
1322        final int childCount = getChildCount();
1323        int left;
1324        int right;
1325        for (int i = 0; i < childCount; ++i) {
1326            left = getRelativeChildOffset(i);
1327            right = (left + getScaledMeasuredWidth(getPageAt(i)));
1328            if (left <= relativeOffset && relativeOffset <= right) {
1329                return i;
1330            }
1331        }
1332        return -1;
1333    }
1334
1335    protected int getChildWidth(int index) {
1336        // This functions are called enough times that it actually makes a difference in the
1337        // profiler -- so just inline the max() here
1338        final int measuredWidth = getPageAt(index).getMeasuredWidth();
1339        final int minWidth = mMinimumWidth;
1340        return (minWidth > measuredWidth) ? minWidth : measuredWidth;
1341    }
1342
1343    int getPageNearestToCenterOfScreen() {
1344        int minDistanceFromScreenCenter = Integer.MAX_VALUE;
1345        int minDistanceFromScreenCenterIndex = -1;
1346        int screenCenter = getScrollX() + (getMeasuredWidth() / 2);
1347        final int childCount = getChildCount();
1348        for (int i = 0; i < childCount; ++i) {
1349            View layout = (View) getPageAt(i);
1350            int childWidth = getScaledMeasuredWidth(layout);
1351            int halfChildWidth = (childWidth / 2);
1352            int childCenter = getChildOffset(i) + halfChildWidth;
1353            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
1354            if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
1355                minDistanceFromScreenCenter = distanceFromScreenCenter;
1356                minDistanceFromScreenCenterIndex = i;
1357            }
1358        }
1359        return minDistanceFromScreenCenterIndex;
1360    }
1361
1362    protected void snapToDestination() {
1363        snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION);
1364    }
1365
1366    private static class ScrollInterpolator implements Interpolator {
1367        public ScrollInterpolator() {
1368        }
1369
1370        public float getInterpolation(float t) {
1371            t -= 1.0f;
1372            return t*t*t*t*t + 1;
1373        }
1374    }
1375
1376    // We want the duration of the page snap animation to be influenced by the distance that
1377    // the screen has to travel, however, we don't want this duration to be effected in a
1378    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
1379    // of travel has on the overall snap duration.
1380    float distanceInfluenceForSnapDuration(float f) {
1381        f -= 0.5f; // center the values about 0.
1382        f *= 0.3f * Math.PI / 2.0f;
1383        return (float) Math.sin(f);
1384    }
1385
1386    protected void snapToPageWithVelocity(int whichPage, int velocity) {
1387        whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1));
1388        int halfScreenSize = getMeasuredWidth() / 2;
1389
1390        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
1391        if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): "
1392                + getMeasuredWidth() + ", " + getChildWidth(whichPage));
1393        final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
1394        int delta = newX - mUnboundedScrollX;
1395        int duration = 0;
1396
1397        if (Math.abs(velocity) < mMinFlingVelocity) {
1398            // If the velocity is low enough, then treat this more as an automatic page advance
1399            // as opposed to an apparent physical response to flinging
1400            snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
1401            return;
1402        }
1403
1404        // Here we compute a "distance" that will be used in the computation of the overall
1405        // snap duration. This is a function of the actual distance that needs to be traveled;
1406        // we keep this value close to half screen size in order to reduce the variance in snap
1407        // duration as a function of the distance the page needs to travel.
1408        float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize));
1409        float distance = halfScreenSize + halfScreenSize *
1410                distanceInfluenceForSnapDuration(distanceRatio);
1411
1412        velocity = Math.abs(velocity);
1413        velocity = Math.max(mMinSnapVelocity, velocity);
1414
1415        // we want the page's snap velocity to approximately match the velocity at which the
1416        // user flings, so we scale the duration by a value near to the derivative of the scroll
1417        // interpolator at zero, ie. 5. We use 4 to make it a little slower.
1418        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
1419
1420        snapToPage(whichPage, delta, duration);
1421    }
1422
1423    protected void snapToPage(int whichPage) {
1424        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
1425    }
1426
1427    protected void snapToPage(int whichPage, int duration) {
1428        whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
1429
1430        if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage));
1431        if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMeasuredWidth() + ", "
1432                + getChildWidth(whichPage));
1433        int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
1434        final int sX = mUnboundedScrollX;
1435        final int delta = newX - sX;
1436        snapToPage(whichPage, delta, duration);
1437    }
1438
1439    protected void snapToPage(int whichPage, int delta, int duration) {
1440        mNextPage = whichPage;
1441
1442        View focusedChild = getFocusedChild();
1443        if (focusedChild != null && whichPage != mCurrentPage &&
1444                focusedChild == getPageAt(mCurrentPage)) {
1445            focusedChild.clearFocus();
1446        }
1447
1448        pageBeginMoving();
1449        awakenScrollBars(duration);
1450        if (duration == 0) {
1451            duration = Math.abs(delta);
1452        }
1453
1454        if (!mScroller.isFinished()) mScroller.abortAnimation();
1455        mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);
1456
1457        notifyPageSwitchListener();
1458        invalidate();
1459    }
1460
1461    public void scrollLeft() {
1462        if (mScroller.isFinished()) {
1463            if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
1464        } else {
1465            if (mNextPage > 0) snapToPage(mNextPage - 1);
1466        }
1467    }
1468
1469    public void scrollRight() {
1470        if (mScroller.isFinished()) {
1471            if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
1472        } else {
1473            if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
1474        }
1475    }
1476
1477    public int getPageForView(View v) {
1478        int result = -1;
1479        if (v != null) {
1480            ViewParent vp = v.getParent();
1481            int count = getChildCount();
1482            for (int i = 0; i < count; i++) {
1483                if (vp == getPageAt(i)) {
1484                    return i;
1485                }
1486            }
1487        }
1488        return result;
1489    }
1490
1491    /**
1492     * @return True is long presses are still allowed for the current touch
1493     */
1494    public boolean allowLongPress() {
1495        return mAllowLongPress;
1496    }
1497
1498    /**
1499     * Set true to allow long-press events to be triggered, usually checked by
1500     * {@link Launcher} to accept or block dpad-initiated long-presses.
1501     */
1502    public void setAllowLongPress(boolean allowLongPress) {
1503        mAllowLongPress = allowLongPress;
1504    }
1505
1506    public static class SavedState extends BaseSavedState {
1507        int currentPage = -1;
1508
1509        SavedState(Parcelable superState) {
1510            super(superState);
1511        }
1512
1513        private SavedState(Parcel in) {
1514            super(in);
1515            currentPage = in.readInt();
1516        }
1517
1518        @Override
1519        public void writeToParcel(Parcel out, int flags) {
1520            super.writeToParcel(out, flags);
1521            out.writeInt(currentPage);
1522        }
1523
1524        public static final Parcelable.Creator<SavedState> CREATOR =
1525                new Parcelable.Creator<SavedState>() {
1526            public SavedState createFromParcel(Parcel in) {
1527                return new SavedState(in);
1528            }
1529
1530            public SavedState[] newArray(int size) {
1531                return new SavedState[size];
1532            }
1533        };
1534    }
1535
1536    protected View getScrollingIndicator() {
1537        return null;
1538    }
1539
1540    protected boolean isScrollingIndicatorEnabled() {
1541        return false;
1542    }
1543
1544    Runnable hideScrollingIndicatorRunnable = new Runnable() {
1545        @Override
1546        public void run() {
1547            hideScrollingIndicator(false);
1548        }
1549    };
1550
1551    protected void flashScrollingIndicator(boolean animated) {
1552        removeCallbacks(hideScrollingIndicatorRunnable);
1553        showScrollingIndicator(!animated);
1554        postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration);
1555    }
1556
1557    protected void showScrollingIndicator(boolean immediately) {
1558        mShouldShowScrollIndicator = true;
1559        mShouldShowScrollIndicatorImmediately = true;
1560        if (getChildCount() <= 1) return;
1561        if (!isScrollingIndicatorEnabled()) return;
1562
1563        mShouldShowScrollIndicator = false;
1564        getScrollingIndicator();
1565        if (mScrollIndicator != null) {
1566            // Fade the indicator in
1567            updateScrollingIndicatorPosition();
1568            mScrollIndicator.setVisibility(View.VISIBLE);
1569            cancelScrollingIndicatorAnimations();
1570            if (immediately) {
1571                mScrollIndicator.setAlpha(1f);
1572            } else {
1573                mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 1f);
1574                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration);
1575                mScrollIndicatorAnimator.start();
1576            }
1577        }
1578    }
1579
1580    protected void cancelScrollingIndicatorAnimations() {
1581        if (mScrollIndicatorAnimator != null) {
1582            mScrollIndicatorAnimator.cancel();
1583        }
1584    }
1585
1586    protected void hideScrollingIndicator(boolean immediately) {
1587        if (getChildCount() <= 1) return;
1588        if (!isScrollingIndicatorEnabled()) return;
1589
1590        getScrollingIndicator();
1591        if (mScrollIndicator != null) {
1592            // Fade the indicator out
1593            updateScrollingIndicatorPosition();
1594            cancelScrollingIndicatorAnimations();
1595            if (immediately) {
1596                mScrollIndicator.setVisibility(View.INVISIBLE);
1597                mScrollIndicator.setAlpha(0f);
1598            } else {
1599                mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 0f);
1600                mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration);
1601                mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() {
1602                    private boolean cancelled = false;
1603                    @Override
1604                    public void onAnimationCancel(android.animation.Animator animation) {
1605                        cancelled = true;
1606                    }
1607                    @Override
1608                    public void onAnimationEnd(Animator animation) {
1609                        if (!cancelled) {
1610                            mScrollIndicator.setVisibility(View.INVISIBLE);
1611                        }
1612                    }
1613                });
1614                mScrollIndicatorAnimator.start();
1615            }
1616        }
1617    }
1618
1619    /**
1620     * To be overridden by subclasses to determine whether the scroll indicator should stretch to
1621     * fill its space on the track or not.
1622     */
1623    protected boolean hasElasticScrollIndicator() {
1624        return true;
1625    }
1626
1627    private void updateScrollingIndicator() {
1628        if (getChildCount() <= 1) return;
1629        if (!isScrollingIndicatorEnabled()) return;
1630
1631        getScrollingIndicator();
1632        if (mScrollIndicator != null) {
1633            updateScrollingIndicatorPosition();
1634        }
1635        if (mShouldShowScrollIndicator) {
1636            showScrollingIndicator(mShouldShowScrollIndicatorImmediately);
1637        }
1638    }
1639
1640    private void updateScrollingIndicatorPosition() {
1641        if (!isScrollingIndicatorEnabled()) return;
1642        if (mScrollIndicator == null) return;
1643        int numPages = getChildCount();
1644        int pageWidth = getMeasuredWidth();
1645        int lastChildIndex = Math.max(0, getChildCount() - 1);
1646        int maxScrollX = getChildOffset(lastChildIndex) - getRelativeChildOffset(lastChildIndex);
1647        int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight;
1648        int indicatorWidth = mScrollIndicator.getMeasuredWidth() -
1649                mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight();
1650
1651        float offset = Math.max(0f, Math.min(1f, (float) getScrollX() / maxScrollX));
1652        int indicatorSpace = trackWidth / numPages;
1653        int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft;
1654        if (hasElasticScrollIndicator()) {
1655            if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) {
1656                mScrollIndicator.getLayoutParams().width = indicatorSpace;
1657                mScrollIndicator.requestLayout();
1658            }
1659        } else {
1660            int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2;
1661            indicatorPos += indicatorCenterOffset;
1662        }
1663        mScrollIndicator.setTranslationX(indicatorPos);
1664    }
1665
1666    /* Accessibility */
1667    @Override
1668    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1669        super.onInitializeAccessibilityNodeInfo(info);
1670        info.setScrollable(getPageCount() > 1);
1671        if (getCurrentPage() < getPageCount() - 1) {
1672            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1673        }
1674        if (getCurrentPage() > 0) {
1675            info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1676        }
1677    }
1678
1679    @Override
1680    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1681        super.onInitializeAccessibilityEvent(event);
1682        event.setScrollable(true);
1683        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
1684            event.setFromIndex(mCurrentPage);
1685            event.setToIndex(mCurrentPage);
1686            event.setItemCount(getChildCount());
1687        }
1688    }
1689
1690    @Override
1691    public boolean performAccessibilityAction(int action, Bundle arguments) {
1692        if (super.performAccessibilityAction(action, arguments)) {
1693            return true;
1694        }
1695        switch (action) {
1696            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1697                if (getCurrentPage() < getPageCount() - 1) {
1698                    scrollRight();
1699                    return true;
1700                }
1701            } break;
1702            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1703                if (getCurrentPage() > 0) {
1704                    scrollLeft();
1705                    return true;
1706                }
1707            } break;
1708        }
1709        return false;
1710    }
1711
1712    @Override
1713    public boolean onHoverEvent(android.view.MotionEvent event) {
1714        return true;
1715    }
1716}
1717