解析ViewPager(二)——ViewPager源码解析

来源:互联网 发布:xsplit直播软件 编辑:程序博客网 时间:2024/06/03 14:53

前言

前一篇博客介绍了ViewPager的简单使用,这篇博客主要从源码的角度来解析ViewPager。

ViewPager的一些变量

      ViewPager是一组视图,那么它的父类必然是ViewGroup,也就是说ViewPager继承了ViewGroup的所有属性。我们先看一下部分源码:

public class ViewPager extends ViewGroup {      private static final String TAG = "ViewPager";      private static final boolean DEBUG = false;            private static final boolean USE_CACHE = false;            private static final int DEFAULT_OFFSCREEN_PAGES = 1;      private static final int MAX_SETTLE_DURATION = 600; // ms      private static final int MIN_DISTANCE_FOR_FLING = 25; // dips            private static final int DEFAULT_GUTTER_SIZE = 16; // dips            private static final int MIN_FLING_VELOCITY = 400; // dips            static final int[] LAYOUT_ATTRS = new int[] {      android.R.attr.layout_gravity      };            /**       * Used to track what the expected number of items in the adapter should be.       * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.       *用于监测项目中我们需要适配器的期望的页卡数       */      private int mExpectedAdapterCount;      /**       *  该类用于保存页面信息       */      static class ItemInfo {            Object object;//页面展示的页卡对象            int position;//页卡下标(页码)            boolean scrolling;//是否滚动            float widthFactor;//表示加载的页面占ViewPager所占的比例[0~1](默认返回1) ,这个值可以设置一个屏幕显示多少个页面            float offset;//页卡偏移量      }      //页卡排序      private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){      @Override      public int compare(ItemInfo lhs, ItemInfo rhs) {      return lhs.position - rhs.position;}};//插值器:他的作用就是根据不同的时间控制滑动的速度。private static final Interpolator sInterpolator = new Interpolator() {@Overridepublic float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}};//表示已经缓存的页面信息private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();private final ItemInfo mTempItem = new ItemInfo();PagerAdapter mAdapter;//页卡适配器int mCurItem;   // Index of currently displayed page.当前页面的下标// Offsets of the first and last items, if known.// Set during population, used to determine if we are at the beginning// or end of the pager data set during touch scrolling.private float mFirstOffset = -Float.MAX_VALUE;//第一个页卡的滑动偏移量private float mLastOffset = Float.MAX_VALUE;//最后一个页卡的滑动偏移量。。。。。。省略部分代码。。。。。。/** * Position of the last motion event. 最后页卡滑动事件的位置 */private float mLastMotionX;private float mLastMotionY;private float mInitialMotionX;private float mInitialMotionY;/** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */private int mActivePointerId = INVALID_POINTER;//活动指针标示 如果使用多个指针,这用于保持拖动/ flings期间的一致性。/** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */private static final int INVALID_POINTER = -1;//没有活动的当前指针的哨兵值/** * Determines speed during touch scrolling *这个速度追踪器用于触摸滑动时追踪滑动速度 */private VelocityTracker mVelocityTracker;private int mMinimumVelocity;private int mMaximumVelocity;private int mFlingDistance;private int mCloseEnough;// If the pager is at least this close to its final position, complete the scroll// on touch down and let the user interact with the content inside instead of// "catching" the flinging pager.//如果页面至少接近它的最终位置,完成向下滚动,让用户与内容中的内容进行交互,而不是“捕获”flinging页面。private static final int CLOSE_ENOUGH = 2; // dpprivate boolean mFakeDragging;private long mFakeDragBeginTime;private EdgeEffectCompat mLeftEdge;private EdgeEffectCompat mRightEdge;private boolean mFirstLayout = true;private boolean mNeedCalculatePageOffsets = false;private boolean mCalledSuper;private int mDecorChildCount;private List<OnPageChangeListener> mOnPageChangeListeners;private OnPageChangeListener mOnPageChangeListener;private OnPageChangeListener mInternalPageChangeListener;private List<OnAdapterChangeListener> mAdapterChangeListeners;private PageTransformer mPageTransformer;private int mPageTransformerLayerType;private Method mSetChildrenDrawingOrderEnabled;private static final int DRAW_ORDER_DEFAULT = 0;private static final int DRAW_ORDER_FORWARD = 1;private static final int DRAW_ORDER_REVERSE = 2;private int mDrawingOrder;private ArrayList<View> mDrawingOrderedChildren;private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();/** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */public static final int SCROLL_STATE_IDLE = 0;//空闲/** * Indicates that the pager is currently being dragged by the user. */public static final int SCROLL_STATE_DRAGGING = 1;//滑动/** * Indicates that the pager is in the process of settling to a final position. */public static final int SCROLL_STATE_SETTLING = 2;//滑动结束

这段代码中我们我们看到ViewPager继承自ViewGroup,主要我们看上面注释的几个变量:

