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