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