  •  mExpectedAdapterCount:这个变量用于监测项目中我们需要适配器的期望的页卡数,如果APP改变了它,当我们不期望它的时候,会抛出一个异常!
  • ItemInfo:这个内部类是用来保存页卡信息的
  • sInterpolator:插值器,它的主要作用是根据不同的时间来控制滑动速度
  • ArrayList<ItemInfo> mItems:表示已经缓存的页面信息(通常会缓存当前显示页面以前当前页面前后页面,不过缓存页面的数量由mOffscreenPageLimit决定)
  • PagerAdapter mAdapter:页卡适配器
  • int mCurItem:当前页面的下标
  • mFirstOffset/mLastOffset  第/最后一个页卡的滑动偏移量
  • mActivePointerId:活动指针标示如果使用多个指针,这用于保持拖动/ flings期间的一致性。
  • mVelocityTracker:速度追踪器用于触摸滑动时追踪滑动速度
  • SCROLL_STATE_IDLE = 0:表示ViewPager处于空闲,建立状态。 当前页面完全在视图中,并且没有正在进行动画。
  • SCROLL_STATE_DRAGGING = 1:表示用户当前正在拖动ViewPager。
  • SCROLL_STATE_SETTLING = 2:表示ViewPager正在设置到最终位置。

ViewPager的几个重要方法

1、initViewPager()

   initViewPager 是初始化ViewPager,其实还是比较简单的,不难理解,源码如下:
void initViewPager() {        setWillNotDraw(false);        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);        setFocusable(true);        final Context context = getContext();        mScroller = new Scroller(context, sInterpolator);//创建Scroller对象        final ViewConfiguration configuration = ViewConfiguration.get(context);//一个标准常量        final float density = context.getResources().getDisplayMetrics().density;//获取屏幕密度        mTouchSlop = configuration.getScaledPagingTouchSlop();//获取TouchSlop:系统所能识别的被认为是滑动的最小距离        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);//最小速度        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();//获取允许执行一个fling手势的最大速度值        mLeftEdge = new EdgeEffectCompat(context);        mRightEdge = new EdgeEffectCompat(context);        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);        mCloseEnough = (int) (CLOSE_ENOUGH * density);        mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);        ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());        if (ViewCompat.getImportantForAccessibility(this)                == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {            ViewCompat.setImportantForAccessibility(this,                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);        }        //ViewCompat一个安卓官方实现兼容的帮助类        ViewCompat.setOnApplyWindowInsetsListener(this,                new android.support.v4.view.OnApplyWindowInsetsListener() {                    private final Rect mTempRect = new Rect();                    @Override                    public WindowInsetsCompat onApplyWindowInsets(final View v,                            final WindowInsetsCompat originalInsets) {                        // First let the ViewPager itself try and consume them...                        final WindowInsetsCompat applied =                                ViewCompat.onApplyWindowInsets(v, originalInsets);                        if (applied.isConsumed()) {                            // If the ViewPager consumed all insets, return now                            return applied;                        }                        // Now we'll manually dispatch the insets to our children. Since ViewPager                        // children are always full-height, we do not want to use the standard                        // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,                        // the rest of the children will not receive any insets. To workaround this                        // we manually dispatch the applied insets, not allowing children to                        // consume them from each other. We do however keep track of any insets                        // which are consumed, returning the union of our children's consumption                        final Rect res = mTempRect;                        res.left = applied.getSystemWindowInsetLeft();                        res.top = applied.getSystemWindowInsetTop();                        res.right = applied.getSystemWindowInsetRight();                        res.bottom = applied.getSystemWindowInsetBottom();                        for (int i = 0, count = getChildCount(); i < count; i++) {                            final WindowInsetsCompat childInsets = ViewCompat                                    .dispatchApplyWindowInsets(getChildAt(i), applied);                            // Now keep track of any consumed by tracking each dimension's min                            // value                            res.left = Math.min(childInsets.getSystemWindowInsetLeft(),                                    res.left);                            res.top = Math.min(childInsets.getSystemWindowInsetTop(),                                    res.top);                            res.right = Math.min(childInsets.getSystemWindowInsetRight(),                                    res.right);                            res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),                                    res.bottom);                        }                        // Now return a new WindowInsets, using the consumed window insets                        return applied.replaceSystemWindowInsets(                                res.left, res.top, res.right, res.bottom);                    }                });    }

2、onLayout()

