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