ViewPager.java revision 47dc57337c13a2760ea3512ad84d6aa9f24ccd28
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        if (mAdapter != null) {
508            ss.adapterState = mAdapter.saveState();
509        }
510        return ss;
511    }
512
513    @Override
514    public void onRestoreInstanceState(Parcelable state) {
515        if (!(state instanceof SavedState)) {
516            super.onRestoreInstanceState(state);
517            return;
518        }
519
520        SavedState ss = (SavedState)state;
521        super.onRestoreInstanceState(ss.getSuperState());
522
523        if (mAdapter != null) {
524            mAdapter.restoreState(ss.adapterState, ss.loader);
525            setCurrentItemInternal(ss.position, false, true);
526        } else {
527            mRestoredCurItem = ss.position;
528            mRestoredAdapterState = ss.adapterState;
529            mRestoredClassLoader = ss.loader;
530        }
531    }
532
533    @Override
534    public void addView(View child, int index, LayoutParams params) {
535        if (mInLayout) {
536            addViewInLayout(child, index, params);
537            child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
538        } else {
539            super.addView(child, index, params);
540        }
541
542        if (USE_CACHE) {
543            if (child.getVisibility() != GONE) {
544                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
545            } else {
546                child.setDrawingCacheEnabled(false);
547            }
548        }
549    }
550
551    ItemInfo infoForChild(View child) {
552        for (int i=0; i<mItems.size(); i++) {
553            ItemInfo ii = mItems.get(i);
554            if (mAdapter.isViewFromObject(child, ii.object)) {
555                return ii;
556            }
557        }
558        return null;
559    }
560
561    @Override
562    protected void onAttachedToWindow() {
563        super.onAttachedToWindow();
564        if (mAdapter != null) {
565            populate();
566        }
567    }
568
569    @Override
570    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
571        // For simple implementation, or internal size is always 0.
572        // We depend on the container to specify the layout size of
573        // our view.  We can't really know what it is since we will be
574        // adding and removing different arbitrary views and do not
575        // want the layout to change as this happens.
576        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
577                getDefaultSize(0, heightMeasureSpec));
578
579        // Children are just made to fill our space.
580        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
581                getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
582        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
583                getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
584
585        // Make sure we have created all fragments that we need to have shown.
586        mInLayout = true;
587        populate();
588        mInLayout = false;
589
590        // Make sure all children have been properly measured.
591        final int size = getChildCount();
592        for (int i = 0; i < size; ++i) {
593            final View child = getChildAt(i);
594            if (child.getVisibility() != GONE) {
595                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
596		        + ": " + mChildWidthMeasureSpec);
597                child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
598            }
599        }
600    }
601
602    @Override
603    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
604        super.onSizeChanged(w, h, oldw, oldh);
605
606        // Make sure scroll position is set correctly.
607        int scrollPos = mCurItem*w;
608        if (scrollPos != getScrollX()) {
609            completeScroll();
610            scrollTo(scrollPos, getScrollY());
611        }
612    }
613
614    @Override
615    protected void onLayout(boolean changed, int l, int t, int r, int b) {
616        mInLayout = true;
617        populate();
618        mInLayout = false;
619
620        final int count = getChildCount();
621        final int width = r-l;
622
623        for (int i = 0; i < count; i++) {
624            View child = getChildAt(i);
625            ItemInfo ii;
626            if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
627                int loff = width*ii.position;
628                int childLeft = getPaddingLeft() + loff;
629                int childTop = getPaddingTop();
630                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
631		        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
632		        + "x" + child.getMeasuredHeight());
633                child.layout(childLeft, childTop,
634                        childLeft + child.getMeasuredWidth(),
635                        childTop + child.getMeasuredHeight());
636            }
637        }
638    }
639
640    @Override
641    public void computeScroll() {
642        if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
643        if (!mScroller.isFinished()) {
644            if (mScroller.computeScrollOffset()) {
645                if (DEBUG) Log.i(TAG, "computeScroll: still scrolling");
646                int oldX = getScrollX();
647                int oldY = getScrollY();
648                int x = mScroller.getCurrX();
649                int y = mScroller.getCurrY();
650
651                if (oldX != x || oldY != y) {
652                    scrollTo(x, y);
653                }
654
655                if (mOnPageChangeListener != null) {
656                    final int width = getWidth();
657                    final int position = x / width;
658                    final int offsetPixels = x % width;
659                    final float offset = (float) offsetPixels / width;
660                    mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
661                }
662
663                // Keep on drawing until the animation has finished.
664                invalidate();
665                return;
666            }
667        }
668
669        // Done with scroll, clean up state.
670        completeScroll();
671    }
672
673    private void completeScroll() {
674        boolean needPopulate;
675        if ((needPopulate=mScrolling)) {
676            // Done with scroll, no longer want to cache view drawing.
677            setScrollingCacheEnabled(false);
678            mScroller.abortAnimation();
679            int oldX = getScrollX();
680            int oldY = getScrollY();
681            int x = mScroller.getCurrX();
682            int y = mScroller.getCurrY();
683            if (oldX != x || oldY != y) {
684                scrollTo(x, y);
685            }
686            setScrollState(SCROLL_STATE_IDLE);
687        }
688        mPopulatePending = false;
689        mScrolling = false;
690        for (int i=0; i<mItems.size(); i++) {
691            ItemInfo ii = mItems.get(i);
692            if (ii.scrolling) {
693                needPopulate = true;
694                ii.scrolling = false;
695            }
696        }
697        if (needPopulate) {
698            populate();
699        }
700    }
701
702    @Override
703    public boolean onInterceptTouchEvent(MotionEvent ev) {
704        /*
705         * This method JUST determines whether we want to intercept the motion.
706         * If we return true, onMotionEvent will be called and we do the actual
707         * scrolling there.
708         */
709
710        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
711
712        // Always take care of the touch gesture being complete.
713        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
714            // Release the drag.
715            if (DEBUG) Log.v(TAG, "Intercept done!");
716            mIsBeingDragged = false;
717            mIsUnableToDrag = false;
718            mActivePointerId = INVALID_POINTER;
719            return false;
720        }
721
722        // Nothing more to do here if we have decided whether or not we
723        // are dragging.
724        if (action != MotionEvent.ACTION_DOWN) {
725            if (mIsBeingDragged) {
726                if (DEBUG) Log.v(TAG, "Intercept returning true!");
727                return true;
728            }
729            if (mIsUnableToDrag) {
730                if (DEBUG) Log.v(TAG, "Intercept returning false!");
731                return false;
732            }
733        }
734
735        switch (action) {
736            case MotionEvent.ACTION_MOVE: {
737                /*
738                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
739                 * whether the user has moved far enough from his original down touch.
740                 */
741
742                /*
743                * Locally do absolute value. mLastMotionY is set to the y value
744                * of the down event.
745                */
746                final int activePointerId = mActivePointerId;
747                if (activePointerId == INVALID_POINTER) {
748                    // If we don't have a valid id, the touch down wasn't on content.
749                    break;
750                }
751
752                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
753                final float x = MotionEventCompat.getX(ev, pointerIndex);
754                final float dx = x - mLastMotionX;
755                final float xDiff = Math.abs(dx);
756                final float y = MotionEventCompat.getY(ev, pointerIndex);
757                final float yDiff = Math.abs(y - mLastMotionY);
758                final int scrollX = getScrollX();
759                final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null &&
760                        scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1);
761                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
762
763                if (atEdge || canScroll(this, false, (int) dx, (int) x, (int) y)) {
764                    // Nested view has scrollable area under this point. Let it be handled there.
765                    mInitialMotionX = mLastMotionX = x;
766                    mLastMotionY = y;
767                    return false;
768                }
769                if (xDiff > mTouchSlop && xDiff > yDiff) {
770                    if (DEBUG) Log.v(TAG, "Starting drag!");
771                    mIsBeingDragged = true;
772                    setScrollState(SCROLL_STATE_DRAGGING);
773                    mLastMotionX = x;
774                    setScrollingCacheEnabled(true);
775                } else {
776                    if (yDiff > mTouchSlop) {
777                        // The finger has moved enough in the vertical
778                        // direction to be counted as a drag...  abort
779                        // any attempt to drag horizontally, to work correctly
780                        // with children that have scrolling containers.
781                        if (DEBUG) Log.v(TAG, "Starting unable to drag!");
782                        mIsUnableToDrag = true;
783                    }
784                }
785                break;
786            }
787
788            case MotionEvent.ACTION_DOWN: {
789                /*
790                 * Remember location of down touch.
791                 * ACTION_DOWN always refers to pointer index 0.
792                 */
793                mLastMotionX = mInitialMotionX = ev.getX();
794                mLastMotionY = ev.getY();
795                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
796
797                if (mScrollState == SCROLL_STATE_SETTLING) {
798                    // Let the user 'catch' the pager as it animates.
799                    mIsBeingDragged = true;
800                    mIsUnableToDrag = false;
801                    setScrollState(SCROLL_STATE_DRAGGING);
802                } else {
803                    completeScroll();
804                    mIsBeingDragged = false;
805                    mIsUnableToDrag = false;
806                }
807
808                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
809                        + " mIsBeingDragged=" + mIsBeingDragged
810                        + "mIsUnableToDrag=" + mIsUnableToDrag);
811                break;
812            }
813
814            case MotionEventCompat.ACTION_POINTER_UP:
815                onSecondaryPointerUp(ev);
816                break;
817        }
818
819        /*
820        * The only time we want to intercept motion events is if we are in the
821        * drag mode.
822        */
823        return mIsBeingDragged;
824    }
825
826    @Override
827    public boolean onTouchEvent(MotionEvent ev) {
828
829        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
830            // Don't handle edge touches immediately -- they may actually belong to one of our
831            // descendants.
832            return false;
833        }
834
835        if (mAdapter == null || mAdapter.getCount() == 0) {
836            // Nothing to present or scroll; nothing to touch.
837            return false;
838        }
839
840        if (mVelocityTracker == null) {
841            mVelocityTracker = VelocityTracker.obtain();
842        }
843        mVelocityTracker.addMovement(ev);
844
845        final int action = ev.getAction();
846
847        switch (action & MotionEventCompat.ACTION_MASK) {
848            case MotionEvent.ACTION_DOWN: {
849                /*
850                 * If being flinged and user touches, stop the fling. isFinished
851                 * will be false if being flinged.
852                 */
853                completeScroll();
854
855                // Remember where the motion event started
856                mLastMotionX = mInitialMotionX = ev.getX();
857                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
858                break;
859            }
860            case MotionEvent.ACTION_MOVE:
861                if (!mIsBeingDragged) {
862                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
863                    final float x = MotionEventCompat.getX(ev, pointerIndex);
864                    final float xDiff = Math.abs(x - mLastMotionX);
865                    final float y = MotionEventCompat.getY(ev, pointerIndex);
866                    final float yDiff = Math.abs(y - mLastMotionY);
867                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
868                    if (xDiff > mTouchSlop && xDiff > yDiff) {
869                        if (DEBUG) Log.v(TAG, "Starting drag!");
870                        mIsBeingDragged = true;
871                        mLastMotionX = x;
872                        setScrollState(SCROLL_STATE_DRAGGING);
873                        setScrollingCacheEnabled(true);
874                    }
875                }
876                if (mIsBeingDragged) {
877                    // Scroll to follow the motion event
878                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
879                            ev, mActivePointerId);
880                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
881                    final float deltaX = mLastMotionX - x;
882                    mLastMotionX = x;
883                    float scrollX = getScrollX() + deltaX;
884                    final int width = getWidth();
885
886                    final float leftBound = Math.max(0, (mCurItem - 1) * width);
887                    final float rightBound =
888                            Math.min(mCurItem + 1, mAdapter.getCount() - 1) * width;
889                    if (scrollX < leftBound) {
890                        scrollX = leftBound;
891                    } else if (scrollX > rightBound) {
892                        scrollX = rightBound;
893                    }
894                    // Don't lose the rounded component
895                    mLastMotionX += scrollX - (int) scrollX;
896                    scrollTo((int) scrollX, getScrollY());
897                    if (mOnPageChangeListener != null) {
898                        final int position = (int) scrollX / width;
899                        final int positionOffsetPixels = (int) scrollX % width;
900                        final float positionOffset = (float) positionOffsetPixels / width;
901                        mOnPageChangeListener.onPageScrolled(position, positionOffset,
902                                positionOffsetPixels);
903                    }
904                }
905                break;
906            case MotionEvent.ACTION_UP:
907                if (mIsBeingDragged) {
908                    final VelocityTracker velocityTracker = mVelocityTracker;
909                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
910                    int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
911                            velocityTracker, mActivePointerId);
912                    mPopulatePending = true;
913                    if ((Math.abs(initialVelocity) > mMinimumVelocity)
914                            || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
915                        if (mLastMotionX > mInitialMotionX) {
916                            setCurrentItemInternal(mCurItem-1, true, true);
917                        } else {
918                            setCurrentItemInternal(mCurItem+1, true, true);
919                        }
920                    } else {
921                        setCurrentItemInternal(mCurItem, true, true);
922                    }
923
924                    mActivePointerId = INVALID_POINTER;
925                    endDrag();
926                }
927                break;
928            case MotionEvent.ACTION_CANCEL:
929                if (mIsBeingDragged) {
930                    setCurrentItemInternal(mCurItem, true, true);
931                    mActivePointerId = INVALID_POINTER;
932                    endDrag();
933                }
934                break;
935            case MotionEventCompat.ACTION_POINTER_DOWN: {
936                final int index = MotionEventCompat.getActionIndex(ev);
937                final float x = MotionEventCompat.getX(ev, index);
938                mLastMotionX = x;
939                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
940                break;
941            }
942            case MotionEventCompat.ACTION_POINTER_UP:
943                onSecondaryPointerUp(ev);
944                mLastMotionX = MotionEventCompat.getX(ev,
945                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
946                break;
947        }
948        return true;
949    }
950
951    private void onSecondaryPointerUp(MotionEvent ev) {
952        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
953        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
954        if (pointerId == mActivePointerId) {
955            // This was our active pointer going up. Choose a new
956            // active pointer and adjust accordingly.
957            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
958            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
959            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
960            if (mVelocityTracker != null) {
961                mVelocityTracker.clear();
962            }
963        }
964    }
965
966    private void endDrag() {
967        mIsBeingDragged = false;
968        mIsUnableToDrag = false;
969
970        if (mVelocityTracker != null) {
971            mVelocityTracker.recycle();
972            mVelocityTracker = null;
973        }
974    }
975
976    private void setScrollingCacheEnabled(boolean enabled) {
977        if (mScrollingCacheEnabled != enabled) {
978            mScrollingCacheEnabled = enabled;
979            if (USE_CACHE) {
980                final int size = getChildCount();
981                for (int i = 0; i < size; ++i) {
982                    final View child = getChildAt(i);
983                    if (child.getVisibility() != GONE) {
984                        child.setDrawingCacheEnabled(enabled);
985                    }
986                }
987            }
988        }
989    }
990
991    /**
992     * Test scrollability within child views of v given a delta of dx.
993     *
994     * @param v View to test for horizontal scrollability
995     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
996     *               or just its children (false).
997     * @param dx Delta scrolled in pixels
998     * @param x X coorindate of the active touch point
999     * @param y Y coordinate of the active touch point
1000     * @return Delta still left to be scrolled by a parent.
1001     */
1002    static boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
1003        if (v instanceof ViewGroup) {
1004            final ViewGroup group = (ViewGroup) v;
1005            final int scrollX = v.getScrollX();
1006            final int scrollY = v.getScrollY();
1007            final int count = group.getChildCount();
1008            // Count backwards - let topmost views consume scroll distance first.
1009            for (int i = count - 1; i >= 0; i--) {
1010                // TODO: Add versioned support here for transformed views.
1011                // This will not work for transformed views in Honeycomb+
1012                final View child = group.getChildAt(i);
1013                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
1014                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
1015                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
1016                                y + scrollY - child.getTop())) {
1017                    return true;
1018                }
1019            }
1020        }
1021
1022        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
1023    }
1024
1025    private class DataSetObserver implements PagerAdapter.DataSetObserver {
1026        @Override
1027        public void onDataSetChanged() {
1028            dataSetChanged();
1029        }
1030    }
1031}
1032