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

Android 同时被 2 个专栏收录
73 篇文章 0 订阅

前言

前一篇博客介绍了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() {
@Override
public 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; // dp

private 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()重新绘制界面。
@Override
protected 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进行测量
   @Override
protected 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 表示当前页面左移的像素个数。
     */
 @CallSuper
protected 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时,才会拦截该触摸事件。
   @Override
public 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比我想象的要强大许多。以上有什么不准确的地方,希望大家多多指正。

  • 2
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值