   ViewPager继承了ViewGroup那么肯定就要重写onLayout()方法,该方法的主要作用是布局,那么当然也复写了onMeasure()方法测量。关于View的原理可以看看View的工作原理(三)--View的Layout和Draw过程。ViewPager的子View是水平摆放的,所以在onLayout中,大部分工作的就是计算childLeft,即子View的左边位置,而顶部位置基本上是一样的。Viewpager的onlayout其实就根据populate()方法中计算出的当前页面的offset来绘制当前页面,和其他页面.下面我们仔细去研究内部滑动源码或者setCurrentPage源码都可以发现实际上是调用了populate()方法。当我们需要有View更新的时候比如addView()、removeView()都会进行requestLayout()重新布局、以及invalidate()重新绘制界面。
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    final int count = getChildCount();    int width = r - l;    int height = b - t;    int paddingLeft = getPaddingLeft();    int paddingTop = getPaddingTop();    int paddingRight = getPaddingRight();    int paddingBottom = getPaddingBottom();    final int scrollX = getScrollX();    //DecorView 数量    int decorCount = 0;    //首先对DecorView进行layout,再对普通页卡进行layout,之所以先对DecorView布局,是为了让普通页卡(页卡)能有合适的偏移    //下面循环主要是针对DecorView    for (int i = 0; i < count; i++) {        final View child = getChildAt(i);        //visibility不为GONE才layout        if (child.getVisibility() != GONE) {            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //左边和顶部的边距初始化为0            int childLeft = 0;            int childTop = 0;            if (lp.isDecor) {//只针对Decor View                //获取水平或垂直方向上的Gravity                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;                //根据水平方向上的Gravity,确定childLeft以及paddingRight                switch (hgrav) {                    default://没有设置水平方向Gravity时(左中右),childLeft就取paddingLeft                        childLeft = paddingLeft;                        break;                    case Gravity.LEFT://水平方向Gravity为left,DecorView往最左边靠                        childLeft = paddingLeft;                        paddingLeft += child.getMeasuredWidth();                        break;                    case Gravity.CENTER_HORIZONTAL://将DecorView居中摆放                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2,                                paddingLeft);                        break;                    case Gravity.RIGHT://将DecorView往最右边靠                        childLeft = width - paddingRight - child.getMeasuredWidth();                        paddingRight += child.getMeasuredWidth();                        break;                }                //与上面水平方向的同理,据水平方向上的Gravity,确定childTop以及paddingTop                switch (vgrav) {                    default:                        childTop = paddingTop;                        break;                    case Gravity.TOP:                        childTop = paddingTop;                        paddingTop += child.getMeasuredHeight();                        break;                    case Gravity.CENTER_VERTICAL:                        childTop = Math.max((height - child.getMeasuredHeight()) / 2,                                paddingTop);                        break;                    case Gravity.BOTTOM:                        childTop = height - paddingBottom - child.getMeasuredHeight();                        paddingBottom += child.getMeasuredHeight();                        break;                }                //上面计算的childLeft是相对ViewPager的左边计算的,                //还需要加上x方向已经滑动的距离scrollX                childLeft += scrollX;                //对DecorView布局                child.layout(childLeft, childTop,                        childLeft + child.getMeasuredWidth(),                        childTop + child.getMeasuredHeight());                //将DecorView数量+1                decorCount++;            }        }    }    //普通页卡的宽度    final int childWidth = width - paddingLeft - paddingRight;    // Page views. Do this once we have the right padding offsets from above.    //下面针对普通页卡布局,在此之前我们已经得到正确的偏移量了    for (int i = 0; i < count; i++) {        final View child = getChildAt(i);        if (child.getVisibility() != GONE) {            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //ItemInfo 是ViewPager静态内部类,前面介绍过它保存了普通页卡(也就是页卡)的position、offset等信息,是对普通页卡的一个抽象描述            ItemInfo ii;            //infoForChild通过传入View查询对应的ItemInfo对象            if (!lp.isDecor && (ii = infoForChild(child)) != null) {            //计算当前页卡的左边偏移量                int loff = (int) (childWidth * ii.offset);                //将左边距+左边偏移量得到最终页卡左边位置                int childLeft = paddingLeft + loff;                int childTop = paddingTop;                //如果当前页卡需要进行测量(measure),当这个页卡是在Layout期间新添加新的,                // 那么这个页卡需要进行测量,即needsMeasure为true                if (lp.needsMeasure) {                    //标记已经测量过了                    lp.needsMeasure = false;                    //下面过程跟onMeasure类似                    final int widthSpec = MeasureSpec.makeMeasureSpec(                            (int) (childWidth * lp.widthFactor),                            MeasureSpec.EXACTLY);                    final int heightSpec = MeasureSpec.makeMeasureSpec(                            (int) (height - paddingTop - paddingBottom),                            MeasureSpec.EXACTLY);                    child.measure(widthSpec, heightSpec);                }                if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object                        + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()                        + "x" + child.getMeasuredHeight());                //对普通页卡进行layout                child.layout(childLeft, childTop,                        childLeft + child.getMeasuredWidth(),                        childTop + child.getMeasuredHeight());            }        }    }    //将部分局部变量保存到实例变量中    mTopPageBounds = paddingTop;    mBottomPageBounds = height - paddingBottom;    mDecorChildCount = decorCount;    //如果是第一次layout,则将ViewPager滑动到第一个页卡的位置    if (mFirstLayout) {        scrollToItem(mCurItem, false, 0, false);    }    //标记已经布局过了,即不再是第一次布局了    mFirstLayout = false;}

3,onMeasure()

