ViewPager.java revision 390dc3748e6dea81760eb1773d55fcbd42976388
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.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Rect;
22import android.graphics.drawable.Drawable;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.os.SystemClock;
26import android.support.v4.os.ParcelableCompat;
27import android.support.v4.os.ParcelableCompatCreatorCallbacks;
28import android.support.v4.widget.EdgeEffectCompat;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.FocusFinder;
32import android.view.KeyEvent;
33import android.view.MotionEvent;
34import android.view.SoundEffectConstants;
35import android.view.VelocityTracker;
36import android.view.View;
37import android.view.ViewConfiguration;
38import android.view.ViewGroup;
39import android.view.ViewParent;
40import android.view.accessibility.AccessibilityEvent;
41import android.widget.Scroller;
42
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.Comparator;
46
47/**
48 * Layout manager that allows the user to flip left and right
49 * through pages of data.  You supply an implementation of a
50 * {@link PagerAdapter} to generate the pages that the view shows.
51 *
52 * <p>Note this class is currently under early design and
53 * development.  The API will likely change in later updates of
54 * the compatibility library, requiring changes to the source code
55 * of apps when they are compiled against the newer version.</p>
56 */
57public class ViewPager extends ViewGroup {
58    private static final String TAG = "ViewPager";
59    private static final boolean DEBUG = false;
60
61    private static final boolean USE_CACHE = false;
62
63    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
64
65    static class ItemInfo {
66        Object object;
67        int position;
68        boolean scrolling;
69    }
70
71    private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
72        @Override
73        public int compare(ItemInfo lhs, ItemInfo rhs) {
74            return lhs.position - rhs.position;
75        }};
76
77    private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
78
79    private PagerAdapter mAdapter;
80    private int mCurItem;   // Index of currently displayed page.
81    private int mRestoredCurItem = -1;
82    private Parcelable mRestoredAdapterState = null;
83    private ClassLoader mRestoredClassLoader = null;
84    private Scroller mScroller;
85    private PagerAdapter.DataSetObserver mObserver;
86
87    private int mPageMargin;
88    private Drawable mMarginDrawable;
89
90    private int mChildWidthMeasureSpec;
91    private int mChildHeightMeasureSpec;
92    private boolean mInLayout;
93
94    private boolean mScrollingCacheEnabled;
95
96    private boolean mPopulatePending;
97    private boolean mScrolling;
98    private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
99
100    private boolean mIsBeingDragged;
101    private boolean mIsUnableToDrag;
102    private int mTouchSlop;
103    private float mInitialMotionX;
104    /**
105     * Position of the last motion event.
106     */
107    private float mLastMotionX;
108    private float mLastMotionY;
109    /**
110     * ID of the active pointer. This is used to retain consistency during
111     * drags/flings if multiple pointers are used.
112     */
113    private int mActivePointerId = INVALID_POINTER;
114    /**
115     * Sentinel value for no current active pointer.
116     * Used by {@link #mActivePointerId}.
117     */
118    private static final int INVALID_POINTER = -1;
119
120    /**
121     * Determines speed during touch scrolling
122     */
123    private VelocityTracker mVelocityTracker;
124    private int mMinimumVelocity;
125    private int mMaximumVelocity;
126
127    private boolean mFakeDragging;
128    private long mFakeDragBeginTime;
129
130    private EdgeEffectCompat mLeftEdge;
131    private EdgeEffectCompat mRightEdge;
132
133    private boolean mFirstLayout = true;
134
135    private OnPageChangeListener mOnPageChangeListener;
136
137    /**
138     * Indicates that the pager is in an idle, settled state. The current page
139     * is fully in view and no animation is in progress.
140     */
141    public static final int SCROLL_STATE_IDLE = 0;
142
143    /**
144     * Indicates that the pager is currently being dragged by the user.
145     */
146    public static final int SCROLL_STATE_DRAGGING = 1;
147
148    /**
149     * Indicates that the pager is in the process of settling to a final position.
150     */
151    public static final int SCROLL_STATE_SETTLING = 2;
152
153    private int mScrollState = SCROLL_STATE_IDLE;
154
155    /**
156     * Callback interface for responding to changing state of the selected page.
157     */
158    public interface OnPageChangeListener {
159
160        /**
161         * This method will be invoked when the current page is scrolled, either as part
162         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
163         *
164         * @param position Position index of the first page currently being displayed.
165         *                 Page position+1 will be visible if positionOffset is nonzero.
166         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
167         * @param positionOffsetPixels Value in pixels indicating the offset from position.
168         */
169        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
170
171        /**
172         * This method will be invoked when a new page becomes selected. Animation is not
173         * necessarily complete.
174         *
175         * @param position Position index of the new selected page.
176         */
177        public void onPageSelected(int position);
178
179        /**
180         * Called when the scroll state changes. Useful for discovering when the user
181         * begins dragging, when the pager is automatically settling to the current page,
182         * or when it is fully stopped/idle.
183         *
184         * @param state The new scroll state.
185         * @see ViewPager#SCROLL_STATE_IDLE
186         * @see ViewPager#SCROLL_STATE_DRAGGING
187         * @see ViewPager#SCROLL_STATE_SETTLING
188         */
189        public void onPageScrollStateChanged(int state);
190    }
191
192    /**
193     * Simple implementation of the {@link OnPageChangeListener} interface with stub
194     * implementations of each method. Extend this if you do not intend to override
195     * every method of {@link OnPageChangeListener}.
196     */
197    public static class SimpleOnPageChangeListener implements OnPageChangeListener {
198        @Override
199        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
200            // This space for rent
201        }
202
203        @Override
204        public void onPageSelected(int position) {
205            // This space for rent
206        }
207
208        @Override
209        public void onPageScrollStateChanged(int state) {
210            // This space for rent
211        }
212    }
213
214    public ViewPager(Context context) {
215        super(context);
216        initViewPager();
217    }
218
219    public ViewPager(Context context, AttributeSet attrs) {
220        super(context, attrs);
221        initViewPager();
222    }
223
224    void initViewPager() {
225        setWillNotDraw(false);
226        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
227        setFocusable(true);
228        final Context context = getContext();
229        mScroller = new Scroller(context);
230        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
231        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
232        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
233        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
234        mLeftEdge = new EdgeEffectCompat(context);
235        mRightEdge = new EdgeEffectCompat(context);
236    }
237
238    private void setScrollState(int newState) {
239        if (mScrollState == newState) {
240            return;
241        }
242
243        mScrollState = newState;
244        if (mOnPageChangeListener != null) {
245            mOnPageChangeListener.onPageScrollStateChanged(newState);
246        }
247    }
248
249    public void setAdapter(PagerAdapter adapter) {
250        if (mAdapter != null) {
251            mAdapter.setDataSetObserver(null);
252            mAdapter.startUpdate(this);
253            for (int i = 0; i < mItems.size(); i++) {
254                final ItemInfo ii = mItems.get(i);
255                mAdapter.destroyItem(this, ii.position, ii.object);
256            }
257            mAdapter.finishUpdate(this);
258            mItems.clear();
259            removeAllViews();
260            mCurItem = 0;
261            scrollTo(0, 0);
262        }
263
264        mAdapter = adapter;
265
266        if (mAdapter != null) {
267            if (mObserver == null) {
268                mObserver = new DataSetObserver();
269            }
270            mAdapter.setDataSetObserver(mObserver);
271            mPopulatePending = false;
272            if (mRestoredCurItem >= 0) {
273                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
274                setCurrentItemInternal(mRestoredCurItem, false, true);
275                mRestoredCurItem = -1;
276                mRestoredAdapterState = null;
277                mRestoredClassLoader = null;
278            } else {
279                populate();
280            }
281        }
282    }
283
284    public PagerAdapter getAdapter() {
285        return mAdapter;
286    }
287
288    /**
289     * Set the currently selected page. If the ViewPager has already been through its first
290     * layout there will be a smooth animated transition between the current item and the
291     * specified item.
292     *
293     * @param item Item index to select
294     */
295    public void setCurrentItem(int item) {
296        mPopulatePending = false;
297        setCurrentItemInternal(item, !mFirstLayout, false);
298    }
299
300    /**
301     * Set the currently selected page.
302     *
303     * @param item Item index to select
304     * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
305     */
306    public void setCurrentItem(int item, boolean smoothScroll) {
307        mPopulatePending = false;
308        setCurrentItemInternal(item, smoothScroll, false);
309    }
310
311    public int getCurrentItem() {
312        return mCurItem;
313    }
314
315    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
316        if (mAdapter == null || mAdapter.getCount() <= 0) {
317            setScrollingCacheEnabled(false);
318            return;
319        }
320        if (!always && mCurItem == item && mItems.size() != 0) {
321            setScrollingCacheEnabled(false);
322            return;
323        }
324        if (item < 0) {
325            item = 0;
326        } else if (item >= mAdapter.getCount()) {
327            item = mAdapter.getCount() - 1;
328        }
329        final int pageLimit = mOffscreenPageLimit;
330        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
331            // We are doing a jump by more than one page.  To avoid
332            // glitches, we want to keep all current pages in the view
333            // until the scroll ends.
334            for (int i=0; i<mItems.size(); i++) {
335                mItems.get(i).scrolling = true;
336            }
337        }
338        final boolean dispatchSelected = mCurItem != item;
339        mCurItem = item;
340        populate();
341        final int destX = (getWidth() + mPageMargin) * item;
342        if (smoothScroll) {
343            smoothScrollTo(destX, 0);
344            if (dispatchSelected && mOnPageChangeListener != null) {
345                mOnPageChangeListener.onPageSelected(item);
346            }
347        } else {
348            if (dispatchSelected && mOnPageChangeListener != null) {
349                mOnPageChangeListener.onPageSelected(item);
350            }
351            completeScroll();
352            scrollTo(destX, 0);
353        }
354    }
355
356    public void setOnPageChangeListener(OnPageChangeListener listener) {
357        mOnPageChangeListener = listener;
358    }
359
360    /**
361     * Returns the number of pages that will be retained to either side of the
362     * current page in the view hierarchy in an idle state. Defaults to 1.
363     *
364     * @return How many pages will be kept offscreen on either side
365     * @see #setOffscreenPageLimit(int)
366     */
367    public int getOffscreenPageLimit() {
368        return mOffscreenPageLimit;
369    }
370
371    /**
372     * Set the number of pages that should be retained to either side of the
373     * current page in the view hierarchy in an idle state. Pages beyond this
374     * limit will be recreated from the adapter when needed.
375     *
376     * <p>This is offered as an optimization. If you know in advance the number
377     * of pages you will need to support or have lazy-loading mechanisms in place
378     * on your pages, tweaking this setting can have benefits in perceived smoothness
379     * of paging animations and interaction. If you have a small number of pages (3-4)
380     * that you can keep active all at once, less time will be spent in layout for
381     * newly created view subtrees as the user pages back and forth.</p>
382     *
383     * <p>You should keep this limit low, especially if your pages have complex layouts.
384     * This setting defaults to 1.</p>
385     *
386     * @param limit How many pages will be kept offscreen in an idle state.
387     */
388    public void setOffscreenPageLimit(int limit) {
389        if (limit < DEFAULT_OFFSCREEN_PAGES) {
390            Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
391                    DEFAULT_OFFSCREEN_PAGES);
392            limit = DEFAULT_OFFSCREEN_PAGES;
393        }
394        if (limit != mOffscreenPageLimit) {
395            mOffscreenPageLimit = limit;
396            populate();
397        }
398    }
399
400    /**
401     * Set the margin between pages.
402     *
403     * @param marginPixels Distance between adjacent pages in pixels
404     * @see #getPageMargin()
405     * @see #setPageMarginDrawable(Drawable)
406     * @see #setPageMarginDrawable(int)
407     */
408    public void setPageMargin(int marginPixels) {
409        final int oldMargin = mPageMargin;
410        mPageMargin = marginPixels;
411
412        final int width = getWidth();
413        recomputeScrollPosition(width, width, marginPixels, oldMargin);
414
415        requestLayout();
416    }
417
418    /**
419     * Return the margin between pages.
420     *
421     * @return The size of the margin in pixels
422     */
423    public int getPageMargin() {
424        return mPageMargin;
425    }
426
427    /**
428     * Set a drawable that will be used to fill the margin between pages.
429     *
430     * @param d Drawable to display between pages
431     */
432    public void setPageMarginDrawable(Drawable d) {
433        mMarginDrawable = d;
434        if (d != null) refreshDrawableState();
435        setWillNotDraw(d == null);
436        invalidate();
437    }
438
439    /**
440     * Set a drawable that will be used to fill the margin between pages.
441     *
442     * @param resId Resource ID of a drawable to display between pages
443     */
444    public void setPageMarginDrawable(int resId) {
445        setPageMarginDrawable(getContext().getResources().getDrawable(resId));
446    }
447
448    @Override
449    protected boolean verifyDrawable(Drawable who) {
450        return super.verifyDrawable(who) || who == mMarginDrawable;
451    }
452
453    @Override
454    protected void drawableStateChanged() {
455        super.drawableStateChanged();
456        final Drawable d = mMarginDrawable;
457        if (d != null && d.isStateful()) {
458            d.setState(getDrawableState());
459        }
460    }
461
462    /**
463     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
464     *
465     * @param x the number of pixels to scroll by on the X axis
466     * @param y the number of pixels to scroll by on the Y axis
467     */
468    void smoothScrollTo(int x, int y) {
469        if (getChildCount() == 0) {
470            // Nothing to do.
471            setScrollingCacheEnabled(false);
472            return;
473        }
474        int sx = getScrollX();
475        int sy = getScrollY();
476        int dx = x - sx;
477        int dy = y - sy;
478        if (dx == 0 && dy == 0) {
479            completeScroll();
480            setScrollState(SCROLL_STATE_IDLE);
481            return;
482        }
483
484        setScrollingCacheEnabled(true);
485        mScrolling = true;
486        setScrollState(SCROLL_STATE_SETTLING);
487        mScroller.startScroll(sx, sy, dx, dy);
488        invalidate();
489    }
490
491    void addNewItem(int position, int index) {
492        ItemInfo ii = new ItemInfo();
493        ii.position = position;
494        ii.object = mAdapter.instantiateItem(this, position);
495        if (index < 0) {
496            mItems.add(ii);
497        } else {
498            mItems.add(index, ii);
499        }
500    }
501
502    void dataSetChanged() {
503        // This method only gets called if our observer is attached, so mAdapter is non-null.
504
505        boolean needPopulate = mItems.size() < 3 && mItems.size() < mAdapter.getCount();
506        int newCurrItem = -1;
507
508        for (int i = 0; i < mItems.size(); i++) {
509            final ItemInfo ii = mItems.get(i);
510            final int newPos = mAdapter.getItemPosition(ii.object);
511
512            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
513                continue;
514            }
515
516            if (newPos == PagerAdapter.POSITION_NONE) {
517                mItems.remove(i);
518                i--;
519                mAdapter.destroyItem(this, ii.position, ii.object);
520                needPopulate = true;
521
522                if (mCurItem == ii.position) {
523                    // Keep the current item in the valid range
524                    newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1));
525                }
526                continue;
527            }
528
529            if (ii.position != newPos) {
530                if (ii.position == mCurItem) {
531                    // Our current item changed position. Follow it.
532                    newCurrItem = newPos;
533                }
534
535                ii.position = newPos;
536                needPopulate = true;
537            }
538        }
539
540        Collections.sort(mItems, COMPARATOR);
541
542        if (newCurrItem >= 0) {
543            // TODO This currently causes a jump.
544            setCurrentItemInternal(newCurrItem, false, true);
545            needPopulate = true;
546        }
547        if (needPopulate) {
548            populate();
549            requestLayout();
550        }
551    }
552
553    void populate() {
554        if (mAdapter == null) {
555            return;
556        }
557
558        // Bail now if we are waiting to populate.  This is to hold off
559        // on creating views from the time the user releases their finger to
560        // fling to a new position until we have finished the scroll to
561        // that position, avoiding glitches from happening at that point.
562        if (mPopulatePending) {
563            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
564            return;
565        }
566
567        // Also, don't populate until we are attached to a window.  This is to
568        // avoid trying to populate before we have restored our view hierarchy
569        // state and conflicting with what is restored.
570        if (getWindowToken() == null) {
571            return;
572        }
573
574        mAdapter.startUpdate(this);
575
576        final int pageLimit = mOffscreenPageLimit;
577        final int startPos = Math.max(0, mCurItem - pageLimit);
578        final int N = mAdapter.getCount();
579        final int endPos = Math.min(N-1, mCurItem + pageLimit);
580
581        if (DEBUG) Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos);
582
583        // Add and remove pages in the existing list.
584        int lastPos = -1;
585        for (int i=0; i<mItems.size(); i++) {
586            ItemInfo ii = mItems.get(i);
587            if ((ii.position < startPos || ii.position > endPos) && !ii.scrolling) {
588                if (DEBUG) Log.i(TAG, "removing: " + ii.position + " @ " + i);
589                mItems.remove(i);
590                i--;
591                mAdapter.destroyItem(this, ii.position, ii.object);
592            } else if (lastPos < endPos && ii.position > startPos) {
593                // The next item is outside of our range, but we have a gap
594                // between it and the last item where we want to have a page
595                // shown.  Fill in the gap.
596                lastPos++;
597                if (lastPos < startPos) {
598                    lastPos = startPos;
599                }
600                while (lastPos <= endPos && lastPos < ii.position) {
601                    if (DEBUG) Log.i(TAG, "inserting: " + lastPos + " @ " + i);
602                    addNewItem(lastPos, i);
603                    lastPos++;
604                    i++;
605                }
606            }
607            lastPos = ii.position;
608        }
609
610        // Add any new pages we need at the end.
611        lastPos = mItems.size() > 0 ? mItems.get(mItems.size()-1).position : -1;
612        if (lastPos < endPos) {
613            lastPos++;
614            lastPos = lastPos > startPos ? lastPos : startPos;
615            while (lastPos <= endPos) {
616                if (DEBUG) Log.i(TAG, "appending: " + lastPos);
617                addNewItem(lastPos, -1);
618                lastPos++;
619            }
620        }
621
622        if (DEBUG) {
623            Log.i(TAG, "Current page list:");
624            for (int i=0; i<mItems.size(); i++) {
625                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
626            }
627        }
628
629        ItemInfo curItem = null;
630        for (int i=0; i<mItems.size(); i++) {
631            if (mItems.get(i).position == mCurItem) {
632                curItem = mItems.get(i);
633                break;
634            }
635        }
636        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
637
638        mAdapter.finishUpdate(this);
639
640        if (hasFocus()) {
641            View currentFocused = findFocus();
642            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
643            if (ii == null || ii.position != mCurItem) {
644                for (int i=0; i<getChildCount(); i++) {
645                    View child = getChildAt(i);
646                    ii = infoForChild(child);
647                    if (ii != null && ii.position == mCurItem) {
648                        if (child.requestFocus(FOCUS_FORWARD)) {
649                            break;
650                        }
651                    }
652                }
653            }
654        }
655    }
656
657    public static class SavedState extends BaseSavedState {
658        int position;
659        Parcelable adapterState;
660        ClassLoader loader;
661
662        public SavedState(Parcelable superState) {
663            super(superState);
664        }
665
666        @Override
667        public void writeToParcel(Parcel out, int flags) {
668            super.writeToParcel(out, flags);
669            out.writeInt(position);
670            out.writeParcelable(adapterState, flags);
671        }
672
673        @Override
674        public String toString() {
675            return "FragmentPager.SavedState{"
676                    + Integer.toHexString(System.identityHashCode(this))
677                    + " position=" + position + "}";
678        }
679
680        public static final Parcelable.Creator<SavedState> CREATOR
681                = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
682                    @Override
683                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
684                        return new SavedState(in, loader);
685                    }
686                    @Override
687                    public SavedState[] newArray(int size) {
688                        return new SavedState[size];
689                    }
690                });
691
692        SavedState(Parcel in, ClassLoader loader) {
693            super(in);
694            if (loader == null) {
695                loader = getClass().getClassLoader();
696            }
697            position = in.readInt();
698            adapterState = in.readParcelable(loader);
699            this.loader = loader;
700        }
701    }
702
703    @Override
704    public Parcelable onSaveInstanceState() {
705        Parcelable superState = super.onSaveInstanceState();
706        SavedState ss = new SavedState(superState);
707        ss.position = mCurItem;
708        if (mAdapter != null) {
709            ss.adapterState = mAdapter.saveState();
710        }
711        return ss;
712    }
713
714    @Override
715    public void onRestoreInstanceState(Parcelable state) {
716        if (!(state instanceof SavedState)) {
717            super.onRestoreInstanceState(state);
718            return;
719        }
720
721        SavedState ss = (SavedState)state;
722        super.onRestoreInstanceState(ss.getSuperState());
723
724        if (mAdapter != null) {
725            mAdapter.restoreState(ss.adapterState, ss.loader);
726            setCurrentItemInternal(ss.position, false, true);
727        } else {
728            mRestoredCurItem = ss.position;
729            mRestoredAdapterState = ss.adapterState;
730            mRestoredClassLoader = ss.loader;
731        }
732    }
733
734    @Override
735    public void addView(View child, int index, LayoutParams params) {
736        if (mInLayout) {
737            addViewInLayout(child, index, params);
738            child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
739        } else {
740            super.addView(child, index, params);
741        }
742
743        if (USE_CACHE) {
744            if (child.getVisibility() != GONE) {
745                child.setDrawingCacheEnabled(mScrollingCacheEnabled);
746            } else {
747                child.setDrawingCacheEnabled(false);
748            }
749        }
750    }
751
752    ItemInfo infoForChild(View child) {
753        for (int i=0; i<mItems.size(); i++) {
754            ItemInfo ii = mItems.get(i);
755            if (mAdapter.isViewFromObject(child, ii.object)) {
756                return ii;
757            }
758        }
759        return null;
760    }
761
762    ItemInfo infoForAnyChild(View child) {
763        ViewParent parent;
764        while ((parent=child.getParent()) != this) {
765            if (parent == null || !(parent instanceof View)) {
766                return null;
767            }
768            child = (View)parent;
769        }
770        return infoForChild(child);
771    }
772
773    @Override
774    protected void onAttachedToWindow() {
775        super.onAttachedToWindow();
776        mFirstLayout = true;
777    }
778
779    @Override
780    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
781        // For simple implementation, or internal size is always 0.
782        // We depend on the container to specify the layout size of
783        // our view.  We can't really know what it is since we will be
784        // adding and removing different arbitrary views and do not
785        // want the layout to change as this happens.
786        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
787                getDefaultSize(0, heightMeasureSpec));
788
789        // Children are just made to fill our space.
790        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
791                getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
792        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
793                getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
794
795        // Make sure we have created all fragments that we need to have shown.
796        mInLayout = true;
797        populate();
798        mInLayout = false;
799
800        // Make sure all children have been properly measured.
801        final int size = getChildCount();
802        for (int i = 0; i < size; ++i) {
803            final View child = getChildAt(i);
804            if (child.getVisibility() != GONE) {
805                if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
806		        + ": " + mChildWidthMeasureSpec);
807                child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
808            }
809        }
810    }
811
812    @Override
813    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
814        super.onSizeChanged(w, h, oldw, oldh);
815
816        // Make sure scroll position is set correctly.
817        if (w != oldw) {
818            recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
819        }
820    }
821
822    private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
823        final int widthWithMargin = width + margin;
824        if (oldWidth > 0) {
825            final int oldScrollPos = getScrollX();
826            final int oldwwm = oldWidth + oldMargin;
827            final int oldScrollItem = oldScrollPos / oldwwm;
828            final float scrollOffset = (float) (oldScrollPos % oldwwm) / oldwwm;
829            final int scrollPos = (int) ((oldScrollItem + scrollOffset) * widthWithMargin);
830            scrollTo(scrollPos, getScrollY());
831            if (!mScroller.isFinished()) {
832                // We now return to your regularly scheduled scroll, already in progress.
833                final int newDuration = mScroller.getDuration() - mScroller.timePassed();
834                mScroller.startScroll(scrollPos, 0, mCurItem * widthWithMargin, 0, newDuration);
835            }
836        } else {
837            int scrollPos = mCurItem * widthWithMargin;
838            if (scrollPos != getScrollX()) {
839                completeScroll();
840                scrollTo(scrollPos, getScrollY());
841            }
842        }
843    }
844
845    @Override
846    protected void onLayout(boolean changed, int l, int t, int r, int b) {
847        mInLayout = true;
848        populate();
849        mInLayout = false;
850
851        final int count = getChildCount();
852        final int width = r-l;
853
854        for (int i = 0; i < count; i++) {
855            View child = getChildAt(i);
856            ItemInfo ii;
857            if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
858                int loff = (width + mPageMargin) * ii.position;
859                int childLeft = getPaddingLeft() + loff;
860                int childTop = getPaddingTop();
861                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
862		        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
863		        + "x" + child.getMeasuredHeight());
864                child.layout(childLeft, childTop,
865                        childLeft + child.getMeasuredWidth(),
866                        childTop + child.getMeasuredHeight());
867            }
868        }
869        mFirstLayout = false;
870    }
871
872    @Override
873    public void computeScroll() {
874        if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished());
875        if (!mScroller.isFinished()) {
876            if (mScroller.computeScrollOffset()) {
877                if (DEBUG) Log.i(TAG, "computeScroll: still scrolling");
878                int oldX = getScrollX();
879                int oldY = getScrollY();
880                int x = mScroller.getCurrX();
881                int y = mScroller.getCurrY();
882
883                if (oldX != x || oldY != y) {
884                    scrollTo(x, y);
885                }
886
887                if (mOnPageChangeListener != null) {
888                    final int widthWithMargin = getWidth() + mPageMargin;
889                    final int position = x / widthWithMargin;
890                    final int offsetPixels = x % widthWithMargin;
891                    final float offset = (float) offsetPixels / widthWithMargin;
892                    mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
893                }
894
895                // Keep on drawing until the animation has finished.
896                invalidate();
897                return;
898            }
899        }
900
901        // Done with scroll, clean up state.
902        completeScroll();
903    }
904
905    private void completeScroll() {
906        boolean needPopulate = mScrolling;
907        if (needPopulate) {
908            // Done with scroll, no longer want to cache view drawing.
909            setScrollingCacheEnabled(false);
910            mScroller.abortAnimation();
911            int oldX = getScrollX();
912            int oldY = getScrollY();
913            int x = mScroller.getCurrX();
914            int y = mScroller.getCurrY();
915            if (oldX != x || oldY != y) {
916                scrollTo(x, y);
917            }
918            setScrollState(SCROLL_STATE_IDLE);
919        }
920        mPopulatePending = false;
921        mScrolling = false;
922        for (int i=0; i<mItems.size(); i++) {
923            ItemInfo ii = mItems.get(i);
924            if (ii.scrolling) {
925                needPopulate = true;
926                ii.scrolling = false;
927            }
928        }
929        if (needPopulate) {
930            populate();
931        }
932    }
933
934    @Override
935    public boolean onInterceptTouchEvent(MotionEvent ev) {
936        /*
937         * This method JUST determines whether we want to intercept the motion.
938         * If we return true, onMotionEvent will be called and we do the actual
939         * scrolling there.
940         */
941
942        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
943
944        // Always take care of the touch gesture being complete.
945        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
946            // Release the drag.
947            if (DEBUG) Log.v(TAG, "Intercept done!");
948            mIsBeingDragged = false;
949            mIsUnableToDrag = false;
950            mActivePointerId = INVALID_POINTER;
951            return false;
952        }
953
954        // Nothing more to do here if we have decided whether or not we
955        // are dragging.
956        if (action != MotionEvent.ACTION_DOWN) {
957            if (mIsBeingDragged) {
958                if (DEBUG) Log.v(TAG, "Intercept returning true!");
959                return true;
960            }
961            if (mIsUnableToDrag) {
962                if (DEBUG) Log.v(TAG, "Intercept returning false!");
963                return false;
964            }
965        }
966
967        switch (action) {
968            case MotionEvent.ACTION_MOVE: {
969                /*
970                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
971                 * whether the user has moved far enough from his original down touch.
972                 */
973
974                /*
975                * Locally do absolute value. mLastMotionY is set to the y value
976                * of the down event.
977                */
978                final int activePointerId = mActivePointerId;
979                if (activePointerId == INVALID_POINTER) {
980                    // If we don't have a valid id, the touch down wasn't on content.
981                    break;
982                }
983
984                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
985                final float x = MotionEventCompat.getX(ev, pointerIndex);
986                final float dx = x - mLastMotionX;
987                final float xDiff = Math.abs(dx);
988                final float y = MotionEventCompat.getY(ev, pointerIndex);
989                final float yDiff = Math.abs(y - mLastMotionY);
990                final int scrollX = getScrollX();
991                final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null &&
992                        scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1);
993                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
994
995                if (canScroll(this, false, (int) dx, (int) x, (int) y)) {
996                    // Nested view has scrollable area under this point. Let it be handled there.
997                    mInitialMotionX = mLastMotionX = x;
998                    mLastMotionY = y;
999                    return false;
1000                }
1001                if (xDiff > mTouchSlop && xDiff > yDiff) {
1002                    if (DEBUG) Log.v(TAG, "Starting drag!");
1003                    mIsBeingDragged = true;
1004                    setScrollState(SCROLL_STATE_DRAGGING);
1005                    mLastMotionX = x;
1006                    setScrollingCacheEnabled(true);
1007                } else {
1008                    if (yDiff > mTouchSlop) {
1009                        // The finger has moved enough in the vertical
1010                        // direction to be counted as a drag...  abort
1011                        // any attempt to drag horizontally, to work correctly
1012                        // with children that have scrolling containers.
1013                        if (DEBUG) Log.v(TAG, "Starting unable to drag!");
1014                        mIsUnableToDrag = true;
1015                    }
1016                }
1017                break;
1018            }
1019
1020            case MotionEvent.ACTION_DOWN: {
1021                /*
1022                 * Remember location of down touch.
1023                 * ACTION_DOWN always refers to pointer index 0.
1024                 */
1025                mLastMotionX = mInitialMotionX = ev.getX();
1026                mLastMotionY = ev.getY();
1027                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1028
1029                if (mScrollState == SCROLL_STATE_SETTLING) {
1030                    // Let the user 'catch' the pager as it animates.
1031                    mIsBeingDragged = true;
1032                    mIsUnableToDrag = false;
1033                    setScrollState(SCROLL_STATE_DRAGGING);
1034                } else {
1035                    completeScroll();
1036                    mIsBeingDragged = false;
1037                    mIsUnableToDrag = false;
1038                }
1039
1040                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
1041                        + " mIsBeingDragged=" + mIsBeingDragged
1042                        + "mIsUnableToDrag=" + mIsUnableToDrag);
1043                break;
1044            }
1045
1046            case MotionEventCompat.ACTION_POINTER_UP:
1047                onSecondaryPointerUp(ev);
1048                break;
1049        }
1050
1051        /*
1052        * The only time we want to intercept motion events is if we are in the
1053        * drag mode.
1054        */
1055        return mIsBeingDragged;
1056    }
1057
1058    @Override
1059    public boolean onTouchEvent(MotionEvent ev) {
1060        if (mFakeDragging) {
1061            // A fake drag is in progress already, ignore this real one
1062            // but still eat the touch events.
1063            // (It is likely that the user is multi-touching the screen.)
1064            return true;
1065        }
1066
1067        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
1068            // Don't handle edge touches immediately -- they may actually belong to one of our
1069            // descendants.
1070            return false;
1071        }
1072
1073        if (mAdapter == null || mAdapter.getCount() == 0) {
1074            // Nothing to present or scroll; nothing to touch.
1075            return false;
1076        }
1077
1078        if (mVelocityTracker == null) {
1079            mVelocityTracker = VelocityTracker.obtain();
1080        }
1081        mVelocityTracker.addMovement(ev);
1082
1083        final int action = ev.getAction();
1084        boolean needsInvalidate = false;
1085
1086        switch (action & MotionEventCompat.ACTION_MASK) {
1087            case MotionEvent.ACTION_DOWN: {
1088                /*
1089                 * If being flinged and user touches, stop the fling. isFinished
1090                 * will be false if being flinged.
1091                 */
1092                completeScroll();
1093
1094                // Remember where the motion event started
1095                mLastMotionX = mInitialMotionX = ev.getX();
1096                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1097                break;
1098            }
1099            case MotionEvent.ACTION_MOVE:
1100                if (!mIsBeingDragged) {
1101                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1102                    final float x = MotionEventCompat.getX(ev, pointerIndex);
1103                    final float xDiff = Math.abs(x - mLastMotionX);
1104                    final float y = MotionEventCompat.getY(ev, pointerIndex);
1105                    final float yDiff = Math.abs(y - mLastMotionY);
1106                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
1107                    if (xDiff > mTouchSlop && xDiff > yDiff) {
1108                        if (DEBUG) Log.v(TAG, "Starting drag!");
1109                        mIsBeingDragged = true;
1110                        mLastMotionX = x;
1111                        setScrollState(SCROLL_STATE_DRAGGING);
1112                        setScrollingCacheEnabled(true);
1113                    }
1114                }
1115                if (mIsBeingDragged) {
1116                    // Scroll to follow the motion event
1117                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
1118                            ev, mActivePointerId);
1119                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
1120                    final float deltaX = mLastMotionX - x;
1121                    mLastMotionX = x;
1122                    float oldScrollX = getScrollX();
1123                    float scrollX = oldScrollX + deltaX;
1124                    final int width = getWidth();
1125                    final int widthWithMargin = width + mPageMargin;
1126
1127                    final int lastItemIndex = mAdapter.getCount() - 1;
1128                    final float leftBound = Math.max(0, (mCurItem - 1) * widthWithMargin);
1129                    final float rightBound =
1130                            Math.min(mCurItem + 1, lastItemIndex) * widthWithMargin;
1131                    if (scrollX < leftBound) {
1132                        if (leftBound == 0) {
1133                            float over = -scrollX;
1134                            needsInvalidate = mLeftEdge.onPull(over / width);
1135                        }
1136                        scrollX = leftBound;
1137                    } else if (scrollX > rightBound) {
1138                        if (rightBound == lastItemIndex * widthWithMargin) {
1139                            float over = scrollX - rightBound;
1140                            needsInvalidate = mRightEdge.onPull(over / width);
1141                        }
1142                        scrollX = rightBound;
1143                    }
1144                    // Don't lose the rounded component
1145                    mLastMotionX += scrollX - (int) scrollX;
1146                    scrollTo((int) scrollX, getScrollY());
1147                    if (mOnPageChangeListener != null) {
1148                        final int position = (int) scrollX / widthWithMargin;
1149                        final int positionOffsetPixels = (int) scrollX % widthWithMargin;
1150                        final float positionOffset = (float) positionOffsetPixels / widthWithMargin;
1151                        mOnPageChangeListener.onPageScrolled(position, positionOffset,
1152                                positionOffsetPixels);
1153                    }
1154                }
1155                break;
1156            case MotionEvent.ACTION_UP:
1157                if (mIsBeingDragged) {
1158                    final VelocityTracker velocityTracker = mVelocityTracker;
1159                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1160                    int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
1161                            velocityTracker, mActivePointerId);
1162                    mPopulatePending = true;
1163                    if ((Math.abs(initialVelocity) > mMinimumVelocity)
1164                            || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
1165                        if (mLastMotionX > mInitialMotionX) {
1166                            setCurrentItemInternal(mCurItem-1, true, true);
1167                        } else {
1168                            setCurrentItemInternal(mCurItem+1, true, true);
1169                        }
1170                    } else {
1171                        setCurrentItemInternal(mCurItem, true, true);
1172                    }
1173
1174                    mActivePointerId = INVALID_POINTER;
1175                    endDrag();
1176                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
1177                }
1178                break;
1179            case MotionEvent.ACTION_CANCEL:
1180                if (mIsBeingDragged) {
1181                    setCurrentItemInternal(mCurItem, true, true);
1182                    mActivePointerId = INVALID_POINTER;
1183                    endDrag();
1184                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
1185                }
1186                break;
1187            case MotionEventCompat.ACTION_POINTER_DOWN: {
1188                final int index = MotionEventCompat.getActionIndex(ev);
1189                final float x = MotionEventCompat.getX(ev, index);
1190                mLastMotionX = x;
1191                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
1192                break;
1193            }
1194            case MotionEventCompat.ACTION_POINTER_UP:
1195                onSecondaryPointerUp(ev);
1196                mLastMotionX = MotionEventCompat.getX(ev,
1197                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));
1198                break;
1199        }
1200        if (needsInvalidate) {
1201            invalidate();
1202        }
1203        return true;
1204    }
1205
1206    @Override
1207    public void draw(Canvas canvas) {
1208        super.draw(canvas);
1209        boolean needsInvalidate = false;
1210
1211        final int overScrollMode = ViewCompat.getOverScrollMode(this);
1212        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
1213                (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
1214                        mAdapter != null && mAdapter.getCount() > 1)) {
1215            if (!mLeftEdge.isFinished()) {
1216                final int restoreCount = canvas.save();
1217                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
1218
1219                canvas.rotate(270);
1220                canvas.translate(-height + getPaddingTop(), 0);
1221                mLeftEdge.setSize(height, getWidth());
1222                needsInvalidate |= mLeftEdge.draw(canvas);
1223                canvas.restoreToCount(restoreCount);
1224            }
1225            if (!mRightEdge.isFinished()) {
1226                final int restoreCount = canvas.save();
1227                final int width = getWidth();
1228                final int height = getHeight() - getPaddingTop() - getPaddingBottom();
1229                final int itemCount = mAdapter != null ? mAdapter.getCount() : 1;
1230
1231                canvas.rotate(90);
1232                canvas.translate(-getPaddingTop(),
1233                        -itemCount * (width + mPageMargin) + mPageMargin);
1234                mRightEdge.setSize(height, width);
1235                needsInvalidate |= mRightEdge.draw(canvas);
1236                canvas.restoreToCount(restoreCount);
1237            }
1238        } else {
1239            mLeftEdge.finish();
1240            mRightEdge.finish();
1241        }
1242
1243        if (needsInvalidate) {
1244            // Keep animating
1245            invalidate();
1246        }
1247    }
1248
1249    @Override
1250    protected void onDraw(Canvas canvas) {
1251        super.onDraw(canvas);
1252
1253        // Draw the margin drawable if needed.
1254        if (mPageMargin > 0 && mMarginDrawable != null) {
1255            final int scrollX = getScrollX();
1256            final int width = getWidth();
1257            final int offset = scrollX % (width + mPageMargin);
1258            if (offset != 0) {
1259                // Pages fit completely when settled; we only need to draw when in between
1260                final int left = scrollX - offset + width;
1261                mMarginDrawable.setBounds(left, 0, left + mPageMargin, getHeight());
1262                mMarginDrawable.draw(canvas);
1263            }
1264        }
1265    }
1266
1267    /**
1268     * Start a fake drag of the pager.
1269     *
1270     * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
1271     * with the touch scrolling of another view, while still letting the ViewPager
1272     * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
1273     * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
1274     * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
1275     *
1276     * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
1277     * is already in progress, this method will return false.
1278     *
1279     * @return true if the fake drag began successfully, false if it could not be started.
1280     *
1281     * @see #fakeDragBy(float)
1282     * @see #endFakeDrag()
1283     */
1284    public boolean beginFakeDrag() {
1285        if (mIsBeingDragged) {
1286            return false;
1287        }
1288        mFakeDragging = true;
1289        setScrollState(SCROLL_STATE_DRAGGING);
1290        mInitialMotionX = mLastMotionX = 0;
1291        if (mVelocityTracker == null) {
1292            mVelocityTracker = VelocityTracker.obtain();
1293        } else {
1294            mVelocityTracker.clear();
1295        }
1296        final long time = SystemClock.uptimeMillis();
1297        final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
1298        mVelocityTracker.addMovement(ev);
1299        ev.recycle();
1300        mFakeDragBeginTime = time;
1301        return true;
1302    }
1303
1304    /**
1305     * End a fake drag of the pager.
1306     *
1307     * @see #beginFakeDrag()
1308     * @see #fakeDragBy(float)
1309     */
1310    public void endFakeDrag() {
1311        if (!mFakeDragging) {
1312            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
1313        }
1314
1315        final VelocityTracker velocityTracker = mVelocityTracker;
1316        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1317        int initialVelocity = (int)VelocityTrackerCompat.getYVelocity(
1318                velocityTracker, mActivePointerId);
1319        mPopulatePending = true;
1320        if ((Math.abs(initialVelocity) > mMinimumVelocity)
1321                || Math.abs(mInitialMotionX-mLastMotionX) >= (getWidth()/3)) {
1322            if (mLastMotionX > mInitialMotionX) {
1323                setCurrentItemInternal(mCurItem-1, true, true);
1324            } else {
1325                setCurrentItemInternal(mCurItem+1, true, true);
1326            }
1327        } else {
1328            setCurrentItemInternal(mCurItem, true, true);
1329        }
1330        endDrag();
1331
1332        mFakeDragging = false;
1333    }
1334
1335    /**
1336     * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
1337     *
1338     * @param xOffset Offset in pixels to drag by.
1339     * @see #beginFakeDrag()
1340     * @see #endFakeDrag()
1341     */
1342    public void fakeDragBy(float xOffset) {
1343        if (!mFakeDragging) {
1344            throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
1345        }
1346
1347        mLastMotionX += xOffset;
1348        float scrollX = getScrollX() - xOffset;
1349        final int width = getWidth();
1350        final int widthWithMargin = width + mPageMargin;
1351
1352        final float leftBound = Math.max(0, (mCurItem - 1) * widthWithMargin);
1353        final float rightBound =
1354                Math.min(mCurItem + 1, mAdapter.getCount() - 1) * widthWithMargin;
1355        if (scrollX < leftBound) {
1356            scrollX = leftBound;
1357        } else if (scrollX > rightBound) {
1358            scrollX = rightBound;
1359        }
1360        // Don't lose the rounded component
1361        mLastMotionX += scrollX - (int) scrollX;
1362        scrollTo((int) scrollX, getScrollY());
1363        if (mOnPageChangeListener != null) {
1364            final int position = (int) scrollX / widthWithMargin;
1365            final int positionOffsetPixels = (int) scrollX % widthWithMargin;
1366            final float positionOffset = (float) positionOffsetPixels / widthWithMargin;
1367            mOnPageChangeListener.onPageScrolled(position, positionOffset,
1368                    positionOffsetPixels);
1369        }
1370
1371        // Synthesize an event for the VelocityTracker.
1372        final long time = SystemClock.uptimeMillis();
1373        final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
1374                mLastMotionX, 0, 0);
1375        mVelocityTracker.addMovement(ev);
1376        ev.recycle();
1377    }
1378
1379    /**
1380     * Returns true if a fake drag is in progress.
1381     *
1382     * @return true if currently in a fake drag, false otherwise.
1383     *
1384     * @see #beginFakeDrag()
1385     * @see #fakeDragBy(float)
1386     * @see #endFakeDrag()
1387     */
1388    public boolean isFakeDragging() {
1389        return mFakeDragging;
1390    }
1391
1392    private void onSecondaryPointerUp(MotionEvent ev) {
1393        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
1394        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
1395        if (pointerId == mActivePointerId) {
1396            // This was our active pointer going up. Choose a new
1397            // active pointer and adjust accordingly.
1398            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
1399            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
1400            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
1401            if (mVelocityTracker != null) {
1402                mVelocityTracker.clear();
1403            }
1404        }
1405    }
1406
1407    private void endDrag() {
1408        mIsBeingDragged = false;
1409        mIsUnableToDrag = false;
1410
1411        if (mVelocityTracker != null) {
1412            mVelocityTracker.recycle();
1413            mVelocityTracker = null;
1414        }
1415    }
1416
1417    private void setScrollingCacheEnabled(boolean enabled) {
1418        if (mScrollingCacheEnabled != enabled) {
1419            mScrollingCacheEnabled = enabled;
1420            if (USE_CACHE) {
1421                final int size = getChildCount();
1422                for (int i = 0; i < size; ++i) {
1423                    final View child = getChildAt(i);
1424                    if (child.getVisibility() != GONE) {
1425                        child.setDrawingCacheEnabled(enabled);
1426                    }
1427                }
1428            }
1429        }
1430    }
1431
1432    /**
1433     * Tests scrollability within child views of v given a delta of dx.
1434     *
1435     * @param v View to test for horizontal scrollability
1436     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
1437     *               or just its children (false).
1438     * @param dx Delta scrolled in pixels
1439     * @param x X coordinate of the active touch point
1440     * @param y Y coordinate of the active touch point
1441     * @return true if child views of v can be scrolled by delta of dx.
1442     */
1443    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
1444        if (v instanceof ViewGroup) {
1445            final ViewGroup group = (ViewGroup) v;
1446            final int scrollX = v.getScrollX();
1447            final int scrollY = v.getScrollY();
1448            final int count = group.getChildCount();
1449            // Count backwards - let topmost views consume scroll distance first.
1450            for (int i = count - 1; i >= 0; i--) {
1451                // TODO: Add versioned support here for transformed views.
1452                // This will not work for transformed views in Honeycomb+
1453                final View child = group.getChildAt(i);
1454                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
1455                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
1456                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
1457                                y + scrollY - child.getTop())) {
1458                    return true;
1459                }
1460            }
1461        }
1462
1463        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
1464    }
1465
1466    @Override
1467    public boolean dispatchKeyEvent(KeyEvent event) {
1468        // Let the focused view and/or our descendants get the key first
1469        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
1470    }
1471
1472    /**
1473     * You can call this function yourself to have the scroll view perform
1474     * scrolling from a key event, just as if the event had been dispatched to
1475     * it by the view hierarchy.
1476     *
1477     * @param event The key event to execute.
1478     * @return Return true if the event was handled, else false.
1479     */
1480    public boolean executeKeyEvent(KeyEvent event) {
1481        boolean handled = false;
1482        if (event.getAction() == KeyEvent.ACTION_DOWN) {
1483            switch (event.getKeyCode()) {
1484                case KeyEvent.KEYCODE_DPAD_LEFT:
1485                    handled = arrowScroll(FOCUS_LEFT);
1486                    break;
1487                case KeyEvent.KEYCODE_DPAD_RIGHT:
1488                    handled = arrowScroll(FOCUS_RIGHT);
1489                    break;
1490                case KeyEvent.KEYCODE_TAB:
1491                    if (KeyEventCompat.hasNoModifiers(event)) {
1492                        handled = arrowScroll(FOCUS_FORWARD);
1493                    } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
1494                        handled = arrowScroll(FOCUS_BACKWARD);
1495                    }
1496                    break;
1497            }
1498        }
1499        return handled;
1500    }
1501
1502    public boolean arrowScroll(int direction) {
1503        View currentFocused = findFocus();
1504        if (currentFocused == this) currentFocused = null;
1505
1506        boolean handled = false;
1507
1508        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
1509                direction);
1510        if (nextFocused != null && nextFocused != currentFocused) {
1511            if (direction == View.FOCUS_LEFT) {
1512                // If there is nothing to the left, or this is causing us to
1513                // jump to the right, then what we really want to do is page left.
1514                if (currentFocused != null && nextFocused.getLeft() >= currentFocused.getLeft()) {
1515                    handled = pageLeft();
1516                } else {
1517                    handled = nextFocused.requestFocus();
1518                }
1519            } else if (direction == View.FOCUS_RIGHT) {
1520                // If there is nothing to the right, or this is causing us to
1521                // jump to the left, then what we really want to do is page right.
1522                if (currentFocused != null && nextFocused.getLeft() <= currentFocused.getLeft()) {
1523                    handled = pageRight();
1524                } else {
1525                    handled = nextFocused.requestFocus();
1526                }
1527            }
1528        } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
1529            // Trying to move left and nothing there; try to page.
1530            handled = pageLeft();
1531        } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
1532            // Trying to move right and nothing there; try to page.
1533            handled = pageRight();
1534        }
1535        if (handled) {
1536            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
1537        }
1538        return handled;
1539    }
1540
1541    boolean pageLeft() {
1542        if (mCurItem > 0) {
1543            setCurrentItem(mCurItem-1, true);
1544            return true;
1545        }
1546        return false;
1547    }
1548
1549    boolean pageRight() {
1550        if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
1551            setCurrentItem(mCurItem+1, true);
1552            return true;
1553        }
1554        return false;
1555    }
1556
1557    /**
1558     * We only want the current page that is being shown to be focusable.
1559     */
1560    @Override
1561    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
1562        final int focusableCount = views.size();
1563
1564        final int descendantFocusability = getDescendantFocusability();
1565
1566        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
1567            for (int i = 0; i < getChildCount(); i++) {
1568                final View child = getChildAt(i);
1569                if (child.getVisibility() == VISIBLE) {
1570                    ItemInfo ii = infoForChild(child);
1571                    if (ii != null && ii.position == mCurItem) {
1572                        child.addFocusables(views, direction, focusableMode);
1573                    }
1574                }
1575            }
1576        }
1577
1578        // we add ourselves (if focusable) in all cases except for when we are
1579        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
1580        // to avoid the focus search finding layouts when a more precise search
1581        // among the focusable children would be more interesting.
1582        if (
1583            descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
1584                // No focusable descendants
1585                (focusableCount == views.size())) {
1586            // Note that we can't call the superclass here, because it will
1587            // add all views in.  So we need to do the same thing View does.
1588            if (!isFocusable()) {
1589                return;
1590            }
1591            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
1592                    isInTouchMode() && !isFocusableInTouchMode()) {
1593                return;
1594            }
1595            if (views != null) {
1596                views.add(this);
1597            }
1598        }
1599    }
1600
1601    /**
1602     * We only want the current page that is being shown to be touchable.
1603     */
1604    @Override
1605    public void addTouchables(ArrayList<View> views) {
1606        // Note that we don't call super.addTouchables(), which means that
1607        // we don't call View.addTouchables().  This is okay because a ViewPager
1608        // is itself not touchable.
1609        for (int i = 0; i < getChildCount(); i++) {
1610            final View child = getChildAt(i);
1611            if (child.getVisibility() == VISIBLE) {
1612                ItemInfo ii = infoForChild(child);
1613                if (ii != null && ii.position == mCurItem) {
1614                    child.addTouchables(views);
1615                }
1616            }
1617        }
1618    }
1619
1620    /**
1621     * We only want the current page that is being shown to be focusable.
1622     */
1623    @Override
1624    protected boolean onRequestFocusInDescendants(int direction,
1625            Rect previouslyFocusedRect) {
1626        int index;
1627        int increment;
1628        int end;
1629        int count = getChildCount();
1630        if ((direction & FOCUS_FORWARD) != 0) {
1631            index = 0;
1632            increment = 1;
1633            end = count;
1634        } else {
1635            index = count - 1;
1636            increment = -1;
1637            end = -1;
1638        }
1639        for (int i = index; i != end; i += increment) {
1640            View child = getChildAt(i);
1641            if (child.getVisibility() == VISIBLE) {
1642                ItemInfo ii = infoForChild(child);
1643                if (ii != null && ii.position == mCurItem) {
1644                    if (child.requestFocus(direction, previouslyFocusedRect)) {
1645                        return true;
1646                    }
1647                }
1648            }
1649        }
1650        return false;
1651    }
1652
1653    @Override
1654    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1655        // ViewPagers should only report accessibility info for the current page,
1656        // otherwise things get very confusing.
1657
1658        // TODO: Should this note something about the paging container?
1659
1660        final int childCount = getChildCount();
1661        for (int i = 0; i < childCount; i++) {
1662            final View child = getChildAt(i);
1663            if (child.getVisibility() == VISIBLE) {
1664                final ItemInfo ii = infoForChild(child);
1665                if (ii != null && ii.position == mCurItem &&
1666                        child.dispatchPopulateAccessibilityEvent(event)) {
1667                    return true;
1668                }
1669            }
1670        }
1671
1672        return false;
1673    }
1674
1675    private class DataSetObserver implements PagerAdapter.DataSetObserver {
1676        @Override
1677        public void onDataSetChanged() {
1678            dataSetChanged();
1679        }
1680    }
1681}
1682