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