  前面,onLayout()方法中布局,用到了measure测量的结果,下面我们就来看下ViewPager的onMeasure()。该方法主要做了四件事:

  1. 测量DecorView
  2. 确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)
  3. 确保我们需要显示的fragment已经被我们创建好了
  4. 再对子View进行测量
   @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    //根据布局文件,设置尺寸信息,默认大小为0    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),            getDefaultSize(0, heightMeasureSpec));    final int measuredWidth = getMeasuredWidth();    final int maxGutterSize = measuredWidth / 10;    //设置mGutterSize的值,后面再讲mGutterSize    mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);    // ViewPager的显示区域只能显示对于一个View    //childWidthSize和childHeightSize为一个View的可用宽高大小    //即去除了ViewPager内边距后的宽高    int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();    int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();    //先对DecorView进行测量    //下面这个循环是只针对DecorView的,即用于装饰ViewPager的View    int size = getChildCount();    for (int i = 0; i < size; ++i) {        final View child = getChildAt(i);        if (child.getVisibility() != GONE) {            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //如果该View是DecorView,即用于装饰ViewPager的View            if (lp != null && lp.isDecor) {                //获取Decor View的在水平方向和竖直方向上的Gravity                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;                //默认DedorView模式对应的宽高是wrap_content                int widthMode = MeasureSpec.AT_MOST;                int heightMode = MeasureSpec.AT_MOST;                //记录DecorView是在垂直方向上还是在水平方向上占用空间                boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;                boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;                //consumeHorizontal:如果是在垂直方向上占用空间,                // 那么水平方向就是match_parent,即EXACTLY                //而垂直方向上具体占用多少空间,还得由DecorView决定                //consumeHorizontal也是同理                if (consumeVertical) {                    widthMode = MeasureSpec.EXACTLY;                } else if (consumeHorizontal) {                    heightMode = MeasureSpec.EXACTLY;                }                //宽高大小,初始化为ViewPager可视区域中页卡可用空间                int widthSize = childWidthSize;                int heightSize = childHeightSize;                //如果宽度不是wrap_content,那么width的测量模式就是EXACTLY                //如果宽度既不是wrap_content又不是match_parent,那么说明是用户                //在布局文件写的具体的尺寸,直接将widthSize设置为这个具体尺寸                if (lp.width != LayoutParams.WRAP_CONTENT) {                    widthMode = MeasureSpec.EXACTLY;                    if (lp.width != LayoutParams.FILL_PARENT) {                        widthSize = lp.width;                    }                }                if (lp.height != LayoutParams.WRAP_CONTENT) {                    heightMode = MeasureSpec.EXACTLY;                    if (lp.height != LayoutParams.FILL_PARENT) {                        heightSize = lp.height;                    }                }                //确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);                final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);                //对DecorView进行测量                child.measure(widthSpec, heightSpec);                //如果Decor View占用了ViewPager的垂直方向的空间                //需要将页卡的竖直方向可用的空间减去DecorView的高度,                //同理,水平方向上也做同样的处理                if (consumeVertical) {                    childHeightSize -= child.getMeasuredHeight();                } else if (consumeHorizontal) {                    childWidthSize -= child.getMeasuredWidth();                }            }        }    }    //确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)    mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);    mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);    //确保我们需要显示的fragment已经被我们创建好了    mInLayout = true;    populate();//后面再详细介绍    mInLayout = false;    //再对页卡进行测量    size = getChildCount();    for (int i = 0; i < size; ++i) {        final View child = getChildAt(i);        //visibility为GONE的无需测量        if (child.getVisibility() != GONE) {            if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child                    + ": " + mChildWidthMeasureSpec);            //获取页卡的LayoutParams            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //只针对页卡而不对Decor View测量            if (lp == null || !lp.isDecor) {                //LayoutParams的widthFactor是取值为[0,1]的浮点数,                // 用于表示页卡占ViewPager显示区域中页卡可用宽度的比例,                // 即(childWidthSize * lp.widthFactor)表示当前页卡的实际宽度                final int widthSpec = MeasureSpec.makeMeasureSpec(                        (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);                //对当前页卡进行测量                child.measure(widthSpec, mChildHeightMeasureSpec);            }        }    }}

