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