ViewPager.java revision 388026a7c2dc75f106e60cbfbe655f5fb8b8200e
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        }
217
218        mAdapter = adapter;
219
220        if (mAdapter != null) {
221            if (mObserver == null) {
222                mObserver = new DataSetObserver();
223            }
224            mAdapter.setDataSetObserver(mObserver);
225            mPopulatePending = false;
226            if (mRestoredCurItem >= 0) {
227                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
228                setCurrentItemInternal(mRestoredCurItem, false, true);
229                mRestoredCurItem = -1;
230                mRestoredAdapterState = null;
231                mRestoredClassLoader = null;
232            } else {
233                populate();
234            }
235        }
236    }
237
238    public PagerAdapter getAdapter() {
239        return mAdapter;
240    }
241
242    public void setCurrentItem(int item) {
243        mPopulatePending = false;
244        setCurrentItemInternal(item, true, false);
245    }
246
247    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
248        if (mAdapter == null || mAdapter.getCount() <= 0) {
249            setScrollingCacheEnabled(false);
250            return;
251        }
252        if (!always && mCurItem == item && mItems.size() != 0) {
253            setScrollingCacheEnabled(false);
254            return;
255        }
256        if (item < 0) {
257            item = 0;
258        } else if (item >= mAdapter.getCount()) {
259            item = mAdapter.getCount() - 1;
260        }
261        if (item > (mCurItem+1) || item < (mCurItem-1)) {
262            // We are doing a jump by more than one page.  To avoid
263            // glitches, we want to keep all current pages in the view
264            // until the scroll ends.
265            for (int i=0; i<mItems.size(); i++) {
266                mItems.get(i).scrolling = true;
267            }
268        }
269        final boolean dispatchSelected = mCurItem != item;
270        mCurItem = item;
271        populate();
272        if (smoothScroll) {
273            smoothScrollTo(getWidth()*item, 0);
274            if (dispatchSelected && mOnPageChangeListener != null) {
275                mOnPageChangeListener.onPageSelected(item);
276            }
277        } else {
278            if (dispatchSelected && mOnPageChangeListener != null) {
279                mOnPageChangeListener.onPageSelected(item);
280            }
281            completeScroll();
282            scrollTo(getWidth()*item, 0);
283        }
284    }
285
286    public void setOnPageChangeListener(OnPageChangeListener listener) {
287        mOnPageChangeListener = listener;
288    }
289
290    /**
291     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
292     *
293     * @param dx the number of pixels to scroll by on the X axis
294     * @param dy the number of pixels to scroll by on the Y axis
295     */
296    void smoothScrollTo(int x, int y) {
297        if (getChildCount() == 0) {
298            // Nothing to do.
299            setScrollingCacheEnabled(false);
300            return;
301        }
302        int sx = getScrollX();
303        int sy = getScrollY();
304        int dx = x - sx;
305        int dy = y - sy;
306        if (dx == 0 && dy == 0) {
307            completeScroll();
308            return;
309        }
310
311        setScrollingCacheEnabled(true);
312        mScrolling = true;
313        setScrollState(SCROLL_STATE_SETTLING);
314        mScroller.startScroll(sx, sy, dx, dy);
315        invalidate();
316    }
317
318    void addNewItem(int position, int index) {
319        ItemInfo ii = new ItemInfo();
320        ii.position = position;
321        ii.object = mAdapter.instantiateItem(this, position);
322        if (index < 0) {
323            mItems.add(ii);
324        } else {
325            mItems.add(index, ii);
326        }
327    }
328
329    void dataSetChanged() {
330        // This method only gets called if our observer is attached, so mAdapter is non-null.
331
332        boolean needPopulate = mItems.isEmpty() && mAdapter.getCount() > 0;
333        int newCurrItem = -1;
334
335        for (int i = 0; i < mItems.size(); i++) {
336            final ItemInfo ii = mItems.get(i);
337            final int newPos = mAdapter.getItemPosition(ii.object);
338
339            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
340                continue;
341            }
342
343            if (newPos == PagerAdapter.POSITION_NONE) {
344                mItems.remove(i);
345                i--;
346                mAdapter.destroyItem(this, ii.position, ii.object);
347                needPopulate = true;
348
349                if (mCurItem == ii.position) {
350                    // Keep the current item in the valid range
351                    newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1));
352                }
353                continue;
354            }
355
356            if (ii.position != newPos) {
357                if (ii.position == mCurItem) {
358                    // Our current item changed position. Follow it.
359                    newCurrItem = newPos;
360                }
361
362                ii.position = newPos;
363                needPopulate = true;
364            }
365        }
366
367        if (newCurrItem >= 0) {
368            // TODO This currently causes a jump.
369            setCurrentItemInternal(newCurrItem, false, true);
370            needPopulate = true;
371        }
372        if (needPopulate) {
373            populate();
374            requestLayout();
375        }
376    }
377
378    void populate() {
379        if (mAdapter == null) {
380            return;
381        }
382
383        // Bail now if we are waiting to populate.  This is to hold off
384        // on creating views from the time the user releases their finger to
385        // fling to a new position until we have finished the scroll to
386        // that position, avoiding glitches from happening at that point.
387        if (mPopulatePending) {
388            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
389            return;
390        }
391
392        // Also, don't populate until we are attached to a window.  This is to
393        // avoid trying to populate before we have restored our view hierarchy
394        // state and conflicting with what is restored.
395        if (getWindowToken() == null) {
396            return;
397        }
398
399        mAdapter.startUpdate(this);
400
401        final int startPos = mCurItem > 0 ? mCurItem - 1 : mCurItem;
402        final int N = mAdapter.getCount();
403        final int endPos = mCurItem < (N-1) ? mCurItem+1 : N-1;
404
405        if (DEBUG) Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos);
406
407        // Add and remove pages in the existing list.
408        int lastPos = -1;
409        for (int i=0; i<mItems.size(); i++) {
410            ItemInfo ii = mItems.get(i);
411            if ((ii.position < startPos || ii.position > endPos) && !ii.scrolling) {
412                if (DEBUG) Log.i(TAG, "removing: " + ii.position + " @ " + i);
413                mItems.remove(i);
414                i--;
415                mAdapter.destroyItem(this, ii.position, ii.object);
416            } else if (lastPos < endPos && ii.position > startPos) {
417                // The next item is outside of our range, but we have a gap
418                // between it and the last item where we want to have a page
419                // shown.  Fill in the gap.
420                lastPos++;
421                if (lastPos < startPos) {
422                    lastPos = startPos;
423                }
424                while (lastPos <= endPos && lastPos < ii.position) {
425                    if (DEBUG) Log.i(TAG, "inserting: " + lastPos + " @ " + i);
426                    addNewItem(lastPos, i);
427                    lastPos++;
428                    i++;
429                }
430            }
431            lastPos = ii.position;
432        }
433
434        // Add any new pages we need at the end.
435        lastPos = mItems.size() > 0 ? mItems.get(mItems.size()-1).position : -1;
436        if (lastPos < endPos) {
437            lastPos++;
438            lastPos = lastPos > startPos ? lastPos : startPos;
439            while (lastPos <= endPos) {
440                if (DEBUG) Log.i(TAG, "appending: " + lastPos);
441                addNewItem(lastPos, -1);
442                lastPos++;
443            }
444        }
445
446        if (DEBUG) {
447            Log.i(TAG, "Current page list:");
448            for (int i=0; i<mItems.size(); i++) {
449                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
450            }
451        }
452
453        mAdapter.finishUpdate(this);
454    }
455
456    public static class SavedState extends BaseSavedState {
457        int position;
458        Parcelable adapterState;
459        ClassLoader loader;
460
461        public SavedState(Parcelable superState) {
462            super(superState);
463        }
464
465        @Override
466        public void writeToParcel(Parcel out, int flags) {
467            super.writeToParcel(out, flags);
468            out.writeInt(position);
469            out.writeParcelable(adapterState, flags);
470        }
471
472        @Override
473        public String toString() {
474            return "FragmentPager.SavedState{"
475                    + Integer.toHexString(System.identityHashCode(this))
476                    + " position=" + position + "}";
477        }
478
479        public static final Parcelable.Creator<SavedState> CREATOR
480                = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
481                    @Override
482                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
483                        return new SavedState(in, loader);
484                    }
485                    @Override
486                    public SavedState[] newArray(int size) {
487                        return new SavedState[size];
488                    }
489                });
490
491        SavedState(Parcel in, ClassLoader loader) {
492            super(in);
493            if (loader == null) {
494                loader = getClass().getClassLoader();
495            }
496            position = in.readInt();
497            adapterState = in.readParcelable(loader);
498            this.loader = loader;
499        }
500    }
501
502    @Override
503    public Parcelable onSaveInstanceState() {
504        Parcelable superState = super.onSaveInstanceState();
505        SavedState ss = new SavedState(superState);
506        ss.position = mCurItem;
507        ss.adapterState = mAdapter.saveState();
508        return ss;
509    }
510
511    @Override
512    public void onRestoreInstanceState(Parcelable state) {
513        if (!(state instanceof SavedState)) {
514            super.onRestoreInstanceState(state);
515            return;
516        }
517
518        SavedState ss = (SavedState)state;
519        super.onRestoreInstanceState(ss.getSuperState());
520
521        if (mAdapter != null) {
522            mAdapter.restoreState(ss.adapterState, ss.loader);
523            setCurrentItemInternal(ss.position, false, true);
524        } else {
525            mRestoredCurItem = ss.position;
526            mRestoredAdapterState = ss.adapterState;
527            mRestoredClassLoader = ss.loader;
528        }
529    }
530
531    @Override
532    public void addView(View child, int index, LayoutParams params) {
533        if (mInLayout) {
534            addViewInLayout(child, index, params);
535            child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
536        } else {
537            super.addView(child, index, params);
538        }
539
540        if (USE_CACHE) {
541            if (child.getVisibility() != GONE) {
542                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
543            } else {
544                child.setDrawingCacheEnabled(false);
545            }
546        }
547    }
548
549    ItemInfo infoForChild(View child) {
550        for (int i=0; i<mItems.size(); i++) {
551            ItemInfo ii = mItems.get(i);
552            if (mAdapter.isViewFromObject(child, ii.object)) {
553                return ii;
554            }
555        }
556        return null;
557    }
558
559    @Override
560    protected void onAttachedToWindow() {
561        super.onAttachedToWindow();
562        if (mAdapter != null) {
563            populate();
564        }
565    }
566
567    @Override
568    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
569        // For simple implementation, or internal size is always 0.
570        // We depend on the container to specify the layout size of
571        // our view.  We can't really know what it is since we will be
572        // adding and removing different arbitrary views and do not
573        // want the layout to change as this happens.
574        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
575                getDefaultSize(0, heightMeasureSpec));
576
577        // Children are just made to fill our space.
578        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
579                getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
580        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
581                getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
582
583        // Make sure we have created all fragments that we need to have shown.
584        mInLayout = true;
585        populate();
586        mInLayout = false;
587
588        // Make sure all children have been properly measured.
589        final int size = getChildCount();
590        for (int i = 0; i < size; ++i) {
591            final View child = getChildAt(i);
592            if (child.getVisibility() != GONE) {
593                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
594		        + ": " + mChildWidthMeasureSpec);
595                child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
596            }
597        }
598    }
599
600    @Override
601    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
602        super.onSizeChanged(w, h, oldw, oldh);
603
604        // Make sure scroll position is set correctly.
605        int scrollPos = mCurItem*w;
606        if (scrollPos != getScrollX()) {
607            completeScroll();
608            scrollTo(scrollPos, getScrollY());
609        }
610    }
611
612    @Override
613    protected void onLayout(boolean changed, int l, int t, int r, int b) {
614        mInLayout = true;
615        populate();
616        mInLayout = false;
617
618        final int count = getChildCount();
619        final int width = r-l;
620
621        for (int i = 0; i < count; i++) {
622            View child = getChildAt(i);
623            ItemInfo ii;
624            if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
625                int loff = width*ii.position;
626                int childLeft = getPaddingLeft() + loff;
627                int childTop = getPaddingTop();
628                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
629		        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
630		        + "x" + child.getMeasuredHeight());
631                child.layout(childLeft, childTop,
632                        childLeft + child.getMeasuredWidth(),
633                        childTop + child.getMeasuredHeight());
634            }
635        }
636    }
637
638    @Override
639    public void computeScroll() {
640        if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
641        if (!mScroller.isFinished()) {
642            if (mScroller.computeScrollOffset()) {
643                if (DEBUG) Log.i(TAG, "computeScroll: still scrolling");
644                int oldX = getScrollX();
645                int oldY = getScrollY();
646                int x = mScroller.getCurrX();
647                int y = mScroller.getCurrY();
648
649                if (oldX != x || oldY != y) {
650                    scrollTo(x, y);
651                }
652
653                if (mOnPageChangeListener != null) {
654                    final int width = getWidth();
655                    final int position = x / width;
656                    final int offsetPixels = x % width;
657                    final float offset = (float) offsetPixels / width;
658                    mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
659                }
660
661                // Keep on drawing until the animation has finished.
662                invalidate();
663                return;
664            }
665        }
666
667        // Done with scroll, clean up state.
668        completeScroll();
669    }
670
671    private void completeScroll() {
672        boolean needPopulate;
673        if ((needPopulate=mScrolling)) {
674            // Done with scroll, no longer want to cache view drawing.
675            setScrollingCacheEnabled(false);
676            mScroller.abortAnimation();
677            int oldX = getScrollX();
678            int oldY = getScrollY();
679            int x = mScroller.getCurrX();
680            int y = mScroller.getCurrY();
681            if (oldX != x || oldY != y) {
682                scrollTo(x, y);
683            }
684            setScrollState(SCROLL_STATE_IDLE);
685        }
686        mPopulatePending = false;
687        mScrolling = false;
688        for (int i=0; i<mItems.size(); i++) {
689            ItemInfo ii = mItems.get(i);
690            if (ii.scrolling) {
691                needPopulate = true;
692                ii.scrolling = false;
693            }
694        }
695        if (needPopulate) {
696            populate();
697        }
698    }
699
700    @Override
701    public boolean onInterceptTouchEvent(MotionEvent ev) {
702        /*
703         * This method JUST determines whether we want to intercept the motion.
704         * If we return true, onMotionEvent will be called and we do the actual
705         * scrolling there.
706         */
707
708        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
709
710        // Always take care of the touch gesture being complete.
711        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
712            // Release the drag.
713            if (DEBUG) Log.v(TAG, "Intercept done!");
714            mIsBeingDragged = false;
715            mIsUnableToDrag = false;
716            mActivePointerId = INVALID_POINTER;
717            return false;
718        }
719
720        // Nothing more to do here if we have decided whether or not we
721        // are dragging.
722        if (action != MotionEvent.ACTION_DOWN) {
723            if (mIsBeingDragged) {
724                if (DEBUG) Log.v(TAG, "Intercept returning true!");
725                return true;
726            }
727            if (mIsUnableToDrag) {
728                if (DEBUG) Log.v(TAG, "Intercept returning false!");
729                return false;
730            }
731        }
732
733        switch (action) {
734            case MotionEvent.ACTION_MOVE: {
735                /*
736                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
737                 * whether the user has moved far enough from his original down touch.
738                 */
739
740                /*
741                * Locally do absolute value. mLastMotionY is set to the y value
742                * of the down event.
743                */
744                final int activePointerId = mActivePointerId;
745                if (activePointerId == INVALID_POINTER) {
746                    // If we don't have a valid id, the touch down wasn't on content.
747                    break;
748                }
749
750                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
751                final float x = MotionEventCompat.getX(ev, pointerIndex);
752                final float dx = x - mLastMotionX;
753                final float xDiff = Math.abs(dx);
754                final float y = MotionEventCompat.getY(ev, pointerIndex);
755                final float yDiff = Math.abs(y - mLastMotionY);
756                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
757
758                if (xDiff > mTouchSlop && xDiff > yDiff) {
759                    if (DEBUG) Log.v(TAG, "Starting drag!");
760                    mIsBeingDragged = true;
761                    setScrollState(SCROLL_STATE_DRAGGING);
762                    mLastMotionX = x;
763                    setScrollingCacheEnabled(true);
764                } else {
765                    if (yDiff > mTouchSlop) {
766                        // The finger has moved enough in the vertical
767                        // direction to be counted as a drag...  abort
768                        // any attempt to drag horizontally, to work correctly
769                        // with children that have scrolling containers.
770                        if (DEBUG) Log.v(TAG, "Starting unable to drag!");
771                        mIsUnableToDrag = true;
772                    }
773                }
774                break;
775            }
776
777            case MotionEvent.ACTION_DOWN: {
778                /*
779                 * Remember location of down touch.
780                 * ACTION_DOWN always refers to pointer index 0.
781                 */
782                mLastMotionX = mInitialMotionX = ev.getX();
783                mLastMotionY = ev.getY();
784                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
785
786                if (mScrollState == SCROLL_STATE_SETTLING) {
787                    // Let the user 'catch' the pager as it animates.
788                    mIsBeingDragged = true;
789                    mIsUnableToDrag = false;
790                    setScrollState(SCROLL_STATE_DRAGGING);
791                } else {
792                    completeScroll();
793                    mIsBeingDragged = false;
794                    mIsUnableToDrag = false;
795                }
796
797                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
798                        + " mIsBeingDragged=" + mIsBeingDragged
799                        + "mIsUnableToDrag=" + mIsUnableToDrag);
800                break;
801            }
802
803            case MotionEventCompat.ACTION_POINTER_UP:
804                onSecondaryPointerUp(ev);
805                break;
806        }
807
808        /*
809        * The only time we want to intercept motion events is if we are in the
810        * drag mode.
811        */
812        return mIsBeingDragged;
813    }
814
815    @Override
816    public boolean onTouchEvent(MotionEvent ev) {
817
818        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
819            // Don't handle edge touches immediately -- they may actually belong to one of our
820            // descendants.
821            return false;
822        }
823
824        if (mAdapter == null || mAdapter.getCount() == 0) {
825            // Nothing to present or scroll; nothing to touch.
826            return false;
827        }
828
829        if (mVelocityTracker == null) {
830            mVelocityTracker = VelocityTracker.obtain();
831        }
832        mVelocityTracker.addMovement(ev);
833
834        final int action = ev.getAction();
835
836        switch (action & MotionEventCompat.ACTION_MASK) {
837            case MotionEvent.ACTION_DOWN: {
838                /*
839                 * If being flinged and user touches, stop the fling. isFinished
840                 * will be false if being flinged.
841                 */
842                completeScroll();
843
844                // Remember where the motion event started
845                mLastMotionX = mInitialMotionX = ev.getX();
846                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
847                break;
848            }
849            case MotionEvent.ACTION_MOVE:
850                if (!mIsBeingDragged) {
851                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
852                    final float x = MotionEventCompat.getX(ev, pointerIndex);
853                    final float xDiff = Math.abs(x - mLastMotionX);
854                    final float y = MotionEventCompat.getY(ev, pointerIndex);
855                    final float yDiff = Math.abs(y - mLastMotionY);
856                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
857                    if (xDiff > mTouchSlop && xDiff > yDiff) {
858                        if (DEBUG) Log.v(TAG, "Starting drag!");
859                        mIsBeingDragged = true;
860                        mLastMotionX = x;
861                        setScrollState(SCROLL_STATE_DRAGGING);
862                        setScrollingCacheEnabled(true);
863                    }
864                }
865                if (mIsBeingDragged) {
866                    // Scroll to follow the motion event
867                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
868                            ev, mActivePointerId);
869                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
870                    final float deltaX = mLastMotionX - x;
871                    mLastMotionX = x;
872                    float scrollX = getScrollX() + deltaX;
873                    final int width = getWidth();
874
875                    final float leftBound = Math.max(0, (mCurItem - 1) * width);
876                    final float rightBound =
877                            Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width;
878                    if (scrollX < leftBound) {
879                        scrollX = leftBound;
880                    } else if (scrollX > rightBound) {
881                        scrollX = rightBound;
882                    }
883                    // Don't lose the rounded component
884                    mLastMotionX += scrollX - (int) scrollX;
885                    scrollTo((int) scrollX, getScrollY());
886                    if (mOnPageChangeListener != null) {
887                        final int position = (int) scrollX / width;
888                        final int positionOffsetPixels = (int) scrollX % width;
889                        final float positionOffset = (float) positionOffsetPixels / width;
890                        mOnPageChangeListener.onPageScrolled(position, positionOffset,
891                                positionOffsetPixels);
892                    }
893                }
894                break;
895            case MotionEvent.ACTION_UP:
896                if (mIsBeingDragged) {
897                    final VelocityTracker velocityTracker = mVelocityTracker;
898                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
899                    int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
900                            velocityTracker, mActivePointerId);
901                    mPopulatePending = true;
902                    if ((Math.abs(initialVelocity) > mMinimumVelocity)
903                            || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
904                        if (mLastMotionX > mInitialMotionX) {
905                            setCurrentItemInternal(mCurItem-1, true, true);
906                        } else {
907                            setCurrentItemInternal(mCurItem+1, true, true);
908                        }
909                    } else {
910                        setCurrentItemInternal(mCurItem, true, true);
911                    }
912
913                    mActivePointerId = INVALID_POINTER;
914                    endDrag();
915                }
916                break;
917            case MotionEvent.ACTION_CANCEL:
918                if (mIsBeingDragged) {
919                    setCurrentItemInternal(mCurItem, true, true);
920                    mActivePointerId = INVALID_POINTER;
921                    endDrag();
922                }
923                break;
924            case MotionEventCompat.ACTION_POINTER_DOWN: {
925                final int index = MotionEventCompat.getActionIndex(ev);
926                final float x = MotionEventCompat.getX(ev, index);
927                mLastMotionX = x;
928                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
929                break;
930            }
931            case MotionEventCompat.ACTION_POINTER_UP:
932                onSecondaryPointerUp(ev);
933                mLastMotionX = MotionEventCompat.getX(ev,
934                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
935                break;
936        }
937        return true;
938    }
939
940    private void onSecondaryPointerUp(MotionEvent ev) {
941        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
942        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
943        if (pointerId == mActivePointerId) {
944            // This was our active pointer going up. Choose a new
945            // active pointer and adjust accordingly.
946            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
947            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
948            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
949            if (mVelocityTracker != null) {
950                mVelocityTracker.clear();
951            }
952        }
953    }
954
955    private void endDrag() {
956        mIsBeingDragged = false;
957        mIsUnableToDrag = false;
958
959        if (mVelocityTracker != null) {
960            mVelocityTracker.recycle();
961            mVelocityTracker = null;
962        }
963    }
964
965    private void setScrollingCacheEnabled(boolean enabled) {
966        if (mScrollingCacheEnabled != enabled) {
967            mScrollingCacheEnabled = enabled;
968            if (USE_CACHE) {
969                final int size = getChildCount();
970                for (int i = 0; i < size; ++i) {
971                    final View child = getChildAt(i);
972                    if (child.getVisibility() != GONE) {
973                        child.setDrawingCacheEnabled(enabled);
974                    }
975                }
976            }
977        }
978    }
979
980    private class DataSetObserver implements PagerAdapter.DataSetObserver {
981        @Override
982        public void onDataSetChanged() {
983            dataSetChanged();
984        }
985    }
986}
987