4、populate()

      前面多处用到了populate()方法,下面我们就来研究一下populate()方法,看这个方法我看得有点懵逼!!!主要是之前一直没将重点放在PagerAdapter,与PagerAdapter联系起来后发现原来还是比较容易的,populate()方法主要的作用是:根据制定的页面缓存大小,做了页面的销毁和重建。
1.更新items,将items中的内容换成当前展示页面以及预缓存页面。我们从下面的源码中可以看到,这里会调用PagerAdapter的startUpdate()、instantiateItem()、destroyItem()、setPrimaryItem()、finishUpdate()等方法,基本是把PagerAdapter的所有生命周期从头走到尾。
2.计算每个items的off(偏移量),这个就是布局时onLayout()方法中起作用的。
void populate(int newCurrentItem) {    ItemInfo oldCurInfo = null;    if (mCurItem != newCurrentItem) {        oldCurInfo = infoForPosition(mCurItem);        mCurItem = newCurrentItem;    }    if (mAdapter == null) {        //对页卡的绘制顺序进行排序,优先绘制DecorView        //再按照position从小到大排序        sortChildDrawingOrder();        return;    }    //如果我们正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建页卡,    // 直到滚动到最终位置再去创建,以免在这个期间出现差错    if (mPopulatePending) {        if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");        //对页卡的绘制顺序进行排序,优先绘制Decor View        //再按照position从小到大排序        sortChildDrawingOrder();        return;    }    //同样,在ViewPager没有attached到window之前,不要populate.    // 这是因为如果我们在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突    if (getWindowToken() == null) {        return;    }    //回调PagerAdapter的startUpdate函数,    // 告诉PagerAdapter开始更新要显示的页面    mAdapter.startUpdate(this);    final int pageLimit = mOffscreenPageLimit;    //确保起始位置大于等于0,如果用户设置了缓存页面数量,第一个页面为当前页面减去缓存页面数量    final int startPos = Math.max(0, mCurItem - pageLimit);    //保存数据源中的数据个数    final int N = mAdapter.getCount();    //确保最后的位置小于等于数据源中数据个数-1,    // 如果用户设置了缓存页面数量,第一个页面为当前页面加缓存页面数量    final int endPos = Math.min(N - 1, mCurItem + pageLimit);    //判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常    if (N != mExpectedAdapterCount) {        //resName用于抛异常显示        String resName;        try {            resName = getResources().getResourceName(getId());        } catch (Resources.NotFoundException e) {            resName = Integer.toHexString(getId());        }        throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +                " contents without calling PagerAdapter#notifyDataSetChanged!" +                " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +                " Pager id: " + resName +                " Pager class: " + getClass() +                " Problematic adapter: " + mAdapter.getClass());    }    //定位到当前获焦的页面,如果没有的话,则添加一个    int curIndex = -1;    ItemInfo curItem = null;    //遍历每个页面对应的ItemInfo,找出获焦页面    for (curIndex = 0; curIndex < mItems.size(); curIndex++) {        final ItemInfo ii = mItems.get(curIndex);        //找到当前页面对应的ItemInfo后,跳出循环        if (ii.position >= mCurItem) {            if (ii.position == mCurItem) curItem = ii;            break;        }    }    //如果没有找到获焦的页面,说明mItems列表里面没有保存获焦页面,    // 需要将获焦页面加入到mItems里面    if (curItem == null && N > 0) {        curItem = addNewItem(mCurItem, curIndex);    }    //默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数量,    // 则将当前页面两边都缓存用户指定的数量的页面    //如果当前没有页面,则我们啥也不需要做    if (curItem != null) {        float extraWidthLeft = 0.f;        //左边的页面        int itemIndex = curIndex - 1;        //如果当前页面左边有页面,则将左边页面对应的ItemInfo取出,否则左边页面的ItemInfo为null        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;        //保存显示区域的宽度        final int clientWidth = getClientWidth();        //算出左边页面需要的宽度,注意,这里的宽度是指实际宽度与可视区域宽度比例,        // 即实际宽度=leftWidthNeeded*clientWidth        final float leftWidthNeeded = clientWidth <= 0 ? 0 :                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;        //从当前页面左边第一个页面开始,左边的页面进行遍历        for (int pos = mCurItem - 1; pos >= 0; pos--) {            //如果左边的宽度超过了所需的宽度,并且当前当前页面位置比第一个缓存页面位置小            //这说明这个页面需要Destroy掉            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {                //如果左边已经没有页面了,跳出循环                if (ii == null) {                    break;                }                //将当前页面destroy掉                if (pos == ii.position && !ii.scrolling) {                    mItems.remove(itemIndex);                    //回调PagerAdapter的destroyItem                    mAdapter.destroyItem(this, pos, ii.object);                    if (DEBUG) {                        Log.i(TAG, "populate() - destroyItem() with pos: " + pos +                                " view: " + ((View) ii.object));                    }                    //由于mItems删除了一个元素                    //需要将索引减一                    itemIndex--;                    curIndex--;                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;                }            } else if (ii != null && pos == ii.position) {                //如果当前位置是需要缓存的位置,并且这个位置上的页面已经存在                //则将左边宽度加上当前位置的页面                extraWidthLeft += ii.widthFactor;                //mItems往左遍历                itemIndex--;                //ii设置为当前遍历的页面的左边一个页面                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;            } else {//如果当前位置是需要缓存,并且这个位置没有页面                //需要添加一个ItemInfo,而addNewItem是通过PagerAdapter的instantiateItem获取对象                ii = addNewItem(pos, itemIndex + 1);                //将左边宽度加上当前位置的页面                extraWidthLeft += ii.widthFactor;                //由于新加了一个元素,当前的索引号需要加1                curIndex++;                //ii设置为当前遍历的页面的左边一个页面                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;            }        }        //同理,右边需要添加缓存的页面        /*........................*         *                        *         * 省略右边添加缓存页面代码  *         *                        *         *........................*/        calculatePageOffsets(curItem, curIndex, oldCurInfo);    }    if (DEBUG) {        Log.i(TAG, "Current page list:");        for (int i = 0; i < mItems.size(); i++) {            Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);        }    }    //回调PagerAdapter的setPrimaryItem,告诉PagerAdapter当前显示的页面    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);    //回调PagerAdapter的finishUpdate,告诉PagerAdapter页面更新结束    mAdapter.finishUpdate(this);    //检查页面的宽度是否测量,如果页面的LayoutParams数据没有设定,则去重新设定好    final int childCount = getChildCount();    for (int i = 0; i < childCount; i++) {        final View child = getChildAt(i);        final LayoutParams lp = (LayoutParams) child.getLayoutParams();        lp.childIndex = i;        if (!lp.isDecor && lp.widthFactor == 0.f) {            // 0 means requery the adapter for this, it doesn't have a valid width.            final ItemInfo ii = infoForChild(child);            if (ii != null) {                lp.widthFactor = ii.widthFactor;                lp.position = ii.position;            }        }    }    //重新对页面排序    sortChildDrawingOrder();    //如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦    if (hasFocus()) {        View currentFocused = findFocus();        ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;        if (ii == null || ii.position != mCurItem) {            for (int i = 0; i < getChildCount(); i++) {                View child = getChildAt(i);                ii = infoForChild(child);                if (ii != null && ii.position == mCurItem) {                    if (child.requestFocus(View.FOCUS_FORWARD)) {                        break;                    }                }            }        }    }}

5、setAdapter()

  这个方法很容易理解,就是设置ViewPager所需要的适配器。我们看下面的源码:
   /**     * Set a PagerAdapter that will supply views for this pager as needed.     *     * @param adapter Adapter to use     */public void setAdapter(PagerAdapter adapter) {    //如果已经设置过PagerAdapter,即mAdapter != null,做一些清理工作    if (mAdapter != null) {        //清除观察者        mAdapter.setViewPagerObserver(null);        //回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面        mAdapter.startUpdate(this);        //4如果之前保存有页面,则将之前所有的页面destroy掉        for (int i = 0; i < mItems.size(); i++) {            final ItemInfo ii = mItems.get(i);            mAdapter.destroyItem(this, ii.position, ii.object);        }        //回调finishUpdate,告诉PagerAdapter结束更新        mAdapter.finishUpdate(this);        //将所有的页面清除        mItems.clear();        //将所有的非Decor View移除,即将页面移除        removeNonDecorViews();        //当前的显示页面重置到第一个        mCurItem = 0;        //滑动重置到(0,0)位置        scrollTo(0, 0);    }    //保存上一次的PagerAdapter    final PagerAdapter oldAdapter = mAdapter;    //设置mAdapter为新的PagerAdapter    mAdapter = adapter;    //设置期望的适配器中的页面数量为0个    mExpectedAdapterCount = 0;    //如果设置的PagerAdapter不为null    if (mAdapter != null) {        //确保观察者不为null,观察者主要是用于监视数据源的内容发生变化        if (mObserver == null) {            mObserver = new PagerObserver();        }        //将观察者设置到PagerAdapter中        mAdapter.setViewPagerObserver(mObserver);        mPopulatePending = false;        //保存上一次是否是第一次Layout        final boolean wasFirstLayout = mFirstLayout;        //设定当前为第一次Layout        mFirstLayout = true;        //更新期望的数据源中页面个数        mExpectedAdapterCount = mAdapter.getCount();        //如果有数据需要恢复        if (mRestoredCurItem >= 0) {            //回调PagerAdapter的restoreState函数            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);            setCurrentItemInternal(mRestoredCurItem, false, true);            //标记无需再恢复            mRestoredCurItem = -1;            mRestoredAdapterState = null;            mRestoredClassLoader = null;        } else if (!wasFirstLayout) {//如果在此之前不是第一次Layout            //由于ViewPager并不是将所有页面作为页卡,            // 而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面)            //因此需要创建和销毁页面,populate主要工作就是这些            populate();        } else {            //重新布局(Layout)            requestLayout();        }    }    //如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器    //则回调OnAdapterChangeListener的onAdapterChanged函数    if (mAdapterChangeListener != null && oldAdapter != adapter) {        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);    }}

6、onPageScrolled()

    当滚动当前页时,将调用此方法,作为程序启动的平滑滚动或用户启动的触摸滚动的一部分。如果你重写这个方法,你必须调用到超类实现(例如,在onPageScrolled之前的super.onPageScrolled(position,offset,offsetPixels))。这段代码也比较好理解,就是控制ViewPager的滚动,将我们需要的内容显示在屏幕上,比如滑动到中间时,一半是position另一半是position+1.同时这个方法也是非常重要的,我们如若改造优化ViewPager,就需要重写该方法。
    /**     * This method will be invoked when the current page is scrolled, either as part     * of a programmatically initiated smooth scroll or a user initiated touch scroll.     * If you override this method you must call through to the superclass implementation     * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled     * returns.     *     * @param position 表示当前是第几个页面     *                      * @param offset 表示当前页面移动的距离,其实就是个相对实际宽度比例值,取值为[0,1)。0表示整个页面在显示区域,1表示整个页面已经完全左移出显示区域。     * @param offsetPixels 表示当前页面左移的像素个数。     */ @CallSuperprotected void onPageScrolled(int position, float offset, int offsetPixels) {    // Offset any decor views if needed - keep them on-screen at all times.    //如果有DecorView,则需要使得它们时刻显示在屏幕中,不移出屏幕    if (mDecorChildCount > 0) {        //根据Gravity将DecorView摆放到指定位置。        //这部分代码与onMeasure()方法中的原理一样,这里就不做解释了        final int scrollX = getScrollX();            int paddingLeft = getPaddingLeft();            int paddingRight = getPaddingRight();            final int width = getWidth();            final int childCount = getChildCount();            for (int i = 0; i < childCount; i++) {                final View child = getChildAt(i);                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                if (!lp.isDecor) continue;                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;                int childLeft = 0;                switch (hgrav) {                    default:                        childLeft = paddingLeft;                        break;                    case Gravity.LEFT:                        childLeft = paddingLeft;                        paddingLeft += child.getWidth();                        break;                    case Gravity.CENTER_HORIZONTAL:                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2,                                paddingLeft);                        break;                    case Gravity.RIGHT:                        childLeft = width - paddingRight - child.getMeasuredWidth();                        paddingRight += child.getMeasuredWidth();                        break;                }                childLeft += scrollX;                final int childOffset = childLeft - child.getLeft();                if (childOffset != 0) {                    child.offsetLeftAndRight(childOffset);                }            }    }    //分发页面滚动事件,类似于事件的分发    dispatchOnPageScrolled(position, offset, offsetPixels);    //如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数    if (mPageTransformer != null) {        final int scrollX = getScrollX();        final int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            final View child = getChildAt(i);            final LayoutParams lp = (LayoutParams) child.getLayoutParams();            //只针对页面进行处理            if (lp.isDecor) continue;            //计算child位置            final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();            //调用transformPage            mPageTransformer.transformPage(child, transformPos);        }    }    //标记ViewPager的onPageScrolled函数执行过    mCalledSuper = true;}

  ViewPager的重点是滑动,那么我们来看一下ViewPager的触摸事件,我们主要看事件的拦截onInterceptTouchEvent(),以及事件的消耗onTouchEvent()。我们这里就只看onInterceptTouchEvent(),明白了这段代码,onTouchEvent()也就很容易理解了。

7、onInterceptTouchEvent()

 关于ViewPager对于事件的拦截,我们只有当拖动ViewPager时ViewPager才会变化,也就是只有当我们拖拽ViewPager时,才会拦截该触摸事件。
   @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    // 触摸动作    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;    // 时刻要注意触摸是否已经结束    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {        //Release the drag.        if (DEBUG) Log.v(TAG, "Intercept done!");        //重置一些跟判断是否拦截触摸相关变量        resetTouch();        //触摸结束,无需拦截        return false;    }    // 如果当前不是按下事件,我们就判断一下,是否是在拖拽切换页面    if (action != MotionEvent.ACTION_DOWN) {        //如果当前是正在拽切换页面,直接拦截掉事件,后面无需再做拦截判断        if (mIsBeingDragged) {            if (DEBUG) Log.v(TAG, "Intercept returning true!");            return true;        }        //如果标记为不允许拖拽切换页面,我们就不处理一切触摸事件        if (mIsUnableToDrag) {            if (DEBUG) Log.v(TAG, "Intercept returning false!");            return false;        }    }    //根据不同的动作进行处理    switch (action) {        //如果是手指移动操作        case MotionEvent.ACTION_MOVE: {            //代码能执行到这里,就说明mIsBeingDragged==false,否则的话,在第7个注释处就已经执行结束了            //使用触摸点Id,主要是为了处理多点触摸            final int activePointerId = mActivePointerId;            if (activePointerId == INVALID_POINTER) {                //如果当前的触摸点id不是一个有效的Id,无需再做处理                break;            }            //根据触摸点的id来区分不同的手指,我们只需关注一个手指就好            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);            //根据这个手指的序号,来获取这个手指对应的x坐标            final float x = MotionEventCompat.getX(ev, pointerIndex);            //在x轴方向上移动的距离            final float dx = x - mLastMotionX;            //x轴方向的移动距离绝对值            final float xDiff = Math.abs(dx);            //与x轴同理            final float y = MotionEventCompat.getY(ev, pointerIndex);            final float yDiff = Math.abs(y - mInitialMotionY);            if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);            //判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理            //isGutterDrag是判断是否在两个页面之间的缝隙内移动            //canScroll是判断页面是否可以滑动            if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&                    canScroll(this, false, (int) dx, (int) x, (int) y)) {                mLastMotionX = x;                mLastMotionY = y;                //标记ViewPager不去拦截事件                mIsUnableToDrag = true;                return false;            }            //如果x移动距离大于最小距离,并且斜率小于0.5,表示在水平方向上的拖动            if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {                if (DEBUG) Log.v(TAG, "Starting drag!");                //水平方向的移动,需要ViewPager去拦截                mIsBeingDragged = true;                //如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager                requestParentDisallowInterceptTouchEvent(true);                //设置滚动状态                setScrollState(SCROLL_STATE_DRAGGING);                //保存当前位置                mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :                        mInitialMotionX - mTouchSlop;                mLastMotionY = y;                //启用缓存                setScrollingCacheEnabled(true);            } else if (yDiff > mTouchSlop) {//27.否则的话,表示是竖直方向上的移动                if (DEBUG) Log.v(TAG, "Starting unable to drag!");                //竖直方向上的移动则不去拦截触摸事件                mIsUnableToDrag = true;            }            if (mIsBeingDragged) {                //跟随手指一起滑动                if (performDrag(x)) {                    ViewCompat.postInvalidateOnAnimation(this);                }            }            break;        }        //如果手指是按下操作        case MotionEvent.ACTION_DOWN: {            //记录按下的点位置            mLastMotionX = mInitialMotionX = ev.getX();            mLastMotionY = mInitialMotionY = ev.getY();            //第一个ACTION_DOWN事件对应的手指序号为0            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);            //重置允许拖拽切换页面            mIsUnableToDrag = false;            //标记开始滚动            mIsScrollStarted = true;            //手动调用计算滑动的偏移量            mScroller.computeScrollOffset();            //如果当前滚动状态为正在将页面放置到最终位置,            //且当前位置距离最终位置足够远            if (mScrollState == SCROLL_STATE_SETTLING &&                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {                //如果此时用户手指按下,则立马暂停滑动                mScroller.abortAnimation();                mPopulatePending = false;                populate();                mIsBeingDragged = true;                //如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager                requestParentDisallowInterceptTouchEvent(true);                //设置当前状态为正在拖拽                setScrollState(SCROLL_STATE_DRAGGING);            } else {                //结束滚动                completeScroll(false);                mIsBeingDragged = false;            }            if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY                    + " mIsBeingDragged=" + mIsBeingDragged                    + "mIsUnableToDrag=" + mIsUnableToDrag);            break;        }        case MotionEventCompat.ACTION_POINTER_UP:            onSecondaryPointerUp(ev);            break;    }    //添加速度追踪    if (mVelocityTracker == null) {        mVelocityTracker = VelocityTracker.obtain();    }    mVelocityTracker.addMovement(ev);    //只有在当前是拖拽切换页面时我们才会去拦截事件    return mIsBeingDragged;}

总结

   ViewPager的主要原理我的理解就是,保存缓存的数组mItems的大小永远都在[0,mOffscreenPageLimit*2+1]范围内,我们滑动下一页卡时,它将前一页卡移出数组,将下一页卡加入缓存。本来打算一片文章写完ViewPager的结果写的时候发现,我对ViewPager的认识还是不足,ViewPager比我想象的要强大许多。以上有什么不准确的地方,希望大家多多指正。

1 0