ViewPager与PagerAdapter深度解析

来源:互联网 发布:手机维棠软件 编辑:程序博客网 时间:2024/06/07 13:16

http://blog.csdn.net/zzxzhyt/article/details/50689308

ViewPager与PagerAdapter深度解析

版本 
Android.support.v4.view.ViewPager 
android.support.v4.view. PagerAdapter

这一篇主要是针对ViewPager的深度解析,本篇文章就为了解决两个疑问:ViewPager是怎么实现的?ViewPager与PagerAdapter又是怎么交互的?我们会一一揭开ViewPager的面纱,借此对ViewPager有更深层次的了解,而不仅仅停留在使用层面上。本篇通过以下三个问题来剖析ViewPager源码:

  • ViewPager怎么创建视图?
  • ViewPager怎么实现滚动?
  • PagerAdapter与ViewPager怎么交互的?我们创建适配器时候重写的方法对ViewPager有什么影响?

ViewPager怎么创建视图

如果你有接触过自定义View的话,那么你会知道我们通常都是通过重写View.onMeasure(),View.onLayout(),View.onDraw()。去自定义View.所以我们通过这三个方法去一一剖析。

在分析解析onMeasure之前我们要先明白一点,ViewPager的Child View分为两种,一种是Decor View 
装饰视图,一种是Content View,也就是我们通过适配器给予的内容视图。

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    ViewPager.onMeasure(int,int)做了三件事: 
    1. 设置ViewPager的测量长宽值为父容器给与的最大长宽值
    2. 测量Decor View
    3. 测量Content View
    4. 如果存在适配器且有内容,则更新Content View
 @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {       //设置ViewPager的测量大小,这里只是通过getDefaultSize简单获取并设置了整个ViewPager,这里获取的值实质是父容器给予的最大值。        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),                getDefaultSize(0, heightMeasureSpec));         /**. 省略Code..**/        //处理Padding。        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();        int childHeightSize = getMeasuredHeight() - getPaddingTop() -getPaddingBottom();         //确定Decor View的大小,并减去相应的空间,给予Content 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();                        /**..省略Code..***/                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);                    //测量Decor View                    child.measure(widthSpec, heightSpec);                    if (consumeVertical) {                        childHeightSize -= child.getMeasuredHeight();                    } else if (consumeHorizontal) {                        childWidthSize -= child.getMeasuredWidth();                    }                }            }        }        //创建剩余空间的MeasureSpec        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);        mInLayout = true;        //更新Content View.        //这是一个源码中十分重要的方法,下面会具体分析。          populate();        mInLayout = false;        // 测量Content View大小.        size = getChildCount();        for (int i = 0; i < size; ++i) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                if (lp == null || !lp.isDecor) {                    //lp.widthFactor起了什么作用?                    final int widthSpec = MeasureSpec.makeMeasureSpec(                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);                    //测量Content View,mChildHeightMeasureSpec就是上面减去装饰View后的剩余大小                    child.measure(widthSpec, mChildHeightMeasureSpec);                }            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

上面我们提到了populate()方法。这个方法主要是更新&创建&销毁相应的Content View,这个方法在源码中多次被使用,这个方法也确实起着十分重要的作用,我们看看它的源码。

 void populate() {        //调用另一个重载方法,直接看下面的重载方法        populate(mCurItem); } void populate(int newCurrentItem) {        ItemInfo oldCurInfo = null;        int focusDirection = View.FOCUS_FORWARD;        //更新当前项的位置变量        if (mCurItem != newCurrentItem) {            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;            oldCurInfo = infoForPosition(mCurItem);            mCurItem = newCurrentItem;        }        /**..省略Code...,这里是一系列针对强壮性的判断,自行查看源码**/        //PagerAdapter回调        mAdapter.startUpdate(this);        //mOffscreenPagLimit这个常量代表着ViewPager会保持着多少个实例        final int pageLimit = mOffscreenPageLimit;        final int startPos = Math.max(0, mCurItem - pageLimit);        final int N = mAdapter.getCount();        final int endPos = Math.min(N-1, mCurItem + pageLimit);        /**..省略Code...,这里是一系列针对强壮性的判断,自行查看源码**/        int curIndex = -1;        ItemInfo curItem = null;        //获取Content View相应的ItemInfo,ItemInfo保存着Content View的引用以及位置等重要信息        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {            final ItemInfo ii = mItems.get(curIndex);            if (ii.position >= mCurItem) {                if (ii.position == mCurItem) curItem = ii;                break;            }        }        //如果没有当前位置的Content View信息,明显我们需要去创建它,这里调用了addNewItem去创建一个新的Content View。        //addNewItem就是通过PagerApdate.instantiateItem()方法获取新Content View        if (curItem == null && N > 0) {            curItem = addNewItem(mCurItem, curIndex);        }        //这里就是ViewPager固定持有一定页面的具体逻辑代码        if (curItem != null) {                 /** ..省略Code.. **/            for (int pos = mCurItem - 1; pos >= 0; pos--) {                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {                    /** ..省略Code..,根据页面最大持有数创建和销毁左边页面Content View **/                 }            }            float extraWidthRight = curItem.widthFactor;            itemIndex = curIndex + 1;            if (extraWidthRight < 2.f) {                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;                final float rightWidthNeeded = clientWidth <= 0 ? 0 :                        (float) getPaddingRight() / (float) clientWidth + 2.f;                for (int pos = mCurItem + 1; pos < N; pos++) {                    /** ..省略Code..,根据页面最大持有数创建和销毁右边页面Content View **/                }            }            //更新页面的偏移量,主要是ItemInfo里面的信息            calculatePageOffsets(curItem, curIndex, oldCurInfo);        }        //PagerAdapter方法回调        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);         mAdapter.finishUpdate(this);        /**..省略Code..,更新所有 Content View的Layout Params信息**/    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

这里我们总结一下populate(int)做的事情: 
1.如果还未有当前页面的Content View,则通过PagerAdapter创建 Content View。 
2.根据页面数量极限值,即ViewPager.mOffscreenPageLimit.来创建或者销毁页面左右的两边的页面。 
3.更新所有Content View 的LayoutParams属性

  • onLayout(boolean changed, int l, int t, int r, int b)

结束了populate(int)这个小插曲之后,接下来我们分析 onLayout()。在onLayout过程中,主要是处理Decor View 与Content View的位置,由于Decor View 可能有多个,所以这里是根据类似线性布局的方式对Decor View进行布局。例如从上到下等,Content View 则在减去了Decor View占用空间的基础上,从左到右进行布局。

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();        int decorCount = 0;        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                int childLeft = 0;                int childTop = 0;                if (lp.isDecor) {                      /**省略Code,根据装饰视图的布局进行相应处理**/                 }                 childLeft += scrollX;                 //调用Decor View 布局方法                 child.layout(childLeft, childTop,                            childLeft + child.getMeasuredWidth(),                            childTop + child.getMeasuredHeight());                 decorCount++;                }            }        }        //其中padding的值已附加上Decor View所占有的位置        final int childWidth = width - paddingLeft - paddingRight;        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final LayoutParams lp = (LayoutParams) child.getLayoutParams();                ItemInfo ii;                if (!lp.isDecor && (ii = infoForChild(child)) != null) {                    int loff = (int) (childWidth * ii.offset);                    int childLeft = paddingLeft + loff;                    int childTop = paddingTop;                 if (lp.needsMeasure) {                        lp.needsMeasure = false;                        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);                    }                    //调用了Content View的布局方法,注意这里的padding值已减去了Decor View占有的位置                    child.layout(childLeft, childTop,                            childLeft + child.getMeasuredWidth(),                            childTop + child.getMeasuredHeight());                }            }        }        /***省略Code,保存padding值**/        //处理当前滚动信息         if (mFirstLayout) {            scrollToItem(mCurItem, false, 0, false);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

onLayout()方法十分清晰,主要是对Decor View 与Content View的布局。

接下来我们看绘画阶段,ViewPager的重写了draw(Canvas)与 onDraw(Canvas)。draw(Canvas)方法很简单,只是对左右边缘效果进行处理,而onDraw(Canvas)亦十分简单,只是绘画了页面的间隔效果。

  • public void draw(Canvas canvas)
 public void draw(Canvas canvas) {        super.draw(canvas);        boolean needsInvalidate = false;        final int overScrollMode = ViewCompat.getOverScrollMode(this);        if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||                (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&                        mAdapter != null && mAdapter.getCount() > 1)) {            if (!mLeftEdge.isFinished()) {                 /**省略Code,这里绘画左边缘效果**/            }            if (!mRightEdge.isFinished()) {               /**省略Code,这里绘画右边缘效果**/            }        } else {            mLeftEdge.finish();            mRightEdge.finish();        }        if (needsInvalidate) {            // Keep animating            ViewCompat.postInvalidateOnAnimation(this);        }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • public void onDraw(Canvas)
 @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        // Draw the margin drawable between pages if needed.        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {          /**省略Code,根据注释这里绘画了页面的间隔效果**/         }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

至此,ViewPager的测量、布局、绘画三个过程均已经解析完毕。

在这三个过程中,不知道大家在阅读源码的时候会不会对Child View的来源感产生疑问?这里我特别根据ViewGroup.addView在ViewPager里面进行关键字搜索,并没有找到相应添加View的方法,所以这也是为什么在ViewPager.instantiateItem时候需要手动将Content View添加到ViewPager的原因,对应的,我们需要在ViewPager.destoryItem中将Content View从ViewPager移除。


ViewPager怎么实现滚动

Android中滚动具体有两种:一种是拖动Drag,一种是滑动Fling.ViewPager均实现了这两种滚动方式。

如果你对View的事件传递有一定的了解的话,那么你肯定会知道我们对View触摸事件的处理集中在以下三个方法中:

  • public boolean onTouchEvent(MotionEvent ev)
  • public boolean dispatchTouchEvent(MotionEvent ev)
  • public boolean onInterceptTouchEvent(MotionEvent ev) ViewGroup独有

由于ViewPager只重写了onTouchEvent,所以我们直接从onTouchEvent去分析ViewPager.我们直接从MotionEvent的几个原子操作来一一分析ViewPager的滚动。

  • public boolean onTouchEvent(MotionEvent ev) 
    先看看一些无关于触摸逻辑的部分,主要做了一些健壮性判断和参数初始化。
 @Override    public boolean onTouchEvent(MotionEvent ev) {        //这里开始直到switch部分做了了一些健壮性的判断和参数初始化        if (mFakeDragging) {return true; }        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {            return false;        }        if (mAdapter == null || mAdapter.getCount() == 0) {return false;}        if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}        mVelocityTracker.addMovement(ev);        final int action = ev.getAction();        boolean needsInvalidate = false;        switch (action & MotionEventCompat.ACTION_MASK) {            case MotionEvent.ACTION_DOWN: {                /**..省略Code..,下面会分析**/                break;            }            case MotionEvent.ACTION_MOVE:                /**..省略Code..,下面会分析**/                break;            case MotionEvent.ACTION_UP:               /**..省略Code..,下面会分析**/                break;            case MotionEvent.ACTION_CANCEL:                 /**..省略Code..,下面会分析**/                break;            case MotionEventCompat.ACTION_POINTER_DOWN: {                 /**..省略Code..,下面会分析**/                break;            }            case MotionEventCompat.ACTION_POINTER_UP:                /**..省略Code..,下面会分析**/                break;        }        //重绘,更新界面        if (needsInvalidate) {            ViewCompat.postInvalidateOnAnimation(this);        }        return true;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

接下来我们分析各个ACTION动作,这里强调一下ViewPager拖动的逻辑在ACTION_MOVE里面,滑动的逻辑在ACTION_UP里面。 
MotionEvent.ACTION_DOWN:

 case MotionEvent.ACTION_DOWN: {                //停止目前的动画                mScroller.abortAnimation();                mPopulatePending = false;                //更新Content View                populate();                //更新最新触摸坐标值,这两个值十分重要                mLastMotionX = mInitialMotionX = ev.getX();                mLastMotionY = mInitialMotionY = ev.getY();                //需要记录当前有效的触摸手指ID,用于跟踪该手指的触摸情况                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);                break;            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

ACTION_DWON里面主要是记录起始的坐标值与有效的触摸手指,为后续的动作提供数据。ViewPager支持多点触摸,所以我们接着看多触摸相关的ACTION_PONTER_DOWN和ACTION_POINTER_UP。

MotionEvent.ACTION_POINTER_DOWN&MotionEvent.ACTION_POINTER_UP

  case MotionEventCompat.ACTION_POINTER_DOWN: {                final int index = MotionEventCompat.getActionIndex(ev);                final float x = MotionEventCompat.getX(ev, index);                //上面ACTION_DOWN就是记录着两个关键参数                //这里更换了另外一个触摸手指,所以需要更新最新X坐标与触摸手指ID                mLastMotionX = x;                mActivePointerId = MotionEventCompat.getPointerId(ev, index);                break;            }            case MotionEventCompat.ACTION_POINTER_UP:                //重新设置当前触摸手指并且更新最新X坐标                onSecondaryPointerUp(ev);                mLastMotionX = MotionEventCompat.getX(ev,                        MotionEventCompat.findPointerIndex(ev, mActivePointerId));                break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

MotionEvent.ACTION_POINTER_DOWN&MotionEvent.ACTION_POINTER_UP跟ACTION_DOWN的处理逻辑其实类似,均是记录更新当前的最新的X坐标与有效的触摸手指ID。 
接下来重头戏来了,我们先看看ViewPager的拖动是怎么实现的 
MotionEvent.ACTION_MOVE

 case MotionEvent.ACTION_MOVE:                 //未进入拖动状态,不做任何处理                if (!mIsBeingDragged) {                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);                    //健壮性判断                    if (pointerIndex == -1) {                        needsInvalidate = resetTouch();                        break;                    }                    final float x = MotionEventCompat.getX(ev, pointerIndex);                    final float xDiff = Math.abs(x - mLastMotionX);                    final float y = MotionEventCompat.getY(ev, pointerIndex);                    final float yDiff = Math.abs(y - mLastMotionY);                    //根据TouchSlop判断是否是拖动动作                    if (xDiff > mTouchSlop && xDiff > yDiff) {                        //设置拖动标志位                        mIsBeingDragged = true;                        //拦截父View触摸事件                        requestParentDisallowInterceptTouchEvent(true);                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :                                mInitialMotionX - mTouchSlop;                        mLastMotionY = y;                        //设置当前的滚动状态                        setScrollState(SCROLL_STATE_DRAGGING);                        setScrollingCacheEnabled(true);                        // 再次拦截,以防万一                        ViewParent parent = getParent();                        if (parent != null) {                            parent.requestDisallowInterceptTouchEvent(true);                        }                    }                }                //如果上面判别为拖动,这里则会执行拖动的实现方法                if (mIsBeingDragged) {                    final int activePointerIndex = MotionEventCompat.findPointerIndex(                            ev, mActivePointerId);                    final float x = MotionEventCompat.getX(ev, activePointerIndex);                    //拖动的实现在此,看下面分析                    needsInvalidate |= performDrag(x);                }                break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

我们接着看拖动的实现方法,performDrag(float)方法。

  private boolean performDrag(float x) {        boolean needsInvalidate = false;        final float deltaX = mLastMotionX - x;        mLastMotionX = x;        float oldScrollX = getScrollX();        float scrollX = oldScrollX + deltaX;        /**..省略Code..,scrollX的具体计算逻辑**/        // 更新最新X坐标        mLastMotionX += scrollX - (int) scrollX;        //这里就执行了拖动,这个方法就是十分常用的滚动方法        scrollTo((int) scrollX, getScrollY());        /**pageScrolled函数主要做了两件事:            1、对Decor View 进行同步拖动            2、如果ViewPager自定义了页面动画,则会对自定义的过度动画进行处理        **/        pageScrolled((int) scrollX);        return needsInvalidate;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

由此可见ViewPager最终通过scrollTo来实现拖动。我们接着看滑动实现。 
MotionEvent.ACTION_UP 
由于ACTION_UP较为复杂,所以我们先大致说下ACTION_UP做的事情: 
1、确定最后滑动到的页面。我们在使用ViewPager时候,我们会发现如果拖动的页面距离不够,它会弹回原页面,逻辑就是在这里。 
2、更新Content View 
3、滑动到相应的页面,如果是滑动效果,则通过Scroller滑动,否则通过scrollTo滑动。 
下面我们看具体代码。

 case MotionEvent.ACTION_UP:                // 如果是拖动状态才会去执行滑动                if (mIsBeingDragged) {                    //这里到detemineTargetPager函数之前均是计算滑动所需要的参数                    /** 包括currentPage-->当前页面                           pageOffset-->页面偏移量,                           initalVelocity-->X轴滑动的速度                           totalDelta-->与开始滑动时候相比较的滑动偏移量                     **/                    final VelocityTracker velocityTracker = mVelocityTracker;                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(                            velocityTracker, mActivePointerId);                    mPopulatePending = true;                    final int width = getClientWidth();                    final int scrollX = getScrollX();                    final ItemInfo ii = infoForCurrentScrollPosition();                    final int currentPage = ii.position;                    final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;                    final int activePointerIndex =                            MotionEventCompat.findPointerIndex(ev, mActivePointerId);                    final float x = MotionEventCompat.getX(ev, activePointerIndex);                    final int totalDelta = (int) (x - mInitialMotionX);                    //确认最后滑动停止的目标页面                    int nextPage int =determineTargetPage(currentPage,pageOffset,                    //滑动实现的方法,下面具体分析                    setCurrentItemInternal(nextPage, true, true, initialVelocity);                    //停止拖动并且,重新初始化部分重要变量,包括触摸手指等。                    mActivePointerId = INVALID_POINTER;                    endDrag();                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();                }                break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

我们接着看滑动实现的具体方法,setCurrentItemInternal(int,boolean ,boolean,int)方法。

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {        /**..省略Code...,健壮性判断**/        if (item < 0) {            item = 0;        } else if (item >= mAdapter.getCount()) {            item = mAdapter.getCount() - 1;        }        //这里是针对跳跃性页面的处理,例如我从0页面直接滑动到4页面。        final int pageLimit = mOffscreenPageLimit;        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {            for (int i=0; i<mItems.size(); i++) {                mItems.get(i).scrolling = true;            }        }        final boolean dispatchSelected = mCurItem != item;        if (mFirstLayout) {            mCurItem = item;            /**..省略Code...,相应回调调用,自行阅读源码**/            requestLayout();        } else {            //更新Content View            populate(item);            //通过Scroller执行平滑滚动,或者通过scroll直接滚动,具体看下面分析            scrollToItem(item, smoothScroll, velocity, dispatchSelected);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

最终执行滑动的方法是scrollToItem(int , boolean , int ,boolean ) 。话不多说,直接上代码。

private void scrollToItem(int item, boolean smoothScroll, int velocity,            boolean dispatchSelected) {        final ItemInfo curInfo = infoForPosition(item);        //滚动偏移量计算        int destX = 0;        if (curInfo != null) {            final int width = getClientWidth();            destX = (int) (width * Math.max(mFirstOffset,                    Math.min(curInfo.offset, mLastOffset)));        }        //smoothScroll决定是否调用Scroller平滑的滑动        if (smoothScroll) {            //调用Scoller平滑滑动            smoothScrollTo(destX, 0, velocity);            //这里就是我们经常监听的PagerSelected方法            if (dispatchSelected && mOnPageChangeListener != null) {                mOnPageChangeListener.onPageSelected(item);            }            if (dispatchSelected && mInternalPageChangeListener != null) {                mInternalPageChangeListener.onPageSelected(item);            }        } else {            //首先调用PagerSelected回调方法            if (dispatchSelected && mOnPageChangeListener != null) {                mOnPageChangeListener.onPageSelected(item);            }            if (dispatchSelected && mInternalPageChangeListener != null) {                mInternalPageChangeListener.onPageSelected(item);            }            //通过scrollTo滑动页面            completeScroll(false);            scrollTo(destX, 0);            //这个方法在ACTION_MOVE里面亦有作说明,这里不再重复            pageScrolled(destX);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

ViewPager的整个滚动实现过程就是这样了,这里再做简要的总结: 
MotionEvent.ACTION_DOWN&MotionEvent.POINTER_DOWN&MotionEvent.POINTER_UP:主要更新了当前的触摸手指ID和最新的X坐标。 
MotionEvent.ACTION_UP:处理滑动。 
MotionEvent.ACTION_MOVE:处理拖动。


PagerAdapter与ViewPager的交互

ViewPager需要PagerAdapter才能够实现相应的功能,而我们创建PagerAdapter的时候需要重写一些方法,才能使得ViewPager正常运作,所以这里我们这里主要看看这些方法与ViewPager的交互:

  • public Object instantiateItem(ViewGroup,int)
  • public void destroyItem(ViewGroup, int, Object)
  • public abstract boolean isViewFromObject(View,Object)
  • public abstract int getCount()

总共4个方法来分析PagerAdapter与ViewPager的交互。

instantiateItem(ViewGroup,int) 
instantiateItem(ViewGroup,int)方法在ViewPager.addNewItem(int, int)中被调用,主要作用是创建一个页面的Content View。这里再强调一遍,在重写这个方法时候,需要主动将该Content View添加到ViewPager里面去,因为ViewPager没有主动添加该Content View为其Child View。

destroyItem(ViewGroup, int, Object) 
相应的ViewPager不会主动的添加Content View,也不会主动的移除Content View,所以亦需要在重写该方法的时候主动从ViewPager中移除该Content View。在ViewPager以下方法会调用destroyItem(ViewGroup, int, Object):

  • ViewPager.setAdapter().这里调用该方法去销毁旧Adapter的View
  • ViewPager.dataSetChanged(),这里调用该方法去销毁过期数据的View
  • ViewPager.populate().ViewPager有一个ViewPager会保持一定的页面数,所以这里要把不再持有的页面以及相应的Content View销毁。

isViewFromObject(View,Object) 
在使用ViewPager的时候,我曾试过未重写该方法,导致ViewPager运行不正常,所以这里我们看看isViewFromObject(View,Object)对ViewPager会有什么影响。isViewFromObject(View,Object)在ViewPager.infoForChild(View)中被调用到。所以我们分析下ViewPager.infoForChild(View)方法。

    // 根据ViewPager Child View 去获取相应的Content View ItemInfo信息   ItemInfo infoForChild(View child) {        for (int i=0; i<mItems.size(); i++) {            ItemInfo ii = mItems.get(i);            //判断该Child View与相应的Iteminfo中持有的对象是否一致            if (mAdapter.isViewFromObject(child, ii.object)) {                return ii;            }        }        return null;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

至此谜底揭开,我们需要重写isViewFromObject(View,Object) 让ViewPager.infoForChild(View )能够正确判断某个Child View对应的ItemInfo信息。但是这里还是存在疑问,为什么这里ViewPager不写死该方法呢?

getCount() 
这个方法不用多说了,在ViewPager中用的比较广。自行阅读源码。


尾声

至此,整个ViewPager与PagerAdapter也已经初步解析完毕了,你学到东西了吗?