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