ViewPager.java revision cf2312ee850d26c9d1a2413c996d41397e816fb5
1/*
2 * Copyright (C) 2011 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 android.support.v4.view;
18
19import android.os.Parcel;
20import android.os.Parcelable;
21import android.os.SystemClock;
22
23import android.content.Context;
24import android.support.v4.os.ParcelableCompat;
25import android.support.v4.os.ParcelableCompatCreatorCallbacks;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.widget.Scroller;
34
35import java.util.ArrayList;
36
37/**
38 * Layout manager that allows the user to flip left and right
39 * through pages of data.  You supply an implementation of a
40 * {@link PagerAdapter} to generate the pages that the view shows.
41 *
42 * <p>Note this class is currently under early design and
43 * development.  The API will likely change in later updates of
44 * the compatibility library, requiring changes to the source code
45 * of apps when they are compiled against the newer version.</p>
46 */
47public class ViewPager extends ViewGroup {
48    private static final String TAG = "ViewPager";
49    private static final boolean DEBUG = false;
50
51    private static final boolean USE_CACHE = false;
52
53    static class ItemInfo {
54        Object object;
55        int position;
56        boolean scrolling;
57    }
58
59    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
60
61    private PagerAdapter mAdapter;
62    private int mCurItem;   // Index of currently displayed page.
63    private int mRestoredCurItem = -1;
64    private Parcelable mRestoredAdapterState = null;
65    private ClassLoader mRestoredClassLoader = null;
66    private Scroller mScroller;
67    private PagerAdapter.DataSetObserver mObserver;
68
69    private int mChildWidthMeasureSpec;
70    private int mChildHeightMeasureSpec;
71    private boolean mInLayout;
72
73    private boolean mScrollingCacheEnabled;
74
75    private boolean mPopulatePending;
76    private boolean mScrolling;
77
78    private boolean mIsBeingDragged;
79    private boolean mIsUnableToDrag;
80    private int mTouchSlop;
81    private float mInitialMotionX;
82    /**
83     * Position of the last motion event.
84     */
85    private float mLastMotionX;
86    private float mLastMotionY;
87    /**
88     * ID of the active pointer. This is used to retain consistency during
89     * drags/flings if multiple pointers are used.
90     */
91    private int mActivePointerId = INVALID_POINTER;
92    /**
93     * Sentinel value for no current active pointer.
94     * Used by {@link #mActivePointerId}.
95     */
96    private static final int INVALID_POINTER = -1;
97
98    /**
99     * Determines speed during touch scrolling
100     */
101    private VelocityTracker mVelocityTracker;
102    private int mMinimumVelocity;
103    private int mMaximumVelocity;
104
105    private boolean mFakeDragging;
106    private long mFakeDragBeginTime;
107
108    private boolean mFirstLayout = true;
109
110    private OnPageChangeListener mOnPageChangeListener;
111
112    /**
113     * Indicates that the pager is in an idle, settled state. The current page
114     * is fully in view and no animation is in progress.
115     */
116    public static final int SCROLL_STATE_IDLE = 0;
117
118    /**
119     * Indicates that the pager is currently being dragged by the user.
120     */
121    public static final int SCROLL_STATE_DRAGGING = 1;
122
123    /**
124     * Indicates that the pager is in the process of settling to a final position.
125     */
126    public static final int SCROLL_STATE_SETTLING = 2;
127
128    private int mScrollState = SCROLL_STATE_IDLE;
129
130    /**
131     * Callback interface for responding to changing state of the selected page.
132     */
133    public interface OnPageChangeListener {
134
135        /**
136         * This method will be invoked when the current page is scrolled, either as part
137         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
138         *
139         * @param position Position index of the first page currently being displayed.
140         *                 Page position+1 will be visible if positionOffset is nonzero.
141         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
142         * @param positionOffsetPixels Value in pixels indicating the offset from position.
143         */
144        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
145
146        /**
147         * This method will be invoked when a new page becomes selected. Animation is not
148         * necessarily complete.
149         *
150         * @param position Position index of the new selected page.
151         */
152        public void onPageSelected(int position);
153
154        /**
155         * Called when the scroll state changes. Useful for discovering when the user
156         * begins dragging, when the pager is automatically settling to the current page,
157         * or when it is fully stopped/idle.
158         *
159         * @param state The new scroll state.
160         * @see ViewPager#SCROLL_STATE_IDLE
161         * @see ViewPager#SCROLL_STATE_DRAGGING
162         * @see ViewPager#SCROLL_STATE_SETTLING
163         */
164        public void onPageScrollStateChanged(int state);
165    }
166
167    /**
168     * Simple implementation of the {@link OnPageChangeListener} interface with stub
169     * implementations of each method. Extend this if you do not intend to override
170     * every method of {@link OnPageChangeListener}.
171     */
172    public static class SimpleOnPageChangeListener implements OnPageChangeListener {
173        @Override
174        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
175            // This space for rent
176        }
177
178        @Override
179        public void onPageSelected(int position) {
180            // This space for rent
181        }
182
183        @Override
184        public void onPageScrollStateChanged(int state) {
185            // This space for rent
186        }
187    }
188
189    public ViewPager(Context context) {
190        super(context);
191        initViewPager();
192    }
193
194    public ViewPager(Context context, AttributeSet attrs) {
195        super(context, attrs);
196        initViewPager();
197    }
198
199    void initViewPager() {
200        setWillNotDraw(false);
201        mScroller = new Scroller(getContext());
202        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
203        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
204        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
205        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
206    }
207
208    private void setScrollState(int newState) {
209        if (mScrollState == newState) {
210            return;
211        }
212
213        mScrollState = newState;
214        if (mOnPageChangeListener != null) {
215            mOnPageChangeListener.onPageScrollStateChanged(newState);
216        }
217    }
218
219    public void setAdapter(PagerAdapter adapter) {
220        if (mAdapter != null) {
221            mAdapter.setDataSetObserver(null);
222            mAdapter.startUpdate(this);
223            for (int i = 0; i < mItems.size(); i++) {
224                final ItemInfo ii = mItems.get(i);
225                mAdapter.destroyItem(this, ii.position, ii.object);
226            }
227            mAdapter.finishUpdate(this);
228            mItems.clear();
229            removeAllViews();
230            mCurItem = 0;
231            scrollTo(0, 0);
232        }
233
234        mAdapter = adapter;
235
236        if (mAdapter != null) {
237            if (mObserver == null) {
238                mObserver = new DataSetObserver();
239            }
240            mAdapter.setDataSetObserver(mObserver);
241            mPopulatePending = false;
242            if (mRestoredCurItem >= 0) {
243                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
244                setCurrentItemInternal(mRestoredCurItem, false, true);
245                mRestoredCurItem = -1;
246                mRestoredAdapterState = null;
247                mRestoredClassLoader = null;
248            } else {
249                populate();
250            }
251        }
252    }
253
254    public PagerAdapter getAdapter() {
255        return mAdapter;
256    }
257
258    /**
259     * Set the currently selected page. If the ViewPager has already been through its first
260     * layout there will be a smooth animated transition between the current item and the
261     * specified item.
262     *
263     * @param item Item index to select
264     */
265    public void setCurrentItem(int item) {
266        mPopulatePending = false;
267        setCurrentItemInternal(item, !mFirstLayout, false);
268    }
269
270    /**
271     * Set the currently selected page.
272     *
273     * @param item Item index to select
274     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
275     */
276    public void setCurrentItem(int item, boolean smoothScroll) {
277        mPopulatePending = false;
278        setCurrentItemInternal(item, smoothScroll, false);
279    }
280
281    public int getCurrentItem() {
282        return mCurItem;
283    }
284
285    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
286        if (mAdapter == null || mAdapter.getCount() <= 0) {
287            setScrollingCacheEnabled(false);
288            return;
289        }
290        if (!always && mCurItem == item && mItems.size() != 0) {
291            setScrollingCacheEnabled(false);
292            return;
293        }
294        if (item < 0) {
295            item = 0;
296        } else if (item >= mAdapter.getCount()) {
297            item = mAdapter.getCount() - 1;
298        }
299        if (item > (mCurItem+1) || item < (mCurItem-1)) {
300            // We are doing a jump by more than one page.  To avoid
301            // glitches, we want to keep all current pages in the view
302            // until the scroll ends.
303            for (int i=0; i<mItems.size(); i++) {
304                mItems.get(i).scrolling = true;
305            }
306        }
307        final boolean dispatchSelected = mCurItem != item;
308        mCurItem = item;
309        populate();
310        if (smoothScroll) {
311            smoothScrollTo(getWidth()*item, 0);
312            if (dispatchSelected && mOnPageChangeListener != null) {
313                mOnPageChangeListener.onPageSelected(item);
314            }
315        } else {
316            if (dispatchSelected && mOnPageChangeListener != null) {
317                mOnPageChangeListener.onPageSelected(item);
318            }
319            completeScroll();
320            scrollTo(getWidth()*item, 0);
321        }
322    }
323
324    public void setOnPageChangeListener(OnPageChangeListener listener) {
325        mOnPageChangeListener = listener;
326    }
327
328    /**
329     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
330     *
331     * @param x the number of pixels to scroll by on the X axis
332     * @param y the number of pixels to scroll by on the Y axis
333     */
334    void smoothScrollTo(int x, int y) {
335        if (getChildCount() == 0) {
336            // Nothing to do.
337            setScrollingCacheEnabled(false);
338            return;
339        }
340        int sx = getScrollX();
341        int sy = getScrollY();
342        int dx = x - sx;
343        int dy = y - sy;
344        if (dx == 0 && dy == 0) {
345            completeScroll();
346            return;
347        }
348
349        setScrollingCacheEnabled(true);
350        mScrolling = true;
351        setScrollState(SCROLL_STATE_SETTLING);
352        mScroller.startScroll(sx, sy, dx, dy);
353        invalidate();
354    }
355
356    void addNewItem(int position, int index) {
357        ItemInfo ii = new ItemInfo();
358        ii.position = position;
359        ii.object = mAdapter.instantiateItem(this, position);
360        if (index < 0) {
361            mItems.add(ii);
362        } else {
363            mItems.add(index, ii);
364        }
365    }
366
367    void dataSetChanged() {
368        // This method only gets called if our observer is attached, so mAdapter is non-null.
369
370        boolean needPopulate = mItems.isEmpty() && mAdapter.getCount() > 0;
371        int newCurrItem = -1;
372
373        for (int i = 0; i < mItems.size(); i++) {
374            final ItemInfo ii = mItems.get(i);
375            final int newPos = mAdapter.getItemPosition(ii.object);
376
377            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
378                continue;
379            }
380
381            if (newPos == PagerAdapter.POSITION_NONE) {
382                mItems.remove(i);
383                i--;
384                mAdapter.destroyItem(this, ii.position, ii.object);
385                needPopulate = true;
386
387                if (mCurItem == ii.position) {
388                    // Keep the current item in the valid range
389                    newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1));
390                }
391                continue;
392            }
393
394            if (ii.position != newPos) {
395                if (ii.position == mCurItem) {
396                    // Our current item changed position. Follow it.
397                    newCurrItem = newPos;
398                }
399
400                ii.position = newPos;
401                needPopulate = true;
402            }
403        }
404
405        if (newCurrItem >= 0) {
406            // TODO This currently causes a jump.
407            setCurrentItemInternal(newCurrItem, false, true);
408            needPopulate = true;
409        }
410        if (needPopulate) {
411            populate();
412            requestLayout();
413        }
414    }
415
416    void populate() {
417        if (mAdapter == null) {
418            return;
419        }
420
421        // Bail now if we are waiting to populate.  This is to hold off
422        // on creating views from the time the user releases their finger to
423        // fling to a new position until we have finished the scroll to
424        // that position, avoiding glitches from happening at that point.
425        if (mPopulatePending) {
426            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
427            return;
428        }
429
430        // Also, don't populate until we are attached to a window.  This is to
431        // avoid trying to populate before we have restored our view hierarchy
432        // state and conflicting with what is restored.
433        if (getWindowToken() == null) {
434            return;
435        }
436
437        mAdapter.startUpdate(this);
438
439        final int startPos = mCurItem > 0 ? mCurItem - 1 : mCurItem;
440        final int N = mAdapter.getCount();
441        final int endPos = mCurItem < (N-1) ? mCurItem+1 : N-1;
442
443        if (DEBUG) Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos);
444
445        // Add and remove pages in the existing list.
446        int lastPos = -1;
447        for (int i=0; i<mItems.size(); i++) {
448            ItemInfo ii = mItems.get(i);
449            if ((ii.position < startPos || ii.position > endPos) && !ii.scrolling) {
450                if (DEBUG) Log.i(TAG, "removing: " + ii.position + " @ " + i);
451                mItems.remove(i);
452                i--;
453                mAdapter.destroyItem(this, ii.position, ii.object);
454            } else if (lastPos < endPos && ii.position > startPos) {
455                // The next item is outside of our range, but we have a gap
456                // between it and the last item where we want to have a page
457                // shown.  Fill in the gap.
458                lastPos++;
459                if (lastPos < startPos) {
460                    lastPos = startPos;
461                }
462                while (lastPos <= endPos && lastPos < ii.position) {
463                    if (DEBUG) Log.i(TAG, "inserting: " + lastPos + " @ " + i);
464                    addNewItem(lastPos, i);
465                    lastPos++;
466                    i++;
467                }
468            }
469            lastPos = ii.position;
470        }
471
472        // Add any new pages we need at the end.
473        lastPos = mItems.size() > 0 ? mItems.get(mItems.size()-1).position : -1;
474        if (lastPos < endPos) {
475            lastPos++;
476            lastPos = lastPos > startPos ? lastPos : startPos;
477            while (lastPos <= endPos) {
478                if (DEBUG) Log.i(TAG, "appending: " + lastPos);
479                addNewItem(lastPos, -1);
480                lastPos++;
481            }
482        }
483
484        if (DEBUG) {
485            Log.i(TAG, "Current page list:");
486            for (int i=0; i<mItems.size(); i++) {
487                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
488            }
489        }
490
491        mAdapter.finishUpdate(this);
492    }
493
494    public static class SavedState extends BaseSavedState {
495        int position;
496        Parcelable adapterState;
497        ClassLoader loader;
498
499        public SavedState(Parcelable superState) {
500            super(superState);
501        }
502
503        @Override
504        public void writeToParcel(Parcel out, int flags) {
505            super.writeToParcel(out, flags);
506            out.writeInt(position);
507            out.writeParcelable(adapterState, flags);
508        }
509
510        @Override
511        public String toString() {
512            return "FragmentPager.SavedState{"
513                    + Integer.toHexString(System.identityHashCode(this))
514                    + " position=" + position + "}";
515        }
516
517        public static final Parcelable.Creator<SavedState> CREATOR
518                = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
519                    @Override
520                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
521                        return new SavedState(in, loader);
522                    }
523                    @Override
524                    public SavedState[] newArray(int size) {
525                        return new SavedState[size];
526                    }
527                });
528
529        SavedState(Parcel in, ClassLoader loader) {
530            super(in);
531            if (loader == null) {
532                loader = getClass().getClassLoader();
533            }
534            position = in.readInt();
535            adapterState = in.readParcelable(loader);
536            this.loader = loader;
537        }
538    }
539
540    @Override
541    public Parcelable onSaveInstanceState() {
542        Parcelable superState = super.onSaveInstanceState();
543        SavedState ss = new SavedState(superState);
544        ss.position = mCurItem;
545        if (mAdapter != null) {
546            ss.adapterState = mAdapter.saveState();
547        }
548        return ss;
549    }
550
551    @Override
552    public void onRestoreInstanceState(Parcelable state) {
553        if (!(state instanceof SavedState)) {
554            super.onRestoreInstanceState(state);
555            return;
556        }
557
558        SavedState ss = (SavedState)state;
559        super.onRestoreInstanceState(ss.getSuperState());
560
561        if (mAdapter != null) {
562            mAdapter.restoreState(ss.adapterState, ss.loader);
563            setCurrentItemInternal(ss.position, false, true);
564        } else {
565            mRestoredCurItem = ss.position;
566            mRestoredAdapterState = ss.adapterState;
567            mRestoredClassLoader = ss.loader;
568        }
569    }
570
571    @Override
572    public void addView(View child, int index, LayoutParams params) {
573        if (mInLayout) {
574            addViewInLayout(child, index, params);
575            child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
576        } else {
577            super.addView(child, index, params);
578        }
579
580        if (USE_CACHE) {
581            if (child.getVisibility() != GONE) {
582                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
583            } else {
584                child.setDrawingCacheEnabled(false);
585            }
586        }
587    }
588
589    ItemInfo infoForChild(View child) {
590        for (int i=0; i<mItems.size(); i++) {
591            ItemInfo ii = mItems.get(i);
592            if (mAdapter.isViewFromObject(child, ii.object)) {
593                return ii;
594            }
595        }
596        return null;
597    }
598
599    @Override
600    protected void onAttachedToWindow() {
601        super.onAttachedToWindow();
602        mFirstLayout = true;
603        if (mAdapter != null) {
604            populate();
605        }
606    }
607
608    @Override
609    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
610        // For simple implementation, or internal size is always 0.
611        // We depend on the container to specify the layout size of
612        // our view.  We can't really know what it is since we will be
613        // adding and removing different arbitrary views and do not
614        // want the layout to change as this happens.
615        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
616                getDefaultSize(0, heightMeasureSpec));
617
618        // Children are just made to fill our space.
619        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
620                getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
621        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
622                getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
623
624        // Make sure we have created all fragments that we need to have shown.
625        mInLayout = true;
626        populate();
627        mInLayout = false;
628
629        // Make sure all children have been properly measured.
630        final int size = getChildCount();
631        for (int i = 0; i < size; ++i) {
632            final View child = getChildAt(i);
633            if (child.getVisibility() != GONE) {
634                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
635		        + ": " + mChildWidthMeasureSpec);
636                child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
637            }
638        }
639    }
640
641    @Override
642    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
643        super.onSizeChanged(w, h, oldw, oldh);
644
645        // Make sure scroll position is set correctly.
646        if (w != oldw) {
647            if (oldw > 0) {
648                final int oldScrollPos = getScrollX();
649                final int oldScrollItem = oldScrollPos / oldw;
650                final float scrollOffset = (float) (oldScrollPos % oldw) / oldw;
651                final int scrollPos = (int) ((oldScrollItem + scrollOffset) * w);
652                scrollTo(scrollPos, getScrollY());
653                if (!mScroller.isFinished()) {
654                    // We now return to your regularly scheduled scroll, already in progress.
655                    final int newDuration = mScroller.getDuration() - mScroller.timePassed();
656                    mScroller.startScroll(scrollPos, 0, mCurItem * w, 0, newDuration);
657                }
658            } else {
659                int scrollPos = mCurItem * w;
660                if (scrollPos != getScrollX()) {
661                    completeScroll();
662                    scrollTo(scrollPos, getScrollY());
663                }
664            }
665        }
666    }
667
668    @Override
669    protected void onLayout(boolean changed, int l, int t, int r, int b) {
670        mInLayout = true;
671        populate();
672        mInLayout = false;
673
674        final int count = getChildCount();
675        final int width = r-l;
676
677        for (int i = 0; i < count; i++) {
678            View child = getChildAt(i);
679            ItemInfo ii;
680            if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
681                int loff = width*ii.position;
682                int childLeft = getPaddingLeft() + loff;
683                int childTop = getPaddingTop();
684                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
685		        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
686		        + "x" + child.getMeasuredHeight());
687                child.layout(childLeft, childTop,
688                        childLeft + child.getMeasuredWidth(),
689                        childTop + child.getMeasuredHeight());
690            }
691        }
692        mFirstLayout = false;
693    }
694
695    @Override
696    public void computeScroll() {
697        if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
698        if (!mScroller.isFinished()) {
699            if (mScroller.computeScrollOffset()) {
700                if (DEBUG) Log.i(TAG, "computeScroll: still scrolling");
701                int oldX = getScrollX();
702                int oldY = getScrollY();
703                int x = mScroller.getCurrX();
704                int y = mScroller.getCurrY();
705
706                if (oldX != x || oldY != y) {
707                    scrollTo(x, y);
708                }
709
710                if (mOnPageChangeListener != null) {
711                    final int width = getWidth();
712                    final int position = x / width;
713                    final int offsetPixels = x % width;
714                    final float offset = (float) offsetPixels / width;
715                    mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
716                }
717
718                // Keep on drawing until the animation has finished.
719                invalidate();
720                return;
721            }
722        }
723
724        // Done with scroll, clean up state.
725        completeScroll();
726    }
727
728    private void completeScroll() {
729        boolean needPopulate = mScrolling;
730        if (needPopulate) {
731            // Done with scroll, no longer want to cache view drawing.
732            setScrollingCacheEnabled(false);
733            mScroller.abortAnimation();
734            int oldX = getScrollX();
735            int oldY = getScrollY();
736            int x = mScroller.getCurrX();
737            int y = mScroller.getCurrY();
738            if (oldX != x || oldY != y) {
739                scrollTo(x, y);
740            }
741            setScrollState(SCROLL_STATE_IDLE);
742        }
743        mPopulatePending = false;
744        mScrolling = false;
745        for (int i=0; i<mItems.size(); i++) {
746            ItemInfo ii = mItems.get(i);
747            if (ii.scrolling) {
748                needPopulate = true;
749                ii.scrolling = false;
750            }
751        }
752        if (needPopulate) {
753            populate();
754        }
755    }
756
757    @Override
758    public boolean onInterceptTouchEvent(MotionEvent ev) {
759        /*
760         * This method JUST determines whether we want to intercept the motion.
761         * If we return true, onMotionEvent will be called and we do the actual
762         * scrolling there.
763         */
764
765        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
766
767        // Always take care of the touch gesture being complete.
768        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
769            // Release the drag.
770            if (DEBUG) Log.v(TAG, "Intercept done!");
771            mIsBeingDragged = false;
772            mIsUnableToDrag = false;
773            mActivePointerId = INVALID_POINTER;
774            return false;
775        }
776
777        // Nothing more to do here if we have decided whether or not we
778        // are dragging.
779        if (action != MotionEvent.ACTION_DOWN) {
780            if (mIsBeingDragged) {
781                if (DEBUG) Log.v(TAG, "Intercept returning true!");
782                return true;
783            }
784            if (mIsUnableToDrag) {
785                if (DEBUG) Log.v(TAG, "Intercept returning false!");
786                return false;
787            }
788        }
789
790        switch (action) {
791            case MotionEvent.ACTION_MOVE: {
792                /*
793                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
794                 * whether the user has moved far enough from his original down touch.
795                 */
796
797                /*
798                * Locally do absolute value. mLastMotionY is set to the y value
799                * of the down event.
800                */
801                final int activePointerId = mActivePointerId;
802                if (activePointerId == INVALID_POINTER) {
803                    // If we don't have a valid id, the touch down wasn't on content.
804                    break;
805                }
806
807                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
808                final float x = MotionEventCompat.getX(ev, pointerIndex);
809                final float dx = x - mLastMotionX;
810                final float xDiff = Math.abs(dx);
811                final float y = MotionEventCompat.getY(ev, pointerIndex);
812                final float yDiff = Math.abs(y - mLastMotionY);
813                final int scrollX = getScrollX();
814                final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null &&
815                        scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1);
816                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
817
818                if (atEdge || canScroll(this, false, (int) dx, (int) x, (int) y)) {
819                    // Nested view has scrollable area under this point. Let it be handled there.
820                    mInitialMotionX = mLastMotionX = x;
821                    mLastMotionY = y;
822                    return false;
823                }
824                if (xDiff > mTouchSlop && xDiff > yDiff) {
825                    if (DEBUG) Log.v(TAG, "Starting drag!");
826                    mIsBeingDragged = true;
827                    setScrollState(SCROLL_STATE_DRAGGING);
828                    mLastMotionX = x;
829                    setScrollingCacheEnabled(true);
830                } else {
831                    if (yDiff > mTouchSlop) {
832                        // The finger has moved enough in the vertical
833                        // direction to be counted as a drag...  abort
834                        // any attempt to drag horizontally, to work correctly
835                        // with children that have scrolling containers.
836                        if (DEBUG) Log.v(TAG, "Starting unable to drag!");
837                        mIsUnableToDrag = true;
838                    }
839                }
840                break;
841            }
842
843            case MotionEvent.ACTION_DOWN: {
844                /*
845                 * Remember location of down touch.
846                 * ACTION_DOWN always refers to pointer index 0.
847                 */
848                mLastMotionX = mInitialMotionX = ev.getX();
849                mLastMotionY = ev.getY();
850                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
851
852                if (mScrollState == SCROLL_STATE_SETTLING) {
853                    // Let the user 'catch' the pager as it animates.
854                    mIsBeingDragged = true;
855                    mIsUnableToDrag = false;
856                    setScrollState(SCROLL_STATE_DRAGGING);
857                } else {
858                    completeScroll();
859                    mIsBeingDragged = false;
860                    mIsUnableToDrag = false;
861                }
862
863                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
864                        + " mIsBeingDragged=" + mIsBeingDragged
865                        + "mIsUnableToDrag=" + mIsUnableToDrag);
866                break;
867            }
868
869            case MotionEventCompat.ACTION_POINTER_UP:
870                onSecondaryPointerUp(ev);
871                break;
872        }
873
874        /*
875        * The only time we want to intercept motion events is if we are in the
876        * drag mode.
877        */
878        return mIsBeingDragged;
879    }
880
881    @Override
882    public boolean onTouchEvent(MotionEvent ev) {
883        if (mFakeDragging) {
884            // A fake drag is in progress already, ignore this real one
885            // but still eat the touch events.
886            // (It is likely that the user is multi-touching the screen.)
887            return true;
888        }
889
890        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
891            // Don't handle edge touches immediately -- they may actually belong to one of our
892            // descendants.
893            return false;
894        }
895
896        if (mAdapter == null || mAdapter.getCount() == 0) {
897            // Nothing to present or scroll; nothing to touch.
898            return false;
899        }
900
901        if (mVelocityTracker == null) {
902            mVelocityTracker = VelocityTracker.obtain();
903        }
904        mVelocityTracker.addMovement(ev);
905
906        final int action = ev.getAction();
907
908        switch (action & MotionEventCompat.ACTION_MASK) {
909            case MotionEvent.ACTION_DOWN: {
910                /*
911                 * If being flinged and user touches, stop the fling. isFinished
912                 * will be false if being flinged.
913                 */
914                completeScroll();
915
916                // Remember where the motion event started
917                mLastMotionX = mInitialMotionX = ev.getX();
918                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
919                break;
920            }
921            case MotionEvent.ACTION_MOVE:
922                if (!mIsBeingDragged) {
923                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
924                    final float x = MotionEventCompat.getX(ev, pointerIndex);
925                    final float xDiff = Math.abs(x - mLastMotionX);
926                    final float y = MotionEventCompat.getY(ev, pointerIndex);
927                    final float yDiff = Math.abs(y - mLastMotionY);
928                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
929                    if (xDiff > mTouchSlop && xDiff > yDiff) {
930                        if (DEBUG) Log.v(TAG, "Starting drag!");
931                        mIsBeingDragged = true;
932                        mLastMotionX = x;
933                        setScrollState(SCROLL_STATE_DRAGGING);
934                        setScrollingCacheEnabled(true);
935                    }
936                }
937                if (mIsBeingDragged) {
938                    // Scroll to follow the motion event
939                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
940                            ev, mActivePointerId);
941                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
942                    final float deltaX = mLastMotionX - x;
943                    mLastMotionX = x;
944                    float scrollX = getScrollX() + deltaX;
945                    final int width = getWidth();
946
947                    final float leftBound = Math.max(0, (mCurItem - 1) * width);
948                    final float rightBound =
949                            Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width;
950                    if (scrollX < leftBound) {
951                        scrollX = leftBound;
952                    } else if (scrollX > rightBound) {
953                        scrollX = rightBound;
954                    }
955                    // Don't lose the rounded component
956                    mLastMotionX += scrollX - (int) scrollX;
957                    scrollTo((int) scrollX, getScrollY());
958                    if (mOnPageChangeListener != null) {
959                        final int position = (int) scrollX / width;
960                        final int positionOffsetPixels = (int) scrollX % width;
961                        final float positionOffset = (float) positionOffsetPixels / width;
962                        mOnPageChangeListener.onPageScrolled(position, positionOffset,
963                                positionOffsetPixels);
964                    }
965                }
966                break;
967            case MotionEvent.ACTION_UP:
968                if (mIsBeingDragged) {
969                    final VelocityTracker velocityTracker = mVelocityTracker;
970                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
971                    int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
972                            velocityTracker, mActivePointerId);
973                    mPopulatePending = true;
974                    if ((Math.abs(initialVelocity) > mMinimumVelocity)
975                            || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
976                        if (mLastMotionX > mInitialMotionX) {
977                            setCurrentItemInternal(mCurItem-1, true, true);
978                        } else {
979                            setCurrentItemInternal(mCurItem+1, true, true);
980                        }
981                    } else {
982                        setCurrentItemInternal(mCurItem, true, true);
983                    }
984
985                    mActivePointerId = INVALID_POINTER;
986                    endDrag();
987                }
988                break;
989            case MotionEvent.ACTION_CANCEL:
990                if (mIsBeingDragged) {
991                    setCurrentItemInternal(mCurItem, true, true);
992                    mActivePointerId = INVALID_POINTER;
993                    endDrag();
994                }
995                break;
996            case MotionEventCompat.ACTION_POINTER_DOWN: {
997                final int index = MotionEventCompat.getActionIndex(ev);
998                final float x = MotionEventCompat.getX(ev, index);
999                mLastMotionX = x;
1000                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
1001                break;
1002            }
1003            case MotionEventCompat.ACTION_POINTER_UP:
1004                onSecondaryPointerUp(ev);
1005                mLastMotionX = MotionEventCompat.getX(ev,
1006                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
1007                break;
1008        }
1009        return true;
1010    }
1011
1012    /**
1013     * Start a fake drag of the pager.
1014     *
1015     * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
1016     * with the touch scrolling of another view, while still letting the ViewPager
1017     * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
1018     * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
1019     * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
1020     *
1021     * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
1022     * is already in progress, this method will return false.
1023     *
1024     * @return true if the fake drag began successfully, false if it could not be started.
1025     *
1026     * @see #fakeDragBy(float)
1027     * @see #endFakeDrag()
1028     */
1029    public boolean beginFakeDrag() {
1030        if (mIsBeingDragged) {
1031            return false;
1032        }
1033        mFakeDragging = true;
1034        setScrollState(SCROLL_STATE_DRAGGING);
1035        mInitialMotionX = mLastMotionX = 0;
1036        if (mVelocityTracker == null) {
1037            mVelocityTracker = VelocityTracker.obtain();
1038        } else {
1039            mVelocityTracker.clear();
1040        }
1041        final long time = SystemClock.uptimeMillis();
1042        final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
1043        mVelocityTracker.addMovement(ev);
1044        ev.recycle();
1045        mFakeDragBeginTime = time;
1046        return true;
1047    }
1048
1049    /**
1050     * End a fake drag of the pager.
1051     *
1052     * @see #beginFakeDrag()
1053     * @see #fakeDragBy(float)
1054     */
1055    public void endFakeDrag() {
1056        if (!mFakeDragging) {
1057            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
1058        }
1059
1060        final VelocityTracker velocityTracker = mVelocityTracker;
1061        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1062        int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
1063                velocityTracker, mActivePointerId);
1064        mPopulatePending = true;
1065        if ((Math.abs(initialVelocity) > mMinimumVelocity)
1066                || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
1067            if (mLastMotionX > mInitialMotionX) {
1068                setCurrentItemInternal(mCurItem-1, true, true);
1069            } else {
1070                setCurrentItemInternal(mCurItem+1, true, true);
1071            }
1072        } else {
1073            setCurrentItemInternal(mCurItem, true, true);
1074        }
1075        endDrag();
1076
1077        mFakeDragging = false;
1078    }
1079
1080    /**
1081     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
1082     *
1083     * @param xOffset Offset in pixels to drag by.
1084     * @see #beginFakeDrag()
1085     * @see #endFakeDrag()
1086     */
1087    public void fakeDragBy(float xOffset) {
1088        if (!mFakeDragging) {
1089            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
1090        }
1091
1092        mLastMotionX += xOffset;
1093        float scrollX = getScrollX() - xOffset;
1094        final int width = getWidth();
1095
1096        final float leftBound = Math.max(0, (mCurItem - 1) * width);
1097        final float rightBound =
1098                Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width;
1099        if (scrollX < leftBound) {
1100            scrollX = leftBound;
1101        } else if (scrollX > rightBound) {
1102            scrollX = rightBound;
1103        }
1104        // Don't lose the rounded component
1105        mLastMotionX += scrollX - (int) scrollX;
1106        scrollTo((int) scrollX, getScrollY());
1107        if (mOnPageChangeListener != null) {
1108            final int position = (int) scrollX / width;
1109            final int positionOffsetPixels = (int) scrollX % width;
1110            final float positionOffset = (float) positionOffsetPixels / width;
1111            mOnPageChangeListener.onPageScrolled(position, positionOffset,
1112                    positionOffsetPixels);
1113        }
1114
1115        // Synthesize an event for the VelocityTracker.
1116        final long time = SystemClock.uptimeMillis();
1117        final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
1118                mLastMotionX, 0, 0);
1119        mVelocityTracker.addMovement(ev);
1120        ev.recycle();
1121    }
1122
1123    /**
1124     * Returns true if a fake drag is in progress.
1125     *
1126     * @return true if currently in a fake drag, false otherwise.
1127     *
1128     * @see #beginFakeDrag()
1129     * @see #fakeDragBy(float)
1130     * @see #endFakeDrag()
1131     */
1132    public boolean isFakeDragging() {
1133        return mFakeDragging;
1134    }
1135
1136    private void onSecondaryPointerUp(MotionEvent ev) {
1137        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
1138        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
1139        if (pointerId == mActivePointerId) {
1140            // This was our active pointer going up. Choose a new
1141            // active pointer and adjust accordingly.
1142            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1143            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
1144            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
1145            if (mVelocityTracker != null) {
1146                mVelocityTracker.clear();
1147            }
1148        }
1149    }
1150
1151    private void endDrag() {
1152        mIsBeingDragged = false;
1153        mIsUnableToDrag = false;
1154
1155        if (mVelocityTracker != null) {
1156            mVelocityTracker.recycle();
1157            mVelocityTracker = null;
1158        }
1159    }
1160
1161    private void setScrollingCacheEnabled(boolean enabled) {
1162        if (mScrollingCacheEnabled != enabled) {
1163            mScrollingCacheEnabled = enabled;
1164            if (USE_CACHE) {
1165                final int size = getChildCount();
1166                for (int i = 0; i < size; ++i) {
1167                    final View child = getChildAt(i);
1168                    if (child.getVisibility() != GONE) {
1169                        child.setDrawingCacheEnabled(enabled);
1170                    }
1171                }
1172            }
1173        }
1174    }
1175
1176    /**
1177     * Tests scrollability within child views of v given a delta of dx.
1178     *
1179     * @param v View to test for horizontal scrollability
1180     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
1181     *               or just its children (false).
1182     * @param dx Delta scrolled in pixels
1183     * @param x X coordinate of the active touch point
1184     * @param y Y coordinate of the active touch point
1185     * @return true if child views of v can be scrolled by delta of dx.
1186     */
1187    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
1188        if (v instanceof ViewGroup) {
1189            final ViewGroup group = (ViewGroup) v;
1190            final int scrollX = v.getScrollX();
1191            final int scrollY = v.getScrollY();
1192            final int count = group.getChildCount();
1193            // Count backwards - let topmost views consume scroll distance first.
1194            for (int i = count - 1; i >= 0; i--) {
1195                // TODO: Add versioned support here for transformed views.
1196                // This will not work for transformed views in Honeycomb+
1197                final View child = group.getChildAt(i);
1198                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
1199                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
1200                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
1201                                y + scrollY - child.getTop())) {
1202                    return true;
1203                }
1204            }
1205        }
1206
1207        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
1208    }
1209
1210    private class DataSetObserver implements PagerAdapter.DataSetObserver {
1211        @Override
1212        public void onDataSetChanged() {
1213            dataSetChanged();
1214        }
1215    }
1216}
1217