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.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.database.DataSetObserver;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.os.Bundle;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.os.SystemClock;
31 import android.support.annotation.CallSuper;
32 import android.support.annotation.DrawableRes;
33 import android.support.annotation.NonNull;
34 import android.support.annotation.Nullable;
35 import android.support.v4.os.ParcelableCompat;
36 import android.support.v4.os.ParcelableCompatCreatorCallbacks;
37 import android.support.v4.view.accessibility.AccessibilityEventCompat;
38 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
39 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
40 import android.support.v4.widget.EdgeEffectCompat;
41 import android.util.AttributeSet;
42 import android.util.Log;
43 import android.view.FocusFinder;
44 import android.view.Gravity;
45 import android.view.KeyEvent;
46 import android.view.MotionEvent;
47 import android.view.SoundEffectConstants;
48 import android.view.VelocityTracker;
49 import android.view.View;
50 import android.view.ViewConfiguration;
51 import android.view.ViewGroup;
52 import android.view.ViewParent;
53 import android.view.accessibility.AccessibilityEvent;
54 import android.view.animation.Interpolator;
55 import android.widget.Scroller;
56 
57 import java.lang.annotation.ElementType;
58 import java.lang.annotation.Retention;
59 import java.lang.annotation.RetentionPolicy;
60 import java.lang.annotation.Target;
61 import java.lang.reflect.Method;
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.Comparator;
65 import java.util.List;
66 
67 /**
68  * Layout manager that allows the user to flip left and right
69  * through pages of data.  You supply an implementation of a
70  * {@link PagerAdapter} to generate the pages that the view shows.
71  *
72  * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment},
73  * which is a convenient way to supply and manage the lifecycle of each page.
74  * There are standard adapters implemented for using fragments with the ViewPager,
75  * which cover the most common use cases.  These are
76  * {@link android.support.v4.app.FragmentPagerAdapter} and
77  * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these
78  * classes have simple code showing how to build a full user interface
79  * with them.
80  *
81  * <p>Views which are annotated with the {@link DecorView} annotation are treated as
82  * part of the view pagers 'decor'. Each decor view's position can be controlled via
83  * its {@code android:layout_gravity} attribute. For example:
84  *
85  * <pre>
86  * &lt;android.support.v4.view.ViewPager
87  *     android:layout_width=&quot;match_parent&quot;
88  *     android:layout_height=&quot;match_parent&quot;&gt;
89  *
90  *     &lt;android.support.v4.view.PagerTitleStrip
91  *         android:layout_width=&quot;match_parent&quot;
92  *         android:layout_height=&quot;wrap_content&quot;
93  *         android:layout_gravity=&quot;top&quot; /&gt;
94  *
95  * &lt;/android.support.v4.view.ViewPager&gt;
96  * </pre>
97  *
98  * <p>For more information about how to use ViewPager, read <a
99  * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with
100  * Tabs</a>.</p>
101  *
102  * <p>Below is a more complicated example of ViewPager, using it in conjunction
103  * with {@link android.app.ActionBar} tabs.  You can find other examples of using
104  * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
105  *
106  * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
107  *      complete}
108  */
109 public class ViewPager extends ViewGroup {
110     private static final String TAG = "ViewPager";
111     private static final boolean DEBUG = false;
112 
113     private static final boolean USE_CACHE = false;
114 
115     private static final int DEFAULT_OFFSCREEN_PAGES = 1;
116     private static final int MAX_SETTLE_DURATION = 600; // ms
117     private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
118 
119     private static final int DEFAULT_GUTTER_SIZE = 16; // dips
120 
121     private static final int MIN_FLING_VELOCITY = 400; // dips
122 
123     private static final int[] LAYOUT_ATTRS = new int[] {
124         android.R.attr.layout_gravity
125     };
126 
127     /**
128      * Used to track what the expected number of items in the adapter should be.
129      * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
130      */
131     private int mExpectedAdapterCount;
132 
133     static class ItemInfo {
134         Object object;
135         int position;
136         boolean scrolling;
137         float widthFactor;
138         float offset;
139     }
140 
141     private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
142         @Override
143         public int compare(ItemInfo lhs, ItemInfo rhs) {
144             return lhs.position - rhs.position;
145         }
146     };
147 
148     private static final Interpolator sInterpolator = new Interpolator() {
149         public float getInterpolation(float t) {
150             t -= 1.0f;
151             return t * t * t * t * t + 1.0f;
152         }
153     };
154 
155     private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
156     private final ItemInfo mTempItem = new ItemInfo();
157 
158     private final Rect mTempRect = new Rect();
159 
160     private PagerAdapter mAdapter;
161     private int mCurItem;   // Index of currently displayed page.
162     private int mRestoredCurItem = -1;
163     private Parcelable mRestoredAdapterState = null;
164     private ClassLoader mRestoredClassLoader = null;
165 
166     private Scroller mScroller;
167     private boolean mIsScrollStarted;
168 
169     private PagerObserver mObserver;
170 
171     private int mPageMargin;
172     private Drawable mMarginDrawable;
173     private int mTopPageBounds;
174     private int mBottomPageBounds;
175 
176     // Offsets of the first and last items, if known.
177     // Set during population, used to determine if we are at the beginning
178     // or end of the pager data set during touch scrolling.
179     private float mFirstOffset = -Float.MAX_VALUE;
180     private float mLastOffset = Float.MAX_VALUE;
181 
182     private int mChildWidthMeasureSpec;
183     private int mChildHeightMeasureSpec;
184     private boolean mInLayout;
185 
186     private boolean mScrollingCacheEnabled;
187 
188     private boolean mPopulatePending;
189     private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
190 
191     private boolean mIsBeingDragged;
192     private boolean mIsUnableToDrag;
193     private int mDefaultGutterSize;
194     private int mGutterSize;
195     private int mTouchSlop;
196     /**
197      * Position of the last motion event.
198      */
199     private float mLastMotionX;
200     private float mLastMotionY;
201     private float mInitialMotionX;
202     private float mInitialMotionY;
203     /**
204      * ID of the active pointer. This is used to retain consistency during
205      * drags/flings if multiple pointers are used.
206      */
207     private int mActivePointerId = INVALID_POINTER;
208     /**
209      * Sentinel value for no current active pointer.
210      * Used by {@link #mActivePointerId}.
211      */
212     private static final int INVALID_POINTER = -1;
213 
214     /**
215      * Determines speed during touch scrolling
216      */
217     private VelocityTracker mVelocityTracker;
218     private int mMinimumVelocity;
219     private int mMaximumVelocity;
220     private int mFlingDistance;
221     private int mCloseEnough;
222 
223     // If the pager is at least this close to its final position, complete the scroll
224     // on touch down and let the user interact with the content inside instead of
225     // "catching" the flinging pager.
226     private static final int CLOSE_ENOUGH = 2; // dp
227 
228     private boolean mFakeDragging;
229     private long mFakeDragBeginTime;
230 
231     private EdgeEffectCompat mLeftEdge;
232     private EdgeEffectCompat mRightEdge;
233 
234     private boolean mFirstLayout = true;
235     private boolean mNeedCalculatePageOffsets = false;
236     private boolean mCalledSuper;
237     private int mDecorChildCount;
238 
239     private List<OnPageChangeListener> mOnPageChangeListeners;
240     private OnPageChangeListener mOnPageChangeListener;
241     private OnPageChangeListener mInternalPageChangeListener;
242     private List<OnAdapterChangeListener> mAdapterChangeListeners;
243     private PageTransformer mPageTransformer;
244     private Method mSetChildrenDrawingOrderEnabled;
245 
246     private static final int DRAW_ORDER_DEFAULT = 0;
247     private static final int DRAW_ORDER_FORWARD = 1;
248     private static final int DRAW_ORDER_REVERSE = 2;
249     private int mDrawingOrder;
250     private ArrayList<View> mDrawingOrderedChildren;
251     private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
252 
253     /**
254      * Indicates that the pager is in an idle, settled state. The current page
255      * is fully in view and no animation is in progress.
256      */
257     public static final int SCROLL_STATE_IDLE = 0;
258 
259     /**
260      * Indicates that the pager is currently being dragged by the user.
261      */
262     public static final int SCROLL_STATE_DRAGGING = 1;
263 
264     /**
265      * Indicates that the pager is in the process of settling to a final position.
266      */
267     public static final int SCROLL_STATE_SETTLING = 2;
268 
269     private final Runnable mEndScrollRunnable = new Runnable() {
270         public void run() {
271             setScrollState(SCROLL_STATE_IDLE);
272             populate();
273         }
274     };
275 
276     private int mScrollState = SCROLL_STATE_IDLE;
277 
278     /**
279      * Callback interface for responding to changing state of the selected page.
280      */
281     public interface OnPageChangeListener {
282 
283         /**
284          * This method will be invoked when the current page is scrolled, either as part
285          * of a programmatically initiated smooth scroll or a user initiated touch scroll.
286          *
287          * @param position Position index of the first page currently being displayed.
288          *                 Page position+1 will be visible if positionOffset is nonzero.
289          * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
290          * @param positionOffsetPixels Value in pixels indicating the offset from position.
291          */
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)292         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
293 
294         /**
295          * This method will be invoked when a new page becomes selected. Animation is not
296          * necessarily complete.
297          *
298          * @param position Position index of the new selected page.
299          */
onPageSelected(int position)300         public void onPageSelected(int position);
301 
302         /**
303          * Called when the scroll state changes. Useful for discovering when the user
304          * begins dragging, when the pager is automatically settling to the current page,
305          * or when it is fully stopped/idle.
306          *
307          * @param state The new scroll state.
308          * @see ViewPager#SCROLL_STATE_IDLE
309          * @see ViewPager#SCROLL_STATE_DRAGGING
310          * @see ViewPager#SCROLL_STATE_SETTLING
311          */
onPageScrollStateChanged(int state)312         public void onPageScrollStateChanged(int state);
313     }
314 
315     /**
316      * Simple implementation of the {@link OnPageChangeListener} interface with stub
317      * implementations of each method. Extend this if you do not intend to override
318      * every method of {@link OnPageChangeListener}.
319      */
320     public static class SimpleOnPageChangeListener implements OnPageChangeListener {
321         @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)322         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
323             // This space for rent
324         }
325 
326         @Override
onPageSelected(int position)327         public void onPageSelected(int position) {
328             // This space for rent
329         }
330 
331         @Override
onPageScrollStateChanged(int state)332         public void onPageScrollStateChanged(int state) {
333             // This space for rent
334         }
335     }
336 
337     /**
338      * A PageTransformer is invoked whenever a visible/attached page is scrolled.
339      * This offers an opportunity for the application to apply a custom transformation
340      * to the page views using animation properties.
341      *
342      * <p>As property animation is only supported as of Android 3.0 and forward,
343      * setting a PageTransformer on a ViewPager on earlier platform versions will
344      * be ignored.</p>
345      */
346     public interface PageTransformer {
347         /**
348          * Apply a property transformation to the given page.
349          *
350          * @param page Apply the transformation to this page
351          * @param position Position of page relative to the current front-and-center
352          *                 position of the pager. 0 is front and center. 1 is one full
353          *                 page position to the right, and -1 is one page position to the left.
354          */
transformPage(View page, float position)355         public void transformPage(View page, float position);
356     }
357 
358     /**
359      * Callback interface for responding to adapter changes.
360      */
361     public interface OnAdapterChangeListener {
362         /**
363          * Called when the adapter for the given view pager has changed.
364          *
365          * @param viewPager  ViewPager where the adapter change has happened
366          * @param oldAdapter the previously set adapter
367          * @param newAdapter the newly set adapter
368          */
onAdapterChanged(@onNull ViewPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter)369         void onAdapterChanged(@NonNull ViewPager viewPager,
370                 @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter);
371     }
372 
373     /**
374      * Annotation which allows marking of views to be decoration views when added to a view
375      * pager.
376      *
377      * <p>Views marked with this annotation can be added to the view pager with a layout resource.
378      * An example being {@link PagerTitleStrip}.</p>
379      *
380      * <p>You can also control whether a view is a decor view but setting
381      * {@link LayoutParams#isDecor} on the child's layout params.</p>
382      */
383     @Retention(RetentionPolicy.RUNTIME)
384     @Target(ElementType.TYPE)
385     public @interface DecorView {
386     }
387 
ViewPager(Context context)388     public ViewPager(Context context) {
389         super(context);
390         initViewPager();
391     }
392 
ViewPager(Context context, AttributeSet attrs)393     public ViewPager(Context context, AttributeSet attrs) {
394         super(context, attrs);
395         initViewPager();
396     }
397 
initViewPager()398     void initViewPager() {
399         setWillNotDraw(false);
400         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
401         setFocusable(true);
402         final Context context = getContext();
403         mScroller = new Scroller(context, sInterpolator);
404         final ViewConfiguration configuration = ViewConfiguration.get(context);
405         final float density = context.getResources().getDisplayMetrics().density;
406 
407         mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
408         mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
409         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
410         mLeftEdge = new EdgeEffectCompat(context);
411         mRightEdge = new EdgeEffectCompat(context);
412 
413         mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
414         mCloseEnough = (int) (CLOSE_ENOUGH * density);
415         mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
416 
417         ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
418 
419         if (ViewCompat.getImportantForAccessibility(this)
420                 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
421             ViewCompat.setImportantForAccessibility(this,
422                     ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
423         }
424 
425         ViewCompat.setOnApplyWindowInsetsListener(this,
426                 new android.support.v4.view.OnApplyWindowInsetsListener() {
427                     private final Rect mTempRect = new Rect();
428 
429                     @Override
430                     public WindowInsetsCompat onApplyWindowInsets(final View v,
431                             final WindowInsetsCompat originalInsets) {
432                         // First let the ViewPager itself try and consume them...
433                         final WindowInsetsCompat applied =
434                                 ViewCompat.onApplyWindowInsets(v, originalInsets);
435                         if (applied.isConsumed()) {
436                             // If the ViewPager consumed all insets, return now
437                             return applied;
438                         }
439 
440                         // Now we'll manually dispatch the insets to our children. Since ViewPager
441                         // children are always full-height, we do not want to use the standard
442                         // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,
443                         // the rest of the children will not receive any insets. To workaround this
444                         // we manually dispatch the applied insets, not allowing children to
445                         // consume them from each other. We do however keep track of any insets
446                         // which are consumed, returning the union of our children's consumption
447                         final Rect res = mTempRect;
448                         res.left = applied.getSystemWindowInsetLeft();
449                         res.top = applied.getSystemWindowInsetTop();
450                         res.right = applied.getSystemWindowInsetRight();
451                         res.bottom = applied.getSystemWindowInsetBottom();
452 
453                         for (int i = 0, count = getChildCount(); i < count; i++) {
454                             final WindowInsetsCompat childInsets = ViewCompat
455                                     .dispatchApplyWindowInsets(getChildAt(i), applied);
456                             // Now keep track of any consumed by tracking each dimension's min
457                             // value
458                             res.left = Math.min(childInsets.getSystemWindowInsetLeft(),
459                                     res.left);
460                             res.top = Math.min(childInsets.getSystemWindowInsetTop(),
461                                     res.top);
462                             res.right = Math.min(childInsets.getSystemWindowInsetRight(),
463                                     res.right);
464                             res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),
465                                     res.bottom);
466                         }
467 
468                         // Now return a new WindowInsets, using the consumed window insets
469                         return applied.replaceSystemWindowInsets(
470                                 res.left, res.top, res.right, res.bottom);
471                     }
472                 });
473     }
474 
475     @Override
onDetachedFromWindow()476     protected void onDetachedFromWindow() {
477         removeCallbacks(mEndScrollRunnable);
478         // To be on the safe side, abort the scroller
479         if ((mScroller != null) && !mScroller.isFinished()) {
480             mScroller.abortAnimation();
481         }
482         super.onDetachedFromWindow();
483     }
484 
setScrollState(int newState)485     private void setScrollState(int newState) {
486         if (mScrollState == newState) {
487             return;
488         }
489 
490         mScrollState = newState;
491         if (mPageTransformer != null) {
492             // PageTransformers can do complex things that benefit from hardware layers.
493             enableLayers(newState != SCROLL_STATE_IDLE);
494         }
495         dispatchOnScrollStateChanged(newState);
496     }
497 
498     /**
499      * Set a PagerAdapter that will supply views for this pager as needed.
500      *
501      * @param adapter Adapter to use
502      */
setAdapter(PagerAdapter adapter)503     public void setAdapter(PagerAdapter adapter) {
504         if (mAdapter != null) {
505             mAdapter.setViewPagerObserver(null);
506             mAdapter.startUpdate(this);
507             for (int i = 0; i < mItems.size(); i++) {
508                 final ItemInfo ii = mItems.get(i);
509                 mAdapter.destroyItem(this, ii.position, ii.object);
510             }
511             mAdapter.finishUpdate(this);
512             mItems.clear();
513             removeNonDecorViews();
514             mCurItem = 0;
515             scrollTo(0, 0);
516         }
517 
518         final PagerAdapter oldAdapter = mAdapter;
519         mAdapter = adapter;
520         mExpectedAdapterCount = 0;
521 
522         if (mAdapter != null) {
523             if (mObserver == null) {
524                 mObserver = new PagerObserver();
525             }
526             mAdapter.setViewPagerObserver(mObserver);
527             mPopulatePending = false;
528             final boolean wasFirstLayout = mFirstLayout;
529             mFirstLayout = true;
530             mExpectedAdapterCount = mAdapter.getCount();
531             if (mRestoredCurItem >= 0) {
532                 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
533                 setCurrentItemInternal(mRestoredCurItem, false, true);
534                 mRestoredCurItem = -1;
535                 mRestoredAdapterState = null;
536                 mRestoredClassLoader = null;
537             } else if (!wasFirstLayout) {
538                 populate();
539             } else {
540                 requestLayout();
541             }
542         }
543 
544         // Dispatch the change to any listeners
545         if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
546             for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
547                 mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
548             }
549         }
550     }
551 
removeNonDecorViews()552     private void removeNonDecorViews() {
553         for (int i = 0; i < getChildCount(); i++) {
554             final View child = getChildAt(i);
555             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
556             if (!lp.isDecor) {
557                 removeViewAt(i);
558                 i--;
559             }
560         }
561     }
562 
563     /**
564      * Retrieve the current adapter supplying pages.
565      *
566      * @return The currently registered PagerAdapter
567      */
getAdapter()568     public PagerAdapter getAdapter() {
569         return mAdapter;
570     }
571 
572     /**
573      * Add a listener that will be invoked whenever the adapter for this ViewPager changes.
574      *
575      * @param listener listener to add
576      */
addOnAdapterChangeListener(@onNull OnAdapterChangeListener listener)577     public void addOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) {
578         if (mAdapterChangeListeners == null) {
579             mAdapterChangeListeners = new ArrayList<>();
580         }
581         mAdapterChangeListeners.add(listener);
582     }
583 
584     /**
585      * Remove a listener that was previously added via
586      * {@link #addOnAdapterChangeListener(OnAdapterChangeListener)}.
587      *
588      * @param listener listener to remove
589      */
removeOnAdapterChangeListener(@onNull OnAdapterChangeListener listener)590     public void removeOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) {
591         if (mAdapterChangeListeners != null) {
592             mAdapterChangeListeners.remove(listener);
593         }
594     }
595 
getClientWidth()596     private int getClientWidth() {
597         return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
598     }
599 
600     /**
601      * Set the currently selected page. If the ViewPager has already been through its first
602      * layout with its current adapter there will be a smooth animated transition between
603      * the current item and the specified item.
604      *
605      * @param item Item index to select
606      */
setCurrentItem(int item)607     public void setCurrentItem(int item) {
608         mPopulatePending = false;
609         setCurrentItemInternal(item, !mFirstLayout, false);
610     }
611 
612     /**
613      * Set the currently selected page.
614      *
615      * @param item Item index to select
616      * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
617      */
setCurrentItem(int item, boolean smoothScroll)618     public void setCurrentItem(int item, boolean smoothScroll) {
619         mPopulatePending = false;
620         setCurrentItemInternal(item, smoothScroll, false);
621     }
622 
getCurrentItem()623     public int getCurrentItem() {
624         return mCurItem;
625     }
626 
setCurrentItemInternal(int item, boolean smoothScroll, boolean always)627     void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
628         setCurrentItemInternal(item, smoothScroll, always, 0);
629     }
630 
setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity)631     void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
632         if (mAdapter == null || mAdapter.getCount() <= 0) {
633             setScrollingCacheEnabled(false);
634             return;
635         }
636         if (!always && mCurItem == item && mItems.size() != 0) {
637             setScrollingCacheEnabled(false);
638             return;
639         }
640 
641         if (item < 0) {
642             item = 0;
643         } else if (item >= mAdapter.getCount()) {
644             item = mAdapter.getCount() - 1;
645         }
646         final int pageLimit = mOffscreenPageLimit;
647         if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
648             // We are doing a jump by more than one page.  To avoid
649             // glitches, we want to keep all current pages in the view
650             // until the scroll ends.
651             for (int i=0; i<mItems.size(); i++) {
652                 mItems.get(i).scrolling = true;
653             }
654         }
655         final boolean dispatchSelected = mCurItem != item;
656 
657         if (mFirstLayout) {
658             // We don't have any idea how big we are yet and shouldn't have any pages either.
659             // Just set things up and let the pending layout handle things.
660             mCurItem = item;
661             if (dispatchSelected) {
662                 dispatchOnPageSelected(item);
663             }
664             requestLayout();
665         } else {
666             populate(item);
667             scrollToItem(item, smoothScroll, velocity, dispatchSelected);
668         }
669     }
670 
scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)671     private void scrollToItem(int item, boolean smoothScroll, int velocity,
672             boolean dispatchSelected) {
673         final ItemInfo curInfo = infoForPosition(item);
674         int destX = 0;
675         if (curInfo != null) {
676             final int width = getClientWidth();
677             destX = (int) (width * Math.max(mFirstOffset,
678                     Math.min(curInfo.offset, mLastOffset)));
679         }
680         if (smoothScroll) {
681             smoothScrollTo(destX, 0, velocity);
682             if (dispatchSelected) {
683                 dispatchOnPageSelected(item);
684             }
685         } else {
686             if (dispatchSelected) {
687                 dispatchOnPageSelected(item);
688             }
689             completeScroll(false);
690             scrollTo(destX, 0);
691             pageScrolled(destX);
692         }
693     }
694 
695     /**
696      * Set a listener that will be invoked whenever the page changes or is incrementally
697      * scrolled. See {@link OnPageChangeListener}.
698      *
699      * @param listener Listener to set
700      *
701      * @deprecated Use {@link #addOnPageChangeListener(OnPageChangeListener)}
702      * and {@link #removeOnPageChangeListener(OnPageChangeListener)} instead.
703      */
704     @Deprecated
setOnPageChangeListener(OnPageChangeListener listener)705     public void setOnPageChangeListener(OnPageChangeListener listener) {
706         mOnPageChangeListener = listener;
707     }
708 
709     /**
710      * Add a listener that will be invoked whenever the page changes or is incrementally
711      * scrolled. See {@link OnPageChangeListener}.
712      *
713      * <p>Components that add a listener should take care to remove it when finished.
714      * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()}
715      * to remove all attached listeners.</p>
716      *
717      * @param listener listener to add
718      */
addOnPageChangeListener(OnPageChangeListener listener)719     public void addOnPageChangeListener(OnPageChangeListener listener) {
720         if (mOnPageChangeListeners == null) {
721             mOnPageChangeListeners = new ArrayList<>();
722         }
723         mOnPageChangeListeners.add(listener);
724     }
725 
726     /**
727      * Remove a listener that was previously added via
728      * {@link #addOnPageChangeListener(OnPageChangeListener)}.
729      *
730      * @param listener listener to remove
731      */
removeOnPageChangeListener(OnPageChangeListener listener)732     public void removeOnPageChangeListener(OnPageChangeListener listener) {
733         if (mOnPageChangeListeners != null) {
734             mOnPageChangeListeners.remove(listener);
735         }
736     }
737 
738     /**
739      * Remove all listeners that are notified of any changes in scroll state or position.
740      */
clearOnPageChangeListeners()741     public void clearOnPageChangeListeners() {
742         if (mOnPageChangeListeners != null) {
743             mOnPageChangeListeners.clear();
744         }
745     }
746 
747     /**
748      * Set a {@link PageTransformer} that will be called for each attached page whenever
749      * the scroll position is changed. This allows the application to apply custom property
750      * transformations to each page, overriding the default sliding look and feel.
751      *
752      * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
753      * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
754      *
755      * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
756      *                            to be drawn from last to first instead of first to last.
757      * @param transformer PageTransformer that will modify each page's animation properties
758      */
setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer)759     public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
760         if (Build.VERSION.SDK_INT >= 11) {
761             final boolean hasTransformer = transformer != null;
762             final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
763             mPageTransformer = transformer;
764             setChildrenDrawingOrderEnabledCompat(hasTransformer);
765             if (hasTransformer) {
766                 mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
767             } else {
768                 mDrawingOrder = DRAW_ORDER_DEFAULT;
769             }
770             if (needsPopulate) populate();
771         }
772     }
773 
setChildrenDrawingOrderEnabledCompat(boolean enable)774     void setChildrenDrawingOrderEnabledCompat(boolean enable) {
775         if (Build.VERSION.SDK_INT >= 7) {
776             if (mSetChildrenDrawingOrderEnabled == null) {
777                 try {
778                     mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(
779                             "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE });
780                 } catch (NoSuchMethodException e) {
781                     Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);
782                 }
783             }
784             try {
785                 mSetChildrenDrawingOrderEnabled.invoke(this, enable);
786             } catch (Exception e) {
787                 Log.e(TAG, "Error changing children drawing order", e);
788             }
789         }
790     }
791 
792     @Override
getChildDrawingOrder(int childCount, int i)793     protected int getChildDrawingOrder(int childCount, int i) {
794         final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
795         final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
796         return result;
797     }
798 
799     /**
800      * Set a separate OnPageChangeListener for internal use by the support library.
801      *
802      * @param listener Listener to set
803      * @return The old listener that was set, if any.
804      */
setInternalPageChangeListener(OnPageChangeListener listener)805     OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
806         OnPageChangeListener oldListener = mInternalPageChangeListener;
807         mInternalPageChangeListener = listener;
808         return oldListener;
809     }
810 
811     /**
812      * Returns the number of pages that will be retained to either side of the
813      * current page in the view hierarchy in an idle state. Defaults to 1.
814      *
815      * @return How many pages will be kept offscreen on either side
816      * @see #setOffscreenPageLimit(int)
817      */
getOffscreenPageLimit()818     public int getOffscreenPageLimit() {
819         return mOffscreenPageLimit;
820     }
821 
822     /**
823      * Set the number of pages that should be retained to either side of the
824      * current page in the view hierarchy in an idle state. Pages beyond this
825      * limit will be recreated from the adapter when needed.
826      *
827      * <p>This is offered as an optimization. If you know in advance the number
828      * of pages you will need to support or have lazy-loading mechanisms in place
829      * on your pages, tweaking this setting can have benefits in perceived smoothness
830      * of paging animations and interaction. If you have a small number of pages (3-4)
831      * that you can keep active all at once, less time will be spent in layout for
832      * newly created view subtrees as the user pages back and forth.</p>
833      *
834      * <p>You should keep this limit low, especially if your pages have complex layouts.
835      * This setting defaults to 1.</p>
836      *
837      * @param limit How many pages will be kept offscreen in an idle state.
838      */
setOffscreenPageLimit(int limit)839     public void setOffscreenPageLimit(int limit) {
840         if (limit < DEFAULT_OFFSCREEN_PAGES) {
841             Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
842                     DEFAULT_OFFSCREEN_PAGES);
843             limit = DEFAULT_OFFSCREEN_PAGES;
844         }
845         if (limit != mOffscreenPageLimit) {
846             mOffscreenPageLimit = limit;
847             populate();
848         }
849     }
850 
851     /**
852      * Set the margin between pages.
853      *
854      * @param marginPixels Distance between adjacent pages in pixels
855      * @see #getPageMargin()
856      * @see #setPageMarginDrawable(Drawable)
857      * @see #setPageMarginDrawable(int)
858      */
setPageMargin(int marginPixels)859     public void setPageMargin(int marginPixels) {
860         final int oldMargin = mPageMargin;
861         mPageMargin = marginPixels;
862 
863         final int width = getWidth();
864         recomputeScrollPosition(width, width, marginPixels, oldMargin);
865 
866         requestLayout();
867     }
868 
869     /**
870      * Return the margin between pages.
871      *
872      * @return The size of the margin in pixels
873      */
getPageMargin()874     public int getPageMargin() {
875         return mPageMargin;
876     }
877 
878     /**
879      * Set a drawable that will be used to fill the margin between pages.
880      *
881      * @param d Drawable to display between pages
882      */
setPageMarginDrawable(Drawable d)883     public void setPageMarginDrawable(Drawable d) {
884         mMarginDrawable = d;
885         if (d != null) refreshDrawableState();
886         setWillNotDraw(d == null);
887         invalidate();
888     }
889 
890     /**
891      * Set a drawable that will be used to fill the margin between pages.
892      *
893      * @param resId Resource ID of a drawable to display between pages
894      */
setPageMarginDrawable(@rawableRes int resId)895     public void setPageMarginDrawable(@DrawableRes int resId) {
896         setPageMarginDrawable(getContext().getResources().getDrawable(resId));
897     }
898 
899     @Override
verifyDrawable(Drawable who)900     protected boolean verifyDrawable(Drawable who) {
901         return super.verifyDrawable(who) || who == mMarginDrawable;
902     }
903 
904     @Override
drawableStateChanged()905     protected void drawableStateChanged() {
906         super.drawableStateChanged();
907         final Drawable d = mMarginDrawable;
908         if (d != null && d.isStateful()) {
909             d.setState(getDrawableState());
910         }
911     }
912 
913     // We want the duration of the page snap animation to be influenced by the distance that
914     // the screen has to travel, however, we don't want this duration to be effected in a
915     // purely linear fashion. Instead, we use this method to moderate the effect that the distance
916     // of travel has on the overall snap duration.
distanceInfluenceForSnapDuration(float f)917     float distanceInfluenceForSnapDuration(float f) {
918         f -= 0.5f; // center the values about 0.
919         f *= 0.3f * Math.PI / 2.0f;
920         return (float) Math.sin(f);
921     }
922 
923     /**
924      * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
925      *
926      * @param x the number of pixels to scroll by on the X axis
927      * @param y the number of pixels to scroll by on the Y axis
928      */
smoothScrollTo(int x, int y)929     void smoothScrollTo(int x, int y) {
930         smoothScrollTo(x, y, 0);
931     }
932 
933     /**
934      * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
935      *
936      * @param x the number of pixels to scroll by on the X axis
937      * @param y the number of pixels to scroll by on the Y axis
938      * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
939      */
smoothScrollTo(int x, int y, int velocity)940     void smoothScrollTo(int x, int y, int velocity) {
941         if (getChildCount() == 0) {
942             // Nothing to do.
943             setScrollingCacheEnabled(false);
944             return;
945         }
946 
947         int sx;
948         boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
949         if (wasScrolling) {
950             // We're in the middle of a previously initiated scrolling. Check to see
951             // whether that scrolling has actually started (if we always call getStartX
952             // we can get a stale value from the scroller if it hadn't yet had its first
953             // computeScrollOffset call) to decide what is the current scrolling position.
954             sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
955             // And abort the current scrolling.
956             mScroller.abortAnimation();
957             setScrollingCacheEnabled(false);
958         } else {
959             sx = getScrollX();
960         }
961         int sy = getScrollY();
962         int dx = x - sx;
963         int dy = y - sy;
964         if (dx == 0 && dy == 0) {
965             completeScroll(false);
966             populate();
967             setScrollState(SCROLL_STATE_IDLE);
968             return;
969         }
970 
971         setScrollingCacheEnabled(true);
972         setScrollState(SCROLL_STATE_SETTLING);
973 
974         final int width = getClientWidth();
975         final int halfWidth = width / 2;
976         final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
977         final float distance = halfWidth + halfWidth *
978                 distanceInfluenceForSnapDuration(distanceRatio);
979 
980         int duration;
981         velocity = Math.abs(velocity);
982         if (velocity > 0) {
983             duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
984         } else {
985             final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
986             final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
987             duration = (int) ((pageDelta + 1) * 100);
988         }
989         duration = Math.min(duration, MAX_SETTLE_DURATION);
990 
991         // Reset the "scroll started" flag. It will be flipped to true in all places
992         // where we call computeScrollOffset().
993         mIsScrollStarted = false;
994         mScroller.startScroll(sx, sy, dx, dy, duration);
995         ViewCompat.postInvalidateOnAnimation(this);
996     }
997 
addNewItem(int position, int index)998     ItemInfo addNewItem(int position, int index) {
999         ItemInfo ii = new ItemInfo();
1000         ii.position = position;
1001         ii.object = mAdapter.instantiateItem(this, position);
1002         ii.widthFactor = mAdapter.getPageWidth(position);
1003         if (index < 0 || index >= mItems.size()) {
1004             mItems.add(ii);
1005         } else {
1006             mItems.add(index, ii);
1007         }
1008         return ii;
1009     }
1010 
dataSetChanged()1011     void dataSetChanged() {
1012         // This method only gets called if our observer is attached, so mAdapter is non-null.
1013 
1014         final int adapterCount = mAdapter.getCount();
1015         mExpectedAdapterCount = adapterCount;
1016         boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
1017                 mItems.size() < adapterCount;
1018         int newCurrItem = mCurItem;
1019 
1020         boolean isUpdating = false;
1021         for (int i = 0; i < mItems.size(); i++) {
1022             final ItemInfo ii = mItems.get(i);
1023             final int newPos = mAdapter.getItemPosition(ii.object);
1024 
1025             if (newPos == PagerAdapter.POSITION_UNCHANGED) {
1026                 continue;
1027             }
1028 
1029             if (newPos == PagerAdapter.POSITION_NONE) {
1030                 mItems.remove(i);
1031                 i--;
1032 
1033                 if (!isUpdating) {
1034                     mAdapter.startUpdate(this);
1035                     isUpdating = true;
1036                 }
1037 
1038                 mAdapter.destroyItem(this, ii.position, ii.object);
1039                 needPopulate = true;
1040 
1041                 if (mCurItem == ii.position) {
1042                     // Keep the current item in the valid range
1043                     newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
1044                     needPopulate = true;
1045                 }
1046                 continue;
1047             }
1048 
1049             if (ii.position != newPos) {
1050                 if (ii.position == mCurItem) {
1051                     // Our current item changed position. Follow it.
1052                     newCurrItem = newPos;
1053                 }
1054 
1055                 ii.position = newPos;
1056                 needPopulate = true;
1057             }
1058         }
1059 
1060         if (isUpdating) {
1061             mAdapter.finishUpdate(this);
1062         }
1063 
1064         Collections.sort(mItems, COMPARATOR);
1065 
1066         if (needPopulate) {
1067             // Reset our known page widths; populate will recompute them.
1068             final int childCount = getChildCount();
1069             for (int i = 0; i < childCount; i++) {
1070                 final View child = getChildAt(i);
1071                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1072                 if (!lp.isDecor) {
1073                     lp.widthFactor = 0.f;
1074                 }
1075             }
1076 
1077             setCurrentItemInternal(newCurrItem, false, true);
1078             requestLayout();
1079         }
1080     }
1081 
1082     void populate() {
1083         populate(mCurItem);
1084     }
1085 
1086     void populate(int newCurrentItem) {
1087         ItemInfo oldCurInfo = null;
1088         if (mCurItem != newCurrentItem) {
1089             oldCurInfo = infoForPosition(mCurItem);
1090             mCurItem = newCurrentItem;
1091         }
1092 
1093         if (mAdapter == null) {
1094             sortChildDrawingOrder();
1095             return;
1096         }
1097 
1098         // Bail now if we are waiting to populate.  This is to hold off
1099         // on creating views from the time the user releases their finger to
1100         // fling to a new position until we have finished the scroll to
1101         // that position, avoiding glitches from happening at that point.
1102         if (mPopulatePending) {
1103             if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
1104             sortChildDrawingOrder();
1105             return;
1106         }
1107 
1108         // Also, don't populate until we are attached to a window.  This is to
1109         // avoid trying to populate before we have restored our view hierarchy
1110         // state and conflicting with what is restored.
1111         if (getWindowToken() == null) {
1112             return;
1113         }
1114 
1115         mAdapter.startUpdate(this);
1116 
1117         final int pageLimit = mOffscreenPageLimit;
1118         final int startPos = Math.max(0, mCurItem - pageLimit);
1119         final int N = mAdapter.getCount();
1120         final int endPos = Math.min(N-1, mCurItem + pageLimit);
1121 
1122         if (N != mExpectedAdapterCount) {
1123             String resName;
1124             try {
1125                 resName = getResources().getResourceName(getId());
1126             } catch (Resources.NotFoundException e) {
1127                 resName = Integer.toHexString(getId());
1128             }
1129             throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
1130                     " contents without calling PagerAdapter#notifyDataSetChanged!" +
1131                     " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
1132                     " Pager id: " + resName +
1133                     " Pager class: " + getClass() +
1134                     " Problematic adapter: " + mAdapter.getClass());
1135         }
1136 
1137         // Locate the currently focused item or add it if needed.
1138         int curIndex = -1;
1139         ItemInfo curItem = null;
1140         for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
1141             final ItemInfo ii = mItems.get(curIndex);
1142             if (ii.position >= mCurItem) {
1143                 if (ii.position == mCurItem) curItem = ii;
1144                 break;
1145             }
1146         }
1147 
1148         if (curItem == null && N > 0) {
1149             curItem = addNewItem(mCurItem, curIndex);
1150         }
1151 
1152         // Fill 3x the available width or up to the number of offscreen
1153         // pages requested to either side, whichever is larger.
1154         // If we have no current item we have no work to do.
1155         if (curItem != null) {
1156             float extraWidthLeft = 0.f;
1157             int itemIndex = curIndex - 1;
1158             ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1159             final int clientWidth = getClientWidth();
1160             final float leftWidthNeeded = clientWidth <= 0 ? 0 :
1161                     2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
1162             for (int pos = mCurItem - 1; pos >= 0; pos--) {
1163                 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
1164                     if (ii == null) {
1165                         break;
1166                     }
1167                     if (pos == ii.position && !ii.scrolling) {
1168                         mItems.remove(itemIndex);
1169                         mAdapter.destroyItem(this, pos, ii.object);
1170                         if (DEBUG) {
1171                             Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
1172                                     " view: " + ((View) ii.object));
1173                         }
1174                         itemIndex--;
1175                         curIndex--;
1176                         ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1177                     }
1178                 } else if (ii != null && pos == ii.position) {
1179                     extraWidthLeft += ii.widthFactor;
1180                     itemIndex--;
1181                     ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1182                 } else {
1183                     ii = addNewItem(pos, itemIndex + 1);
1184                     extraWidthLeft += ii.widthFactor;
1185                     curIndex++;
1186                     ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
1187                 }
1188             }
1189 
1190             float extraWidthRight = curItem.widthFactor;
1191             itemIndex = curIndex + 1;
1192             if (extraWidthRight < 2.f) {
1193                 ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1194                 final float rightWidthNeeded = clientWidth <= 0 ? 0 :
1195                         (float) getPaddingRight() / (float) clientWidth + 2.f;
1196                 for (int pos = mCurItem + 1; pos < N; pos++) {
1197                     if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
1198                         if (ii == null) {
1199                             break;
1200                         }
1201                         if (pos == ii.position && !ii.scrolling) {
1202                             mItems.remove(itemIndex);
1203                             mAdapter.destroyItem(this, pos, ii.object);
1204                             if (DEBUG) {
1205                                 Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
1206                                         " view: " + ((View) ii.object));
1207                             }
1208                             ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1209                         }
1210                     } else if (ii != null && pos == ii.position) {
1211                         extraWidthRight += ii.widthFactor;
1212                         itemIndex++;
1213                         ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1214                     } else {
1215                         ii = addNewItem(pos, itemIndex);
1216                         itemIndex++;
1217                         extraWidthRight += ii.widthFactor;
1218                         ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
1219                     }
1220                 }
1221             }
1222 
1223             calculatePageOffsets(curItem, curIndex, oldCurInfo);
1224         }
1225 
1226         if (DEBUG) {
1227             Log.i(TAG, "Current page list:");
1228             for (int i=0; i<mItems.size(); i++) {
1229                 Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
1230             }
1231         }
1232 
1233         mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
1234 
1235         mAdapter.finishUpdate(this);
1236 
1237         // Check width measurement of current pages and drawing sort order.
1238         // Update LayoutParams as needed.
1239         final int childCount = getChildCount();
1240         for (int i = 0; i < childCount; i++) {
1241             final View child = getChildAt(i);
1242             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1243             lp.childIndex = i;
1244             if (!lp.isDecor && lp.widthFactor == 0.f) {
1245                 // 0 means requery the adapter for this, it doesn't have a valid width.
1246                 final ItemInfo ii = infoForChild(child);
1247                 if (ii != null) {
1248                     lp.widthFactor = ii.widthFactor;
1249                     lp.position = ii.position;
1250                 }
1251             }
1252         }
1253         sortChildDrawingOrder();
1254 
1255         if (hasFocus()) {
1256             View currentFocused = findFocus();
1257             ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
1258             if (ii == null || ii.position != mCurItem) {
1259                 for (int i=0; i<getChildCount(); i++) {
1260                     View child = getChildAt(i);
1261                     ii = infoForChild(child);
1262                     if (ii != null && ii.position == mCurItem) {
1263                         if (child.requestFocus(View.FOCUS_FORWARD)) {
1264                             break;
1265                         }
1266                     }
1267                 }
1268             }
1269         }
1270     }
1271 
1272     private void sortChildDrawingOrder() {
1273         if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
1274             if (mDrawingOrderedChildren == null) {
1275                 mDrawingOrderedChildren = new ArrayList<View>();
1276             } else {
1277                 mDrawingOrderedChildren.clear();
1278             }
1279             final int childCount = getChildCount();
1280             for (int i = 0; i < childCount; i++) {
1281                 final View child = getChildAt(i);
1282                 mDrawingOrderedChildren.add(child);
1283             }
1284             Collections.sort(mDrawingOrderedChildren, sPositionComparator);
1285         }
1286     }
1287 
1288     private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
1289         final int N = mAdapter.getCount();
1290         final int width = getClientWidth();
1291         final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
1292         // Fix up offsets for later layout.
1293         if (oldCurInfo != null) {
1294             final int oldCurPosition = oldCurInfo.position;
1295             // Base offsets off of oldCurInfo.
1296             if (oldCurPosition < curItem.position) {
1297                 int itemIndex = 0;
1298                 ItemInfo ii = null;
1299                 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
1300                 for (int pos = oldCurPosition + 1;
1301                         pos <= curItem.position && itemIndex < mItems.size(); pos++) {
1302                     ii = mItems.get(itemIndex);
1303                     while (pos > ii.position && itemIndex < mItems.size() - 1) {
1304                         itemIndex++;
1305                         ii = mItems.get(itemIndex);
1306                     }
1307                     while (pos < ii.position) {
1308                         // We don't have an item populated for this,
1309                         // ask the adapter for an offset.
1310                         offset += mAdapter.getPageWidth(pos) + marginOffset;
1311                         pos++;
1312                     }
1313                     ii.offset = offset;
1314                     offset += ii.widthFactor + marginOffset;
1315                 }
1316             } else if (oldCurPosition > curItem.position) {
1317                 int itemIndex = mItems.size() - 1;
1318                 ItemInfo ii = null;
1319                 float offset = oldCurInfo.offset;
1320                 for (int pos = oldCurPosition - 1;
1321                         pos >= curItem.position && itemIndex >= 0; pos--) {
1322                     ii = mItems.get(itemIndex);
1323                     while (pos < ii.position && itemIndex > 0) {
1324                         itemIndex--;
1325                         ii = mItems.get(itemIndex);
1326                     }
1327                     while (pos > ii.position) {
1328                         // We don't have an item populated for this,
1329                         // ask the adapter for an offset.
1330                         offset -= mAdapter.getPageWidth(pos) + marginOffset;
1331                         pos--;
1332                     }
1333                     offset -= ii.widthFactor + marginOffset;
1334                     ii.offset = offset;
1335                 }
1336             }
1337         }
1338 
1339         // Base all offsets off of curItem.
1340         final int itemCount = mItems.size();
1341         float offset = curItem.offset;
1342         int pos = curItem.position - 1;
1343         mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
1344         mLastOffset = curItem.position == N - 1 ?
1345                 curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
1346         // Previous pages
1347         for (int i = curIndex - 1; i >= 0; i--, pos--) {
1348             final ItemInfo ii = mItems.get(i);
1349             while (pos > ii.position) {
1350                 offset -= mAdapter.getPageWidth(pos--) + marginOffset;
1351             }
1352             offset -= ii.widthFactor + marginOffset;
1353             ii.offset = offset;
1354             if (ii.position == 0) mFirstOffset = offset;
1355         }
1356         offset = curItem.offset + curItem.widthFactor + marginOffset;
1357         pos = curItem.position + 1;
1358         // Next pages
1359         for (int i = curIndex + 1; i < itemCount; i++, pos++) {
1360             final ItemInfo ii = mItems.get(i);
1361             while (pos < ii.position) {
1362                 offset += mAdapter.getPageWidth(pos++) + marginOffset;
1363             }
1364             if (ii.position == N - 1) {
1365                 mLastOffset = offset + ii.widthFactor - 1;
1366             }
1367             ii.offset = offset;
1368             offset += ii.widthFactor + marginOffset;
1369         }
1370 
1371         mNeedCalculatePageOffsets = false;
1372     }
1373 
1374     /**
1375      * This is the persistent state that is saved by ViewPager.  Only needed
1376      * if you are creating a sublass of ViewPager that must save its own
1377      * state, in which case it should implement a subclass of this which
1378      * contains that state.
1379      */
1380     public static class SavedState extends AbsSavedState {
1381         int position;
1382         Parcelable adapterState;
1383         ClassLoader loader;
1384 
1385         public SavedState(Parcelable superState) {
1386             super(superState);
1387         }
1388 
1389         @Override
1390         public void writeToParcel(Parcel out, int flags) {
1391             super.writeToParcel(out, flags);
1392             out.writeInt(position);
1393             out.writeParcelable(adapterState, flags);
1394         }
1395 
1396         @Override
1397         public String toString() {
1398             return "FragmentPager.SavedState{"
1399                     + Integer.toHexString(System.identityHashCode(this))
1400                     + " position=" + position + "}";
1401         }
1402 
1403         public static final Parcelable.Creator<SavedState> CREATOR
1404                 = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
1405                     @Override
1406                     public SavedState createFromParcel(Parcel in, ClassLoader loader) {
1407                         return new SavedState(in, loader);
1408                     }
1409                     @Override
1410                     public SavedState[] newArray(int size) {
1411                         return new SavedState[size];
1412                     }
1413                 });
1414 
1415         SavedState(Parcel in, ClassLoader loader) {
1416             super(in, loader);
1417             if (loader == null) {
1418                 loader = getClass().getClassLoader();
1419             }
1420             position = in.readInt();
1421             adapterState = in.readParcelable(loader);
1422             this.loader = loader;
1423         }
1424     }
1425 
1426     @Override
1427     public Parcelable onSaveInstanceState() {
1428         Parcelable superState = super.onSaveInstanceState();
1429         SavedState ss = new SavedState(superState);
1430         ss.position = mCurItem;
1431         if (mAdapter != null) {
1432             ss.adapterState = mAdapter.saveState();
1433         }
1434         return ss;
1435     }
1436 
1437     @Override
1438     public void onRestoreInstanceState(Parcelable state) {
1439         if (!(state instanceof SavedState)) {
1440             super.onRestoreInstanceState(state);
1441             return;
1442         }
1443 
1444         SavedState ss = (SavedState)state;
1445         super.onRestoreInstanceState(ss.getSuperState());
1446 
1447         if (mAdapter != null) {
1448             mAdapter.restoreState(ss.adapterState, ss.loader);
1449             setCurrentItemInternal(ss.position, false, true);
1450         } else {
1451             mRestoredCurItem = ss.position;
1452             mRestoredAdapterState = ss.adapterState;
1453             mRestoredClassLoader = ss.loader;
1454         }
1455     }
1456 
1457     @Override
1458     public void addView(View child, int index, ViewGroup.LayoutParams params) {
1459         if (!checkLayoutParams(params)) {
1460             params = generateLayoutParams(params);
1461         }
1462         final LayoutParams lp = (LayoutParams) params;
1463         // Any views added via inflation should be classed as part of the decor
1464         lp.isDecor |= isDecorView(child);
1465         if (mInLayout) {
1466             if (lp != null && lp.isDecor) {
1467                 throw new IllegalStateException("Cannot add pager decor view during layout");
1468             }
1469             lp.needsMeasure = true;
1470             addViewInLayout(child, index, params);
1471         } else {
1472             super.addView(child, index, params);
1473         }
1474 
1475         if (USE_CACHE) {
1476             if (child.getVisibility() != GONE) {
1477                 child.setDrawingCacheEnabled(mScrollingCacheEnabled);
1478             } else {
1479                 child.setDrawingCacheEnabled(false);
1480             }
1481         }
1482     }
1483 
1484     private static boolean isDecorView(@NonNull View view) {
1485         Class<?> clazz = view.getClass();
1486         return clazz.getAnnotation(DecorView.class) != null;
1487     }
1488 
1489     @Override
1490     public void removeView(View view) {
1491         if (mInLayout) {
1492             removeViewInLayout(view);
1493         } else {
1494             super.removeView(view);
1495         }
1496     }
1497 
1498     ItemInfo infoForChild(View child) {
1499         for (int i=0; i<mItems.size(); i++) {
1500             ItemInfo ii = mItems.get(i);
1501             if (mAdapter.isViewFromObject(child, ii.object)) {
1502                 return ii;
1503             }
1504         }
1505         return null;
1506     }
1507 
1508     ItemInfo infoForAnyChild(View child) {
1509         ViewParent parent;
1510         while ((parent=child.getParent()) != this) {
1511             if (parent == null || !(parent instanceof View)) {
1512                 return null;
1513             }
1514             child = (View)parent;
1515         }
1516         return infoForChild(child);
1517     }
1518 
1519     ItemInfo infoForPosition(int position) {
1520         for (int i = 0; i < mItems.size(); i++) {
1521             ItemInfo ii = mItems.get(i);
1522             if (ii.position == position) {
1523                 return ii;
1524             }
1525         }
1526         return null;
1527     }
1528 
1529     @Override
1530     protected void onAttachedToWindow() {
1531         super.onAttachedToWindow();
1532         mFirstLayout = true;
1533     }
1534 
1535     @Override
1536     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1537         // For simple implementation, our internal size is always 0.
1538         // We depend on the container to specify the layout size of
1539         // our view.  We can't really know what it is since we will be
1540         // adding and removing different arbitrary views and do not
1541         // want the layout to change as this happens.
1542         setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
1543                 getDefaultSize(0, heightMeasureSpec));
1544 
1545         final int measuredWidth = getMeasuredWidth();
1546         final int maxGutterSize = measuredWidth / 10;
1547         mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
1548 
1549         // Children are just made to fill our space.
1550         int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
1551         int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
1552 
1553         /*
1554          * Make sure all children have been properly measured. Decor views first.
1555          * Right now we cheat and make this less complicated by assuming decor
1556          * views won't intersect. We will pin to edges based on gravity.
1557          */
1558         int size = getChildCount();
1559         for (int i = 0; i < size; ++i) {
1560             final View child = getChildAt(i);
1561             if (child.getVisibility() != GONE) {
1562                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1563                 if (lp != null && lp.isDecor) {
1564                     final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1565                     final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1566                     int widthMode = MeasureSpec.AT_MOST;
1567                     int heightMode = MeasureSpec.AT_MOST;
1568                     boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
1569                     boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
1570 
1571                     if (consumeVertical) {
1572                         widthMode = MeasureSpec.EXACTLY;
1573                     } else if (consumeHorizontal) {
1574                         heightMode = MeasureSpec.EXACTLY;
1575                     }
1576 
1577                     int widthSize = childWidthSize;
1578                     int heightSize = childHeightSize;
1579                     if (lp.width != LayoutParams.WRAP_CONTENT) {
1580                         widthMode = MeasureSpec.EXACTLY;
1581                         if (lp.width != LayoutParams.FILL_PARENT) {
1582                             widthSize = lp.width;
1583                         }
1584                     }
1585                     if (lp.height != LayoutParams.WRAP_CONTENT) {
1586                         heightMode = MeasureSpec.EXACTLY;
1587                         if (lp.height != LayoutParams.FILL_PARENT) {
1588                             heightSize = lp.height;
1589                         }
1590                     }
1591                     final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
1592                     final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
1593                     child.measure(widthSpec, heightSpec);
1594 
1595                     if (consumeVertical) {
1596                         childHeightSize -= child.getMeasuredHeight();
1597                     } else if (consumeHorizontal) {
1598                         childWidthSize -= child.getMeasuredWidth();
1599                     }
1600                 }
1601             }
1602         }
1603 
1604         mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
1605         mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
1606 
1607         // Make sure we have created all fragments that we need to have shown.
1608         mInLayout = true;
1609         populate();
1610         mInLayout = false;
1611 
1612         // Page views next.
1613         size = getChildCount();
1614         for (int i = 0; i < size; ++i) {
1615             final View child = getChildAt(i);
1616             if (child.getVisibility() != GONE) {
1617                 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
1618                         + ": " + mChildWidthMeasureSpec);
1619 
1620                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1621                 if (lp == null || !lp.isDecor) {
1622                     final int widthSpec = MeasureSpec.makeMeasureSpec(
1623                             (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
1624                     child.measure(widthSpec, mChildHeightMeasureSpec);
1625                 }
1626             }
1627         }
1628     }
1629 
1630     @Override
1631     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1632         super.onSizeChanged(w, h, oldw, oldh);
1633 
1634         // Make sure scroll position is set correctly.
1635         if (w != oldw) {
1636             recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
1637         }
1638     }
1639 
1640     private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
1641         if (oldWidth > 0 && !mItems.isEmpty()) {
1642             if (!mScroller.isFinished()) {
1643                 mScroller.setFinalX(getCurrentItem() * getClientWidth());
1644             } else {
1645                 final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
1646                 final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
1647                         + oldMargin;
1648                 final int xpos = getScrollX();
1649                 final float pageOffset = (float) xpos / oldWidthWithMargin;
1650                 final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
1651 
1652                 scrollTo(newOffsetPixels, getScrollY());
1653             }
1654         } else {
1655             final ItemInfo ii = infoForPosition(mCurItem);
1656             final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
1657             final int scrollPos = (int) (scrollOffset *
1658                                          (width - getPaddingLeft() - getPaddingRight()));
1659             if (scrollPos != getScrollX()) {
1660                 completeScroll(false);
1661                 scrollTo(scrollPos, getScrollY());
1662             }
1663         }
1664     }
1665 
1666     @Override
1667     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1668         final int count = getChildCount();
1669         int width = r - l;
1670         int height = b - t;
1671         int paddingLeft = getPaddingLeft();
1672         int paddingTop = getPaddingTop();
1673         int paddingRight = getPaddingRight();
1674         int paddingBottom = getPaddingBottom();
1675         final int scrollX = getScrollX();
1676 
1677         int decorCount = 0;
1678 
1679         // First pass - decor views. We need to do this in two passes so that
1680         // we have the proper offsets for non-decor views later.
1681         for (int i = 0; i < count; i++) {
1682             final View child = getChildAt(i);
1683             if (child.getVisibility() != GONE) {
1684                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1685                 int childLeft = 0;
1686                 int childTop = 0;
1687                 if (lp.isDecor) {
1688                     final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1689                     final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
1690                     switch (hgrav) {
1691                         default:
1692                             childLeft = paddingLeft;
1693                             break;
1694                         case Gravity.LEFT:
1695                             childLeft = paddingLeft;
1696                             paddingLeft += child.getMeasuredWidth();
1697                             break;
1698                         case Gravity.CENTER_HORIZONTAL:
1699                             childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
1700                                     paddingLeft);
1701                             break;
1702                         case Gravity.RIGHT:
1703                             childLeft = width - paddingRight - child.getMeasuredWidth();
1704                             paddingRight += child.getMeasuredWidth();
1705                             break;
1706                     }
1707                     switch (vgrav) {
1708                         default:
1709                             childTop = paddingTop;
1710                             break;
1711                         case Gravity.TOP:
1712                             childTop = paddingTop;
1713                             paddingTop += child.getMeasuredHeight();
1714                             break;
1715                         case Gravity.CENTER_VERTICAL:
1716                             childTop = Math.max((height - child.getMeasuredHeight()) / 2,
1717                                     paddingTop);
1718                             break;
1719                         case Gravity.BOTTOM:
1720                             childTop = height - paddingBottom - child.getMeasuredHeight();
1721                             paddingBottom += child.getMeasuredHeight();
1722                             break;
1723                     }
1724                     childLeft += scrollX;
1725                     child.layout(childLeft, childTop,
1726                             childLeft + child.getMeasuredWidth(),
1727                             childTop + child.getMeasuredHeight());
1728                     decorCount++;
1729                 }
1730             }
1731         }
1732 
1733         final int childWidth = width - paddingLeft - paddingRight;
1734         // Page views. Do this once we have the right padding offsets from above.
1735         for (int i = 0; i < count; i++) {
1736             final View child = getChildAt(i);
1737             if (child.getVisibility() != GONE) {
1738                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1739                 ItemInfo ii;
1740                 if (!lp.isDecor && (ii = infoForChild(child)) != null) {
1741                     int loff = (int) (childWidth * ii.offset);
1742                     int childLeft = paddingLeft + loff;
1743                     int childTop = paddingTop;
1744                     if (lp.needsMeasure) {
1745                         // This was added during layout and needs measurement.
1746                         // Do it now that we know what we're working with.
1747                         lp.needsMeasure = false;
1748                         final int widthSpec = MeasureSpec.makeMeasureSpec(
1749                                 (int) (childWidth * lp.widthFactor),
1750                                 MeasureSpec.EXACTLY);
1751                         final int heightSpec = MeasureSpec.makeMeasureSpec(
1752                                 (int) (height - paddingTop - paddingBottom),
1753                                 MeasureSpec.EXACTLY);
1754                         child.measure(widthSpec, heightSpec);
1755                     }
1756                     if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
1757                             + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
1758                             + "x" + child.getMeasuredHeight());
1759                     child.layout(childLeft, childTop,
1760                             childLeft + child.getMeasuredWidth(),
1761                             childTop + child.getMeasuredHeight());
1762                 }
1763             }
1764         }
1765         mTopPageBounds = paddingTop;
1766         mBottomPageBounds = height - paddingBottom;
1767         mDecorChildCount = decorCount;
1768 
1769         if (mFirstLayout) {
1770             scrollToItem(mCurItem, false, 0, false);
1771         }
1772         mFirstLayout = false;
1773     }
1774 
1775     @Override
1776     public void computeScroll() {
1777         mIsScrollStarted = true;
1778         if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
1779             int oldX = getScrollX();
1780             int oldY = getScrollY();
1781             int x = mScroller.getCurrX();
1782             int y = mScroller.getCurrY();
1783 
1784             if (oldX != x || oldY != y) {
1785                 scrollTo(x, y);
1786                 if (!pageScrolled(x)) {
1787                     mScroller.abortAnimation();
1788                     scrollTo(0, y);
1789                 }
1790             }
1791 
1792             // Keep on drawing until the animation has finished.
1793             ViewCompat.postInvalidateOnAnimation(this);
1794             return;
1795         }
1796 
1797         // Done with scroll, clean up state.
1798         completeScroll(true);
1799     }
1800 
1801     private boolean pageScrolled(int xpos) {
1802         if (mItems.size() == 0) {
1803             if (mFirstLayout) {
1804                 // If we haven't been laid out yet, we probably just haven't been populated yet.
1805                 // Let's skip this call since it doesn't make sense in this state
1806                 return false;
1807             }
1808             mCalledSuper = false;
1809             onPageScrolled(0, 0, 0);
1810             if (!mCalledSuper) {
1811                 throw new IllegalStateException(
1812                         "onPageScrolled did not call superclass implementation");
1813             }
1814             return false;
1815         }
1816         final ItemInfo ii = infoForCurrentScrollPosition();
1817         final int width = getClientWidth();
1818         final int widthWithMargin = width + mPageMargin;
1819         final float marginOffset = (float) mPageMargin / width;
1820         final int currentPage = ii.position;
1821         final float pageOffset = (((float) xpos / width) - ii.offset) /
1822                 (ii.widthFactor + marginOffset);
1823         final int offsetPixels = (int) (pageOffset * widthWithMargin);
1824 
1825         mCalledSuper = false;
1826         onPageScrolled(currentPage, pageOffset, offsetPixels);
1827         if (!mCalledSuper) {
1828             throw new IllegalStateException(
1829                     "onPageScrolled did not call superclass implementation");
1830         }
1831         return true;
1832     }
1833 
1834     /**
1835      * This method will be invoked when the current page is scrolled, either as part
1836      * of a programmatically initiated smooth scroll or a user initiated touch scroll.
1837      * If you override this method you must call through to the superclass implementation
1838      * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
1839      * returns.
1840      *
1841      * @param position Position index of the first page currently being displayed.
1842      *                 Page position+1 will be visible if positionOffset is nonzero.
1843      * @param offset Value from [0, 1) indicating the offset from the page at position.
1844      * @param offsetPixels Value in pixels indicating the offset from position.
1845      */
1846     @CallSuper
1847     protected void onPageScrolled(int position, float offset, int offsetPixels) {
1848         // Offset any decor views if needed - keep them on-screen at all times.
1849         if (mDecorChildCount > 0) {
1850             final int scrollX = getScrollX();
1851             int paddingLeft = getPaddingLeft();
1852             int paddingRight = getPaddingRight();
1853             final int width = getWidth();
1854             final int childCount = getChildCount();
1855             for (int i = 0; i < childCount; i++) {
1856                 final View child = getChildAt(i);
1857                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1858                 if (!lp.isDecor) continue;
1859 
1860                 final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1861                 int childLeft = 0;
1862                 switch (hgrav) {
1863                     default:
1864                         childLeft = paddingLeft;
1865                         break;
1866                     case Gravity.LEFT:
1867                         childLeft = paddingLeft;
1868                         paddingLeft += child.getWidth();
1869                         break;
1870                     case Gravity.CENTER_HORIZONTAL:
1871                         childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
1872                                 paddingLeft);
1873                         break;
1874                     case Gravity.RIGHT:
1875                         childLeft = width - paddingRight - child.getMeasuredWidth();
1876                         paddingRight += child.getMeasuredWidth();
1877                         break;
1878                 }
1879                 childLeft += scrollX;
1880 
1881                 final int childOffset = childLeft - child.getLeft();
1882                 if (childOffset != 0) {
1883                     child.offsetLeftAndRight(childOffset);
1884                 }
1885             }
1886         }
1887 
1888         dispatchOnPageScrolled(position, offset, offsetPixels);
1889 
1890         if (mPageTransformer != null) {
1891             final int scrollX = getScrollX();
1892             final int childCount = getChildCount();
1893             for (int i = 0; i < childCount; i++) {
1894                 final View child = getChildAt(i);
1895                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1896 
1897                 if (lp.isDecor) continue;
1898                 final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
1899                 mPageTransformer.transformPage(child, transformPos);
1900             }
1901         }
1902 
1903         mCalledSuper = true;
1904     }
1905 
1906     private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) {
1907         if (mOnPageChangeListener != null) {
1908             mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1909         }
1910         if (mOnPageChangeListeners != null) {
1911             for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
1912                 OnPageChangeListener listener = mOnPageChangeListeners.get(i);
1913                 if (listener != null) {
1914                     listener.onPageScrolled(position, offset, offsetPixels);
1915                 }
1916             }
1917         }
1918         if (mInternalPageChangeListener != null) {
1919             mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
1920         }
1921     }
1922 
1923     private void dispatchOnPageSelected(int position) {
1924         if (mOnPageChangeListener != null) {
1925             mOnPageChangeListener.onPageSelected(position);
1926         }
1927         if (mOnPageChangeListeners != null) {
1928             for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
1929                 OnPageChangeListener listener = mOnPageChangeListeners.get(i);
1930                 if (listener != null) {
1931                     listener.onPageSelected(position);
1932                 }
1933             }
1934         }
1935         if (mInternalPageChangeListener != null) {
1936             mInternalPageChangeListener.onPageSelected(position);
1937         }
1938     }
1939 
1940     private void dispatchOnScrollStateChanged(int state) {
1941         if (mOnPageChangeListener != null) {
1942             mOnPageChangeListener.onPageScrollStateChanged(state);
1943         }
1944         if (mOnPageChangeListeners != null) {
1945             for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
1946                 OnPageChangeListener listener = mOnPageChangeListeners.get(i);
1947                 if (listener != null) {
1948                     listener.onPageScrollStateChanged(state);
1949                 }
1950             }
1951         }
1952         if (mInternalPageChangeListener != null) {
1953             mInternalPageChangeListener.onPageScrollStateChanged(state);
1954         }
1955     }
1956 
1957     private void completeScroll(boolean postEvents) {
1958         boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
1959         if (needPopulate) {
1960             // Done with scroll, no longer want to cache view drawing.
1961             setScrollingCacheEnabled(false);
1962             boolean wasScrolling = !mScroller.isFinished();
1963             if (wasScrolling) {
1964                 mScroller.abortAnimation();
1965                 int oldX = getScrollX();
1966                 int oldY = getScrollY();
1967                 int x = mScroller.getCurrX();
1968                 int y = mScroller.getCurrY();
1969                 if (oldX != x || oldY != y) {
1970                     scrollTo(x, y);
1971                     if (x != oldX) {
1972                         pageScrolled(x);
1973                     }
1974                 }
1975             }
1976         }
1977         mPopulatePending = false;
1978         for (int i=0; i<mItems.size(); i++) {
1979             ItemInfo ii = mItems.get(i);
1980             if (ii.scrolling) {
1981                 needPopulate = true;
1982                 ii.scrolling = false;
1983             }
1984         }
1985         if (needPopulate) {
1986             if (postEvents) {
1987                 ViewCompat.postOnAnimation(this, mEndScrollRunnable);
1988             } else {
1989                 mEndScrollRunnable.run();
1990             }
1991         }
1992     }
1993 
1994     private boolean isGutterDrag(float x, float dx) {
1995         return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
1996     }
1997 
1998     private void enableLayers(boolean enable) {
1999         final int childCount = getChildCount();
2000         for (int i = 0; i < childCount; i++) {
2001             final int layerType = enable ?
2002                     ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
2003             ViewCompat.setLayerType(getChildAt(i), layerType, null);
2004         }
2005     }
2006 
2007     @Override
2008     public boolean onInterceptTouchEvent(MotionEvent ev) {
2009         /*
2010          * This method JUST determines whether we want to intercept the motion.
2011          * If we return true, onMotionEvent will be called and we do the actual
2012          * scrolling there.
2013          */
2014 
2015         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
2016 
2017         // Always take care of the touch gesture being complete.
2018         if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
2019             // Release the drag.
2020             if (DEBUG) Log.v(TAG, "Intercept done!");
2021             resetTouch();
2022             return false;
2023         }
2024 
2025         // Nothing more to do here if we have decided whether or not we
2026         // are dragging.
2027         if (action != MotionEvent.ACTION_DOWN) {
2028             if (mIsBeingDragged) {
2029                 if (DEBUG) Log.v(TAG, "Intercept returning true!");
2030                 return true;
2031             }
2032             if (mIsUnableToDrag) {
2033                 if (DEBUG) Log.v(TAG, "Intercept returning false!");
2034                 return false;
2035             }
2036         }
2037 
2038         switch (action) {
2039             case MotionEvent.ACTION_MOVE: {
2040                 /*
2041                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
2042                  * whether the user has moved far enough from his original down touch.
2043                  */
2044 
2045                 /*
2046                 * Locally do absolute value. mLastMotionY is set to the y value
2047                 * of the down event.
2048                 */
2049                 final int activePointerId = mActivePointerId;
2050                 if (activePointerId == INVALID_POINTER) {
2051                     // If we don't have a valid id, the touch down wasn't on content.
2052                     break;
2053                 }
2054 
2055                 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
2056                 final float x = MotionEventCompat.getX(ev, pointerIndex);
2057                 final float dx = x - mLastMotionX;
2058                 final float xDiff = Math.abs(dx);
2059                 final float y = MotionEventCompat.getY(ev, pointerIndex);
2060                 final float yDiff = Math.abs(y - mInitialMotionY);
2061                 if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
2062 
2063                 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
2064                         canScroll(this, false, (int) dx, (int) x, (int) y)) {
2065                     // Nested view has scrollable area under this point. Let it be handled there.
2066                     mLastMotionX = x;
2067                     mLastMotionY = y;
2068                     mIsUnableToDrag = true;
2069                     return false;
2070                 }
2071                 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
2072                     if (DEBUG) Log.v(TAG, "Starting drag!");
2073                     mIsBeingDragged = true;
2074                     requestParentDisallowInterceptTouchEvent(true);
2075                     setScrollState(SCROLL_STATE_DRAGGING);
2076                     mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
2077                             mInitialMotionX - mTouchSlop;
2078                     mLastMotionY = y;
2079                     setScrollingCacheEnabled(true);
2080                 } else if (yDiff > mTouchSlop) {
2081                     // The finger has moved enough in the vertical
2082                     // direction to be counted as a drag...  abort
2083                     // any attempt to drag horizontally, to work correctly
2084                     // with children that have scrolling containers.
2085                     if (DEBUG) Log.v(TAG, "Starting unable to drag!");
2086                     mIsUnableToDrag = true;
2087                 }
2088                 if (mIsBeingDragged) {
2089                     // Scroll to follow the motion event
2090                     if (performDrag(x)) {
2091                         ViewCompat.postInvalidateOnAnimation(this);
2092                     }
2093                 }
2094                 break;
2095             }
2096 
2097             case MotionEvent.ACTION_DOWN: {
2098                 /*
2099                  * Remember location of down touch.
2100                  * ACTION_DOWN always refers to pointer index 0.
2101                  */
2102                 mLastMotionX = mInitialMotionX = ev.getX();
2103                 mLastMotionY = mInitialMotionY = ev.getY();
2104                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
2105                 mIsUnableToDrag = false;
2106 
2107                 mIsScrollStarted = true;
2108                 mScroller.computeScrollOffset();
2109                 if (mScrollState == SCROLL_STATE_SETTLING &&
2110                         Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
2111                     // Let the user 'catch' the pager as it animates.
2112                     mScroller.abortAnimation();
2113                     mPopulatePending = false;
2114                     populate();
2115                     mIsBeingDragged = true;
2116                     requestParentDisallowInterceptTouchEvent(true);
2117                     setScrollState(SCROLL_STATE_DRAGGING);
2118                 } else {
2119                     completeScroll(false);
2120                     mIsBeingDragged = false;
2121                 }
2122 
2123                 if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
2124                         + " mIsBeingDragged=" + mIsBeingDragged
2125                         + "mIsUnableToDrag=" + mIsUnableToDrag);
2126                 break;
2127             }
2128 
2129             case MotionEventCompat.ACTION_POINTER_UP:
2130                 onSecondaryPointerUp(ev);
2131                 break;
2132         }
2133 
2134         if (mVelocityTracker == null) {
2135             mVelocityTracker = VelocityTracker.obtain();
2136         }
2137         mVelocityTracker.addMovement(ev);
2138 
2139         /*
2140          * The only time we want to intercept motion events is if we are in the
2141          * drag mode.
2142          */
2143         return mIsBeingDragged;
2144     }
2145 
2146     @Override
2147     public boolean onTouchEvent(MotionEvent ev) {
2148         if (mFakeDragging) {
2149             // A fake drag is in progress already, ignore this real one
2150             // but still eat the touch events.
2151             // (It is likely that the user is multi-touching the screen.)
2152             return true;
2153         }
2154 
2155         if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
2156             // Don't handle edge touches immediately -- they may actually belong to one of our
2157             // descendants.
2158             return false;
2159         }
2160 
2161         if (mAdapter == null || mAdapter.getCount() == 0) {
2162             // Nothing to present or scroll; nothing to touch.
2163             return false;
2164         }
2165 
2166         if (mVelocityTracker == null) {
2167             mVelocityTracker = VelocityTracker.obtain();
2168         }
2169         mVelocityTracker.addMovement(ev);
2170 
2171         final int action = ev.getAction();
2172         boolean needsInvalidate = false;
2173 
2174         switch (action & MotionEventCompat.ACTION_MASK) {
2175             case MotionEvent.ACTION_DOWN: {
2176                 mScroller.abortAnimation();
2177                 mPopulatePending = false;
2178                 populate();
2179 
2180                 // Remember where the motion event started
2181                 mLastMotionX = mInitialMotionX = ev.getX();
2182                 mLastMotionY = mInitialMotionY = ev.getY();
2183                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
2184                 break;
2185             }
2186             case MotionEvent.ACTION_MOVE:
2187                 if (!mIsBeingDragged) {
2188                     final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
2189                     if (pointerIndex == -1) {
2190                         // A child has consumed some touch events and put us into an inconsistent state.
2191                         needsInvalidate = resetTouch();
2192                         break;
2193                     }
2194                     final float x = MotionEventCompat.getX(ev, pointerIndex);
2195                     final float xDiff = Math.abs(x - mLastMotionX);
2196                     final float y = MotionEventCompat.getY(ev, pointerIndex);
2197                     final float yDiff = Math.abs(y - mLastMotionY);
2198                     if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
2199                     if (xDiff > mTouchSlop && xDiff > yDiff) {
2200                         if (DEBUG) Log.v(TAG, "Starting drag!");
2201                         mIsBeingDragged = true;
2202                         requestParentDisallowInterceptTouchEvent(true);
2203                         mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
2204                                 mInitialMotionX - mTouchSlop;
2205                         mLastMotionY = y;
2206                         setScrollState(SCROLL_STATE_DRAGGING);
2207                         setScrollingCacheEnabled(true);
2208 
2209                         // Disallow Parent Intercept, just in case
2210                         ViewParent parent = getParent();
2211                         if (parent != null) {
2212                             parent.requestDisallowInterceptTouchEvent(true);
2213                         }
2214                     }
2215                 }
2216                 // Not else! Note that mIsBeingDragged can be set above.
2217                 if (mIsBeingDragged) {
2218                     // Scroll to follow the motion event
2219                     final int activePointerIndex = MotionEventCompat.findPointerIndex(
2220                             ev, mActivePointerId);
2221                     final float x = MotionEventCompat.getX(ev, activePointerIndex);
2222                     needsInvalidate |= performDrag(x);
2223                 }
2224                 break;
2225             case MotionEvent.ACTION_UP:
2226                 if (mIsBeingDragged) {
2227                     final VelocityTracker velocityTracker = mVelocityTracker;
2228                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2229                     int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
2230                             velocityTracker, mActivePointerId);
2231                     mPopulatePending = true;
2232                     final int width = getClientWidth();
2233                     final int scrollX = getScrollX();
2234                     final ItemInfo ii = infoForCurrentScrollPosition();
2235                     final float marginOffset = (float) mPageMargin / width;
2236                     final int currentPage = ii.position;
2237                     final float pageOffset = (((float) scrollX / width) - ii.offset)
2238                             / (ii.widthFactor + marginOffset);
2239                     final int activePointerIndex =
2240                             MotionEventCompat.findPointerIndex(ev, mActivePointerId);
2241                     final float x = MotionEventCompat.getX(ev, activePointerIndex);
2242                     final int totalDelta = (int) (x - mInitialMotionX);
2243                     int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2244                             totalDelta);
2245                     setCurrentItemInternal(nextPage, true, true, initialVelocity);
2246 
2247                     needsInvalidate = resetTouch();
2248                 }
2249                 break;
2250             case MotionEvent.ACTION_CANCEL:
2251                 if (mIsBeingDragged) {
2252                     scrollToItem(mCurItem, true, 0, false);
2253                     needsInvalidate = resetTouch();
2254                 }
2255                 break;
2256             case MotionEventCompat.ACTION_POINTER_DOWN: {
2257                 final int index = MotionEventCompat.getActionIndex(ev);
2258                 final float x = MotionEventCompat.getX(ev, index);
2259                 mLastMotionX = x;
2260                 mActivePointerId = MotionEventCompat.getPointerId(ev, index);
2261                 break;
2262             }
2263             case MotionEventCompat.ACTION_POINTER_UP:
2264                 onSecondaryPointerUp(ev);
2265                 mLastMotionX = MotionEventCompat.getX(ev,
2266                         MotionEventCompat.findPointerIndex(ev, mActivePointerId));
2267                 break;
2268         }
2269         if (needsInvalidate) {
2270             ViewCompat.postInvalidateOnAnimation(this);
2271         }
2272         return true;
2273     }
2274 
2275     private boolean resetTouch() {
2276         boolean needsInvalidate;
2277         mActivePointerId = INVALID_POINTER;
2278         endDrag();
2279         needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
2280         return needsInvalidate;
2281     }
2282 
2283     private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
2284         final ViewParent parent = getParent();
2285         if (parent != null) {
2286             parent.requestDisallowInterceptTouchEvent(disallowIntercept);
2287         }
2288     }
2289 
2290     private boolean performDrag(float x) {
2291         boolean needsInvalidate = false;
2292 
2293         final float deltaX = mLastMotionX - x;
2294         mLastMotionX = x;
2295 
2296         float oldScrollX = getScrollX();
2297         float scrollX = oldScrollX + deltaX;
2298         final int width = getClientWidth();
2299 
2300         float leftBound = width * mFirstOffset;
2301         float rightBound = width * mLastOffset;
2302         boolean leftAbsolute = true;
2303         boolean rightAbsolute = true;
2304 
2305         final ItemInfo firstItem = mItems.get(0);
2306         final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2307         if (firstItem.position != 0) {
2308             leftAbsolute = false;
2309             leftBound = firstItem.offset * width;
2310         }
2311         if (lastItem.position != mAdapter.getCount() - 1) {
2312             rightAbsolute = false;
2313             rightBound = lastItem.offset * width;
2314         }
2315 
2316         if (scrollX < leftBound) {
2317             if (leftAbsolute) {
2318                 float over = leftBound - scrollX;
2319                 needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
2320             }
2321             scrollX = leftBound;
2322         } else if (scrollX > rightBound) {
2323             if (rightAbsolute) {
2324                 float over = scrollX - rightBound;
2325                 needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
2326             }
2327             scrollX = rightBound;
2328         }
2329         // Don't lose the rounded component
2330         mLastMotionX += scrollX - (int) scrollX;
2331         scrollTo((int) scrollX, getScrollY());
2332         pageScrolled((int) scrollX);
2333 
2334         return needsInvalidate;
2335     }
2336 
2337     /**
2338      * @return Info about the page at the current scroll position.
2339      *         This can be synthetic for a missing middle page; the 'object' field can be null.
2340      */
2341     private ItemInfo infoForCurrentScrollPosition() {
2342         final int width = getClientWidth();
2343         final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;
2344         final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
2345         int lastPos = -1;
2346         float lastOffset = 0.f;
2347         float lastWidth = 0.f;
2348         boolean first = true;
2349 
2350         ItemInfo lastItem = null;
2351         for (int i = 0; i < mItems.size(); i++) {
2352             ItemInfo ii = mItems.get(i);
2353             float offset;
2354             if (!first && ii.position != lastPos + 1) {
2355                 // Create a synthetic item for a missing page.
2356                 ii = mTempItem;
2357                 ii.offset = lastOffset + lastWidth + marginOffset;
2358                 ii.position = lastPos + 1;
2359                 ii.widthFactor = mAdapter.getPageWidth(ii.position);
2360                 i--;
2361             }
2362             offset = ii.offset;
2363 
2364             final float leftBound = offset;
2365             final float rightBound = offset + ii.widthFactor + marginOffset;
2366             if (first || scrollOffset >= leftBound) {
2367                 if (scrollOffset < rightBound || i == mItems.size() - 1) {
2368                     return ii;
2369                 }
2370             } else {
2371                 return lastItem;
2372             }
2373             first = false;
2374             lastPos = ii.position;
2375             lastOffset = offset;
2376             lastWidth = ii.widthFactor;
2377             lastItem = ii;
2378         }
2379 
2380         return lastItem;
2381     }
2382 
2383     private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
2384         int targetPage;
2385         if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
2386             targetPage = velocity > 0 ? currentPage : currentPage + 1;
2387         } else {
2388             final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
2389             targetPage = currentPage + (int) (pageOffset + truncator);
2390         }
2391 
2392         if (mItems.size() > 0) {
2393             final ItemInfo firstItem = mItems.get(0);
2394             final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2395 
2396             // Only let the user target pages we have items for
2397             targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
2398         }
2399 
2400         return targetPage;
2401     }
2402 
2403     @Override
2404     public void draw(Canvas canvas) {
2405         super.draw(canvas);
2406         boolean needsInvalidate = false;
2407 
2408         final int overScrollMode = ViewCompat.getOverScrollMode(this);
2409         if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
2410                 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
2411                         mAdapter != null && mAdapter.getCount() > 1)) {
2412             if (!mLeftEdge.isFinished()) {
2413                 final int restoreCount = canvas.save();
2414                 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
2415                 final int width = getWidth();
2416 
2417                 canvas.rotate(270);
2418                 canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
2419                 mLeftEdge.setSize(height, width);
2420                 needsInvalidate |= mLeftEdge.draw(canvas);
2421                 canvas.restoreToCount(restoreCount);
2422             }
2423             if (!mRightEdge.isFinished()) {
2424                 final int restoreCount = canvas.save();
2425                 final int width = getWidth();
2426                 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
2427 
2428                 canvas.rotate(90);
2429                 canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
2430                 mRightEdge.setSize(height, width);
2431                 needsInvalidate |= mRightEdge.draw(canvas);
2432                 canvas.restoreToCount(restoreCount);
2433             }
2434         } else {
2435             mLeftEdge.finish();
2436             mRightEdge.finish();
2437         }
2438 
2439         if (needsInvalidate) {
2440             // Keep animating
2441             ViewCompat.postInvalidateOnAnimation(this);
2442         }
2443     }
2444 
2445     @Override
2446     protected void onDraw(Canvas canvas) {
2447         super.onDraw(canvas);
2448 
2449         // Draw the margin drawable between pages if needed.
2450         if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
2451             final int scrollX = getScrollX();
2452             final int width = getWidth();
2453 
2454             final float marginOffset = (float) mPageMargin / width;
2455             int itemIndex = 0;
2456             ItemInfo ii = mItems.get(0);
2457             float offset = ii.offset;
2458             final int itemCount = mItems.size();
2459             final int firstPos = ii.position;
2460             final int lastPos = mItems.get(itemCount - 1).position;
2461             for (int pos = firstPos; pos < lastPos; pos++) {
2462                 while (pos > ii.position && itemIndex < itemCount) {
2463                     ii = mItems.get(++itemIndex);
2464                 }
2465 
2466                 float drawAt;
2467                 if (pos == ii.position) {
2468                     drawAt = (ii.offset + ii.widthFactor) * width;
2469                     offset = ii.offset + ii.widthFactor + marginOffset;
2470                 } else {
2471                     float widthFactor = mAdapter.getPageWidth(pos);
2472                     drawAt = (offset + widthFactor) * width;
2473                     offset += widthFactor + marginOffset;
2474                 }
2475 
2476                 if (drawAt + mPageMargin > scrollX) {
2477                     mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds,
2478                             Math.round(drawAt + mPageMargin), mBottomPageBounds);
2479                     mMarginDrawable.draw(canvas);
2480                 }
2481 
2482                 if (drawAt > scrollX + width) {
2483                     break; // No more visible, no sense in continuing
2484                 }
2485             }
2486         }
2487     }
2488 
2489     /**
2490      * Start a fake drag of the pager.
2491      *
2492      * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
2493      * with the touch scrolling of another view, while still letting the ViewPager
2494      * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
2495      * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
2496      * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
2497      *
2498      * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
2499      * is already in progress, this method will return false.
2500      *
2501      * @return true if the fake drag began successfully, false if it could not be started.
2502      *
2503      * @see #fakeDragBy(float)
2504      * @see #endFakeDrag()
2505      */
2506     public boolean beginFakeDrag() {
2507         if (mIsBeingDragged) {
2508             return false;
2509         }
2510         mFakeDragging = true;
2511         setScrollState(SCROLL_STATE_DRAGGING);
2512         mInitialMotionX = mLastMotionX = 0;
2513         if (mVelocityTracker == null) {
2514             mVelocityTracker = VelocityTracker.obtain();
2515         } else {
2516             mVelocityTracker.clear();
2517         }
2518         final long time = SystemClock.uptimeMillis();
2519         final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
2520         mVelocityTracker.addMovement(ev);
2521         ev.recycle();
2522         mFakeDragBeginTime = time;
2523         return true;
2524     }
2525 
2526     /**
2527      * End a fake drag of the pager.
2528      *
2529      * @see #beginFakeDrag()
2530      * @see #fakeDragBy(float)
2531      */
2532     public void endFakeDrag() {
2533         if (!mFakeDragging) {
2534             throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2535         }
2536 
2537         if (mAdapter != null) {
2538             final VelocityTracker velocityTracker = mVelocityTracker;
2539             velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2540             int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
2541                     velocityTracker, mActivePointerId);
2542             mPopulatePending = true;
2543             final int width = getClientWidth();
2544             final int scrollX = getScrollX();
2545             final ItemInfo ii = infoForCurrentScrollPosition();
2546             final int currentPage = ii.position;
2547             final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
2548             final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
2549             int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
2550                     totalDelta);
2551             setCurrentItemInternal(nextPage, true, true, initialVelocity);
2552         }
2553         endDrag();
2554 
2555         mFakeDragging = false;
2556     }
2557 
2558     /**
2559      * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
2560      *
2561      * @param xOffset Offset in pixels to drag by.
2562      * @see #beginFakeDrag()
2563      * @see #endFakeDrag()
2564      */
2565     public void fakeDragBy(float xOffset) {
2566         if (!mFakeDragging) {
2567             throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
2568         }
2569 
2570         if (mAdapter == null) {
2571             return;
2572         }
2573 
2574         mLastMotionX += xOffset;
2575 
2576         float oldScrollX = getScrollX();
2577         float scrollX = oldScrollX - xOffset;
2578         final int width = getClientWidth();
2579 
2580         float leftBound = width * mFirstOffset;
2581         float rightBound = width * mLastOffset;
2582 
2583         final ItemInfo firstItem = mItems.get(0);
2584         final ItemInfo lastItem = mItems.get(mItems.size() - 1);
2585         if (firstItem.position != 0) {
2586             leftBound = firstItem.offset * width;
2587         }
2588         if (lastItem.position != mAdapter.getCount() - 1) {
2589             rightBound = lastItem.offset * width;
2590         }
2591 
2592         if (scrollX < leftBound) {
2593             scrollX = leftBound;
2594         } else if (scrollX > rightBound) {
2595             scrollX = rightBound;
2596         }
2597         // Don't lose the rounded component
2598         mLastMotionX += scrollX - (int) scrollX;
2599         scrollTo((int) scrollX, getScrollY());
2600         pageScrolled((int) scrollX);
2601 
2602         // Synthesize an event for the VelocityTracker.
2603         final long time = SystemClock.uptimeMillis();
2604         final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
2605                 mLastMotionX, 0, 0);
2606         mVelocityTracker.addMovement(ev);
2607         ev.recycle();
2608     }
2609 
2610     /**
2611      * Returns true if a fake drag is in progress.
2612      *
2613      * @return true if currently in a fake drag, false otherwise.
2614      *
2615      * @see #beginFakeDrag()
2616      * @see #fakeDragBy(float)
2617      * @see #endFakeDrag()
2618      */
2619     public boolean isFakeDragging() {
2620         return mFakeDragging;
2621     }
2622 
2623     private void onSecondaryPointerUp(MotionEvent ev) {
2624         final int pointerIndex = MotionEventCompat.getActionIndex(ev);
2625         final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
2626         if (pointerId == mActivePointerId) {
2627             // This was our active pointer going up. Choose a new
2628             // active pointer and adjust accordingly.
2629             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2630             mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
2631             mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
2632             if (mVelocityTracker != null) {
2633                 mVelocityTracker.clear();
2634             }
2635         }
2636     }
2637 
2638     private void endDrag() {
2639         mIsBeingDragged = false;
2640         mIsUnableToDrag = false;
2641 
2642         if (mVelocityTracker != null) {
2643             mVelocityTracker.recycle();
2644             mVelocityTracker = null;
2645         }
2646     }
2647 
2648     private void setScrollingCacheEnabled(boolean enabled) {
2649         if (mScrollingCacheEnabled != enabled) {
2650             mScrollingCacheEnabled = enabled;
2651             if (USE_CACHE) {
2652                 final int size = getChildCount();
2653                 for (int i = 0; i < size; ++i) {
2654                     final View child = getChildAt(i);
2655                     if (child.getVisibility() != GONE) {
2656                         child.setDrawingCacheEnabled(enabled);
2657                     }
2658                 }
2659             }
2660         }
2661     }
2662 
2663     public boolean canScrollHorizontally(int direction) {
2664         if (mAdapter == null) {
2665             return false;
2666         }
2667 
2668         final int width = getClientWidth();
2669         final int scrollX = getScrollX();
2670         if (direction < 0) {
2671             return (scrollX > (int) (width * mFirstOffset));
2672         } else if (direction > 0) {
2673             return (scrollX < (int) (width * mLastOffset));
2674         } else {
2675             return false;
2676         }
2677     }
2678 
2679     /**
2680      * Tests scrollability within child views of v given a delta of dx.
2681      *
2682      * @param v View to test for horizontal scrollability
2683      * @param checkV Whether the view v passed should itself be checked for scrollability (true),
2684      *               or just its children (false).
2685      * @param dx Delta scrolled in pixels
2686      * @param x X coordinate of the active touch point
2687      * @param y Y coordinate of the active touch point
2688      * @return true if child views of v can be scrolled by delta of dx.
2689      */
2690     protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
2691         if (v instanceof ViewGroup) {
2692             final ViewGroup group = (ViewGroup) v;
2693             final int scrollX = v.getScrollX();
2694             final int scrollY = v.getScrollY();
2695             final int count = group.getChildCount();
2696             // Count backwards - let topmost views consume scroll distance first.
2697             for (int i = count - 1; i >= 0; i--) {
2698                 // TODO: Add versioned support here for transformed views.
2699                 // This will not work for transformed views in Honeycomb+
2700                 final View child = group.getChildAt(i);
2701                 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
2702                         y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
2703                         canScroll(child, true, dx, x + scrollX - child.getLeft(),
2704                                 y + scrollY - child.getTop())) {
2705                     return true;
2706                 }
2707             }
2708         }
2709 
2710         return checkV && ViewCompat.canScrollHorizontally(v, -dx);
2711     }
2712 
2713     @Override
2714     public boolean dispatchKeyEvent(KeyEvent event) {
2715         // Let the focused view and/or our descendants get the key first
2716         return super.dispatchKeyEvent(event) || executeKeyEvent(event);
2717     }
2718 
2719     /**
2720      * You can call this function yourself to have the scroll view perform
2721      * scrolling from a key event, just as if the event had been dispatched to
2722      * it by the view hierarchy.
2723      *
2724      * @param event The key event to execute.
2725      * @return Return true if the event was handled, else false.
2726      */
2727     public boolean executeKeyEvent(KeyEvent event) {
2728         boolean handled = false;
2729         if (event.getAction() == KeyEvent.ACTION_DOWN) {
2730             switch (event.getKeyCode()) {
2731                 case KeyEvent.KEYCODE_DPAD_LEFT:
2732                     handled = arrowScroll(FOCUS_LEFT);
2733                     break;
2734                 case KeyEvent.KEYCODE_DPAD_RIGHT:
2735                     handled = arrowScroll(FOCUS_RIGHT);
2736                     break;
2737                 case KeyEvent.KEYCODE_TAB:
2738                     if (Build.VERSION.SDK_INT >= 11) {
2739                         // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
2740                         // before Android 3.0. Ignore the tab key on those devices.
2741                         if (KeyEventCompat.hasNoModifiers(event)) {
2742                             handled = arrowScroll(FOCUS_FORWARD);
2743                         } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2744                             handled = arrowScroll(FOCUS_BACKWARD);
2745                         }
2746                     }
2747                     break;
2748             }
2749         }
2750         return handled;
2751     }
2752 
2753     public boolean arrowScroll(int direction) {
2754         View currentFocused = findFocus();
2755         if (currentFocused == this) {
2756             currentFocused = null;
2757         } else if (currentFocused != null) {
2758             boolean isChild = false;
2759             for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2760                     parent = parent.getParent()) {
2761                 if (parent == this) {
2762                     isChild = true;
2763                     break;
2764                 }
2765             }
2766             if (!isChild) {
2767                 // This would cause the focus search down below to fail in fun ways.
2768                 final StringBuilder sb = new StringBuilder();
2769                 sb.append(currentFocused.getClass().getSimpleName());
2770                 for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
2771                         parent = parent.getParent()) {
2772                     sb.append(" => ").append(parent.getClass().getSimpleName());
2773                 }
2774                 Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
2775                         "current focused view " + sb.toString());
2776                 currentFocused = null;
2777             }
2778         }
2779 
2780         boolean handled = false;
2781 
2782         View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
2783                 direction);
2784         if (nextFocused != null && nextFocused != currentFocused) {
2785             if (direction == View.FOCUS_LEFT) {
2786                 // If there is nothing to the left, or this is causing us to
2787                 // jump to the right, then what we really want to do is page left.
2788                 final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2789                 final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2790                 if (currentFocused != null && nextLeft >= currLeft) {
2791                     handled = pageLeft();
2792                 } else {
2793                     handled = nextFocused.requestFocus();
2794                 }
2795             } else if (direction == View.FOCUS_RIGHT) {
2796                 // If there is nothing to the right, or this is causing us to
2797                 // jump to the left, then what we really want to do is page right.
2798                 final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
2799                 final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
2800                 if (currentFocused != null && nextLeft <= currLeft) {
2801                     handled = pageRight();
2802                 } else {
2803                     handled = nextFocused.requestFocus();
2804                 }
2805             }
2806         } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
2807             // Trying to move left and nothing there; try to page.
2808             handled = pageLeft();
2809         } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
2810             // Trying to move right and nothing there; try to page.
2811             handled = pageRight();
2812         }
2813         if (handled) {
2814             playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2815         }
2816         return handled;
2817     }
2818 
2819     private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
2820         if (outRect == null) {
2821             outRect = new Rect();
2822         }
2823         if (child == null) {
2824             outRect.set(0, 0, 0, 0);
2825             return outRect;
2826         }
2827         outRect.left = child.getLeft();
2828         outRect.right = child.getRight();
2829         outRect.top = child.getTop();
2830         outRect.bottom = child.getBottom();
2831 
2832         ViewParent parent = child.getParent();
2833         while (parent instanceof ViewGroup && parent != this) {
2834             final ViewGroup group = (ViewGroup) parent;
2835             outRect.left += group.getLeft();
2836             outRect.right += group.getRight();
2837             outRect.top += group.getTop();
2838             outRect.bottom += group.getBottom();
2839 
2840             parent = group.getParent();
2841         }
2842         return outRect;
2843     }
2844 
2845     boolean pageLeft() {
2846         if (mCurItem > 0) {
2847             setCurrentItem(mCurItem-1, true);
2848             return true;
2849         }
2850         return false;
2851     }
2852 
2853     boolean pageRight() {
2854         if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
2855             setCurrentItem(mCurItem+1, true);
2856             return true;
2857         }
2858         return false;
2859     }
2860 
2861     /**
2862      * We only want the current page that is being shown to be focusable.
2863      */
2864     @Override
2865     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
2866         final int focusableCount = views.size();
2867 
2868         final int descendantFocusability = getDescendantFocusability();
2869 
2870         if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
2871             for (int i = 0; i < getChildCount(); i++) {
2872                 final View child = getChildAt(i);
2873                 if (child.getVisibility() == VISIBLE) {
2874                     ItemInfo ii = infoForChild(child);
2875                     if (ii != null && ii.position == mCurItem) {
2876                         child.addFocusables(views, direction, focusableMode);
2877                     }
2878                 }
2879             }
2880         }
2881 
2882         // we add ourselves (if focusable) in all cases except for when we are
2883         // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
2884         // to avoid the focus search finding layouts when a more precise search
2885         // among the focusable children would be more interesting.
2886         if (
2887             descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
2888                 // No focusable descendants
2889                 (focusableCount == views.size())) {
2890             // Note that we can't call the superclass here, because it will
2891             // add all views in.  So we need to do the same thing View does.
2892             if (!isFocusable()) {
2893                 return;
2894             }
2895             if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
2896                     isInTouchMode() && !isFocusableInTouchMode()) {
2897                 return;
2898             }
2899             if (views != null) {
2900                 views.add(this);
2901             }
2902         }
2903     }
2904 
2905     /**
2906      * We only want the current page that is being shown to be touchable.
2907      */
2908     @Override
2909     public void addTouchables(ArrayList<View> views) {
2910         // Note that we don't call super.addTouchables(), which means that
2911         // we don't call View.addTouchables().  This is okay because a ViewPager
2912         // is itself not touchable.
2913         for (int i = 0; i < getChildCount(); i++) {
2914             final View child = getChildAt(i);
2915             if (child.getVisibility() == VISIBLE) {
2916                 ItemInfo ii = infoForChild(child);
2917                 if (ii != null && ii.position == mCurItem) {
2918                     child.addTouchables(views);
2919                 }
2920             }
2921         }
2922     }
2923 
2924     /**
2925      * We only want the current page that is being shown to be focusable.
2926      */
2927     @Override
2928     protected boolean onRequestFocusInDescendants(int direction,
2929             Rect previouslyFocusedRect) {
2930         int index;
2931         int increment;
2932         int end;
2933         int count = getChildCount();
2934         if ((direction & FOCUS_FORWARD) != 0) {
2935             index = 0;
2936             increment = 1;
2937             end = count;
2938         } else {
2939             index = count - 1;
2940             increment = -1;
2941             end = -1;
2942         }
2943         for (int i = index; i != end; i += increment) {
2944             View child = getChildAt(i);
2945             if (child.getVisibility() == VISIBLE) {
2946                 ItemInfo ii = infoForChild(child);
2947                 if (ii != null && ii.position == mCurItem) {
2948                     if (child.requestFocus(direction, previouslyFocusedRect)) {
2949                         return true;
2950                     }
2951                 }
2952             }
2953         }
2954         return false;
2955     }
2956 
2957     @Override
2958     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
2959         // Dispatch scroll events from this ViewPager.
2960         if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {
2961             return super.dispatchPopulateAccessibilityEvent(event);
2962         }
2963 
2964         // Dispatch all other accessibility events from the current page.
2965         final int childCount = getChildCount();
2966         for (int i = 0; i < childCount; i++) {
2967             final View child = getChildAt(i);
2968             if (child.getVisibility() == VISIBLE) {
2969                 final ItemInfo ii = infoForChild(child);
2970                 if (ii != null && ii.position == mCurItem &&
2971                         child.dispatchPopulateAccessibilityEvent(event)) {
2972                     return true;
2973                 }
2974             }
2975         }
2976 
2977         return false;
2978     }
2979 
2980     @Override
2981     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
2982         return new LayoutParams();
2983     }
2984 
2985     @Override
2986     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
2987         return generateDefaultLayoutParams();
2988     }
2989 
2990     @Override
2991     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
2992         return p instanceof LayoutParams && super.checkLayoutParams(p);
2993     }
2994 
2995     @Override
2996     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
2997         return new LayoutParams(getContext(), attrs);
2998     }
2999 
3000     class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
3001 
3002         @Override
3003         public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
3004             super.onInitializeAccessibilityEvent(host, event);
3005             event.setClassName(ViewPager.class.getName());
3006             final AccessibilityRecordCompat recordCompat =
3007                     AccessibilityEventCompat.asRecord(event);
3008             recordCompat.setScrollable(canScroll());
3009             if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
3010                     && mAdapter != null) {
3011                 recordCompat.setItemCount(mAdapter.getCount());
3012                 recordCompat.setFromIndex(mCurItem);
3013                 recordCompat.setToIndex(mCurItem);
3014             }
3015         }
3016 
3017         @Override
3018         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
3019             super.onInitializeAccessibilityNodeInfo(host, info);
3020             info.setClassName(ViewPager.class.getName());
3021             info.setScrollable(canScroll());
3022             if (canScrollHorizontally(1)) {
3023                 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
3024             }
3025             if (canScrollHorizontally(-1)) {
3026                 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
3027             }
3028         }
3029 
3030         @Override
3031         public boolean performAccessibilityAction(View host, int action, Bundle args) {
3032             if (super.performAccessibilityAction(host, action, args)) {
3033                 return true;
3034             }
3035             switch (action) {
3036                 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
3037                     if (canScrollHorizontally(1)) {
3038                         setCurrentItem(mCurItem + 1);
3039                         return true;
3040                     }
3041                 } return false;
3042                 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
3043                     if (canScrollHorizontally(-1)) {
3044                         setCurrentItem(mCurItem - 1);
3045                         return true;
3046                     }
3047                 } return false;
3048             }
3049             return false;
3050         }
3051 
3052         private boolean canScroll() {
3053             return (mAdapter != null) && (mAdapter.getCount() > 1);
3054         }
3055     }
3056 
3057     private class PagerObserver extends DataSetObserver {
3058         @Override
3059         public void onChanged() {
3060             dataSetChanged();
3061         }
3062         @Override
3063         public void onInvalidated() {
3064             dataSetChanged();
3065         }
3066     }
3067 
3068     /**
3069      * Layout parameters that should be supplied for views added to a
3070      * ViewPager.
3071      */
3072     public static class LayoutParams extends ViewGroup.LayoutParams {
3073         /**
3074          * true if this view is a decoration on the pager itself and not
3075          * a view supplied by the adapter.
3076          */
3077         public boolean isDecor;
3078 
3079         /**
3080          * Gravity setting for use on decor views only:
3081          * Where to position the view page within the overall ViewPager
3082          * container; constants are defined in {@link android.view.Gravity}.
3083          */
3084         public int gravity;
3085 
3086         /**
3087          * Width as a 0-1 multiplier of the measured pager width
3088          */
3089         float widthFactor = 0.f;
3090 
3091         /**
3092          * true if this view was added during layout and needs to be measured
3093          * before being positioned.
3094          */
3095         boolean needsMeasure;
3096 
3097         /**
3098          * Adapter position this view is for if !isDecor
3099          */
3100         int position;
3101 
3102         /**
3103          * Current child index within the ViewPager that this view occupies
3104          */
3105         int childIndex;
3106 
3107         public LayoutParams() {
3108             super(FILL_PARENT, FILL_PARENT);
3109         }
3110 
3111         public LayoutParams(Context context, AttributeSet attrs) {
3112             super(context, attrs);
3113 
3114             final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
3115             gravity = a.getInteger(0, Gravity.TOP);
3116             a.recycle();
3117         }
3118     }
3119 
3120     static class ViewPositionComparator implements Comparator<View> {
3121         @Override
3122         public int compare(View lhs, View rhs) {
3123             final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
3124             final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
3125             if (llp.isDecor != rlp.isDecor) {
3126                 return llp.isDecor ? 1 : -1;
3127             }
3128             return llp.position - rlp.position;
3129         }
3130     }
3131 }
3132