ViewPager源码简析

欣赏一下
听说庐山的冬季很美,一直没机会去.....我很喜欢冬季里银装素裹的大地,那么的淡雅和纯洁
功能点

API 21

  1. 测量,布局,绘制;
  2. 事件的处理机制, viewPager的主动消耗,拦截等;
  3. 页面滚动计算,手动滚动;
  4. viewPager设计带来的问题;
0. 核心变量和标记
- mItems: 已经缓存过的page, 按照page的position从小到大来排列。    
- mCurItem: 当前显示的page的position, 这是全局的。全局是针对mItems来说的.假如有5个page,
mItems存储的可能是最后的三个页面,那他缓存的第一个页面并不是系统中的第一个page,而是全局的第三个page.
- mAdapter: 动态加载子page。
- ItemInfo: page控件构建的对象,里面的position即为全局page的position。
- mOffscreenPageLimit: 离屏限制数量,默认是1,也就是除了当前page左右缓存各一个,总数是3;如果是2,那么就左右各缓存两个,总数是5。
- Scroller: 一个平滑滚动效果的计算工具类,类似的有Overscroller.他是根据起始坐标,终点坐标,以及时间这几个变量来计算不同时间的view的x, y坐标的哦,从而实现滚动计算。
1. 测量:
  • ViewPager我们一般是不会在它的内部主动添加子view的,而是通过Adapter的形式去动态注入。其实除此之外,他还可以在xml添加他的DecorView, 这种特殊的view和adapter中添加的view的测量,布局都是不一样,他一般是固定在viewPager的页面中的不像page view一样随着手势滚动,比如ViewPager的indicator这种就是DecorView。

  • onMeasure: 测量

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         //简单的一行代码告诉了我,这viewPager的大小在这里就已经确定了。
         //如果viewpager是wrap和match的结果都一样就是父容器剩下的宽高,如果是设定了dimense那
         //就是他自己的dimense宽高了。viewPager的这种设定就和通常的控件测量不一样了,他完全忽略了自己的
         //pageview自己设定的宽与高了,这种设计存在这一些缺陷.
         //比如不要轻易地将viewPager放到ScrollView中,你会发现viewpager没有高度。
         setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                              getDefaultSize(0, heightMeasureSpec));
    
         final int measuredWidth = getMeasuredWidth();
         final int maxGutterSize = measuredWidth / 10;
         mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
    
         //这是viewPager测量之后,得到的剩余可用的宽与高
         int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
         int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    
         //先测量装饰的Decor,viewager的pageview的空间是扣除Decor之后的空间的哦;
         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();
                 if (lp != null && lp.isDecor) {//如果是Decor
                     final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                     final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                     //Decor的测量规则大概是这样的,如果是top/bottom就是横向填充,宽的规格mode是
                     //EXACTLY, 规格size根据match/wrap,dimense来定。也就是如果size是
                     //match,wrap, 他们最后的规格尺寸都是一样的即viewpager的可用宽度。dimense就是
                     //设定的宽。高的规格mode则要根据layoutParams来定,如果不是wrap,那么就是
                     //EXACTLY, 是就是AT_MOST,size在match/wrap的状态下都一样的。所以呢,他这个测量
                     //原则和标准的测量行为是保持一致的。一个方向的规格在wrap/match情况下size都是相同
                     //的,只有在dimense情形下不同。
                     int widthMode = MeasureSpec.AT_MOST;
                     int heightMode = MeasureSpec.AT_MOST;
                     boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                     boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
    
                     if (consumeVertical) {
                         widthMode = MeasureSpec.EXACTLY;
                     } else if (consumeHorizontal) {
                         heightMode = MeasureSpec.EXACTLY;
                     }
    
                     int widthSize = childWidthSize;
                     int heightSize = childHeightSize;
                     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;
                         }
                     }
                     final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                     final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                     //得出了Decor的测量规格之后,就可以对Decor进行测量啦;
                     child.measure(widthSpec, heightSpec);
    
                     if (consumeVertical) {
                         //剩余的高就是page view的高度
                         childHeightSize -= child.getMeasuredHeight();
                     } else if (consumeHorizontal) {
                         //剩余的宽就是page view的宽
                         childWidthSize -= child.getMeasuredWidth();
                     }
                 }
             }
         }
    
         //看到了,这就是page view的测量规格,这里已经确定了,他和具体的page view所设定的尺寸
         //没有半毛钱的关系,全靠viewpager除去Decor之后剩余宽,高决定。
         mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
         mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
    
         // Make sure we have created all fragments that we need to have shown.
         mInLayout = true;
         //这里是再次确定下要创建page view;
         populate();
         mInLayout = false;
    
         // Page views next.
         size = getChildCount();
         for (int i = 0; i < size; ++i) {
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
                                  + ": " + mChildWidthMeasureSpec);
    
               final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                 if (lp == null || !lp.isDecor) {
                     //对于pageView的测量啊,我们要计算一下页面的宽度因子,这个是0-1.0之间,1是全部的
                     //宽,0.5是一半这样子....
                     final int widthSpec = MeasureSpec.makeMeasureSpec(
                         (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                     //测量子page啦;
                     child.measure(widthSpec, mChildHeightMeasureSpec);
                 }
             }
         }
     }
    
    
    • 总结一下, 在测量的时候,一开始是没有子page view的,所以需要调用populate来创建和加载子page view, 然后才能测量子page。所以测量的功能大体分为三步骤:一先测量ViewPager大小, 二加载子page, 三测量子page。
  • populate:创建和销毁page view的核心方法

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        //新的page的position和老的不同,那么将新赋值给mCurItem
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            //记录老的position对应的page, 这些都缓存在了mItems中呢。
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
    
        if (mAdapter == null) {//如果没设置adapter, 那么就没法创建和加载子page啦,简单地排下Decor
            //顺序,然后跳过啦.
            sortChildDrawingOrder();
            return;
        }
    
        //这个是在当用户抬起的手指的时候,page还在计算滚动,我们不去创建和更改子page,为了安全起见。跳过啦。
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }
    
        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }
      
        //这是adapter的一个回调,用来告诉外界,已经开始加载子page了。
        mAdapter.startUpdate(this);
    
        
        final int pageLimit = mOffscreenPageLimit;
        //计算出最左端的全局position,最小肯定是0啦;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        //计算出最右边的全局positon, 最大肯定是N-1;
        final int endPos = Math.min(N-1, mCurItem + pageLimit);
      //这是保证当更新了adapter的数据之后,你要手动地去notifyDataSetChanged,否则数据不会更新;
        if (N != mExpectedAdapterCount) {
            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());
        }
    
        // curIndex不是page对应的position, 而是items集合中存储的位置,这个要和mCurItem区分开来哦
        int curIndex = -1;
        //curItem,当前要display的page.
        ItemInfo curItem = null;
        //在mItems寻找当前display的page
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {//如果我写的话肯定就是直接判等,但是没这样好,这样大于的
                //时候会立即终止遍历没必要啦。如果都小于mCurItem就在最后一个位置加上新的item.
                //算是效率上的一次小优化吧,值得学习
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }
    
        //只要当前curItem为null, page view设置了数量,创建新的item,添加到mItems集合中相应的位置中去呢;
        //这种一般是在第一次创建的时候才有的,后面就不会走的了....
        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }
    
        //接下来就是根据当前的page来左右去增删page了,这里也是vp的核心思路啦
        if (curItem != null) {
            //这个是用来累计左边的item的所处的宽度;
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            //这是一个左边的限定因子,用来决定最左边可以使用的宽度是多少,以决定怎么缓存page
            //如果是widthFactor是1,那么左边因子就是1(忽略padding),也就是左边至少能缓存一个page,
            //如果widthFactor是0.5,那么左边因子就是1.5, 也就是左边至少可以缓存3个page
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
            2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            //从当前的page前一个开始往左遍历,在全局的position为0停下来,这样当当前page是0时候,就不会再
            //浪费时间往前去排查了。
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //除了限定因子,还有一个我们设置的mOffscreenPageLimit哦
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    //当满足了限定,并且位置是小于了最左边的position,就需要destory了;
                    //如果查到前面有null了,说明这个位置已经destory过了。就不需要去destory了,可以停下
                    //了。
                    if (ii == null) {
                        break;
                    }
                    //
                    if (pos == ii.position && !ii.scrolling) {
                        //缓存中清除
                        mItems.remove(itemIndex);
                        //从viewPager中清除子page
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                  " view: " + ((View) ii.object));
                        }
                        //因为要往前遍历去destory啦!保证找到一个为null的page.
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                    //如果缓存中的该page正好是需要的page
                } else if (ii != null && pos == ii.position) {
                    //累计一个widthFactor
                    extraWidthLeft += ii.widthFactor;
                    //在缓存中向前查page,
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {//如果缓存中没有需要的page,那么就要创建了哦
                    //没有需要的page是一般因为itemIndex为-1,当前缓存的最左边的就是当前page,所以需要
                    //在0位置上再添加一个page.也还有可能是不符合的page,那么也要添加一个page,因此要在
                    //itemIndex偏移一个位置。
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    //当前page在mItems位置增加1
                    curIndex++;
                    //取出当前不符合的page遍历,下一次他可能就需要destory了。
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
    
            float extraWidthRight = curItem.widthFactor;
            //右边的一个缓存page
            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++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                      " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        //左边要偏移一个位置,这里是不需要的
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }
            //这是最后一个难度计算了;快结束了.....
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }
    
      ......略
            
        //告诉外面当前display的page是谁。
        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    
        //到这里,添加页面删除页面的动作就结束了啦。后面的东西和添加删除page关系不是太大;
        mAdapter.finishUpdate(this);
    
        // 这里是用来设定child的布局参数的,因为child的布局参数是源自于pageadpter的设定的;所以在读取了
        //adapter内容之后,这里要把他的widthFactor和position给到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;
                }
            }
        }
        //这里将缓存的所有view排好绘制顺序,Decor装饰元素是最后绘的.
        sortChildDrawingOrder();
      //如果viewPager有焦点,必须将焦点view放在当前显示的page的结构树上;
        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(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }
    
    
    • 总结一下下:populate的实现比较繁琐略带复杂,但是他的目的是很单纯的,就是在初次加载page或者滑动viewpager的时候在布局容器中加载对应的子page, 同时删除超过限定位置的page,以达到内存的优化啦。比如我们限定的mOffscreenPageLimit是1, 那么内存中缓存的就是3个page, 我们会计算出当前page的左右两个缓存起来的,其他的页面删除掉。随着页面的滚动,动态更新缓存内容page.
  • addNewItem: 添加子Item元素

    //创建新的item到mItems集合中去;position是page在所有的page中对应的位置,全局。index是在mItems中缓存的位置。
    ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        //看到没,这里是调用我们复写adapter的instantiateItem来创建子page的哦;
        ii.object = mAdapter.instantiateItem(this, position);
        //也是调用我们的adapter来加载宽度因子
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }
    
    • calculatePageOffsets:计算各个page的offset的偏移量。
     
    //curItem-当前display的page, curIndex-他在mItems中的位置, oldInfo上一次display的page
    
    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
         final int N = mAdapter.getCount();
         final int width = getClientWidth();
         final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
         // Fix up offsets for later layout.
         if (oldCurInfo != null) {
             final int oldCurPosition = oldCurInfo.position;
             //其实下面的逻辑是计算当前的page和原来显示的page之间的page的offset偏移量
             // 如果当前是向左侧滑动
             if (oldCurPosition < curItem.position) {
                 int itemIndex = 0;
                 ItemInfo ii = null;
                 //当前page的offset是左边一个page的offset+他的宽度因子+margin因子
                 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                 for (int pos = oldCurPosition + 1;
                      pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                     ii = mItems.get(itemIndex);
                     while (pos > ii.position && itemIndex < mItems.size() - 1) {
                         itemIndex++;
                         ii = mItems.get(itemIndex);
                     }
                     while (pos < ii.position) {
                         // We don't have an item populated for this,
                         // ask the adapter for an offset.
                         offset += mAdapter.getPageWidth(pos) + marginOffset;
                         pos++;
                     }
                     
                     ii.offset = offset;
                     //下一个page的offset同样累加上当前page的宽度因子和margin因子
                     offset += ii.widthFactor + marginOffset;
                 }
                 //如果是向右边滑动
             } else if (oldCurPosition > curItem.position) {
                 int itemIndex = mItems.size() - 1;
                 ItemInfo ii = null;
                 float offset = oldCurInfo.offset;
                 for (int pos = oldCurPosition - 1;
                      pos >= curItem.position && itemIndex >= 0; pos--) {
                     ii = mItems.get(itemIndex);
                     while (pos < ii.position && itemIndex > 0) {
                         itemIndex--;
                         ii = mItems.get(itemIndex);
                     }
                     while (pos > ii.position) {
                         // We don't have an item populated for this,
                         // ask the adapter for an offset.
                         offset -= mAdapter.getPageWidth(pos) + marginOffset;
                         pos--;
                     }
                     //当前page的offset = 后一个page的offset - 当前page的width因子- margin因子
                     offset -= ii.widthFactor + marginOffset;
                     ii.offset = offset;
                 }
             }
         }
    
         //接下来计算所有的缓存的page的偏移因子;根据前面的原则,难度也不大。
        // 除此之外,还计算了第一个缓存的page的偏移因子,最后一个page的偏移因子。
         final int itemCount = mItems.size();
        
         float offset = curItem.offset;
         int pos = curItem.position - 1;
         mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
         mLastOffset = curItem.position == N - 1 ?
             curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
         // Previous pages
         for (int i = curIndex - 1; i >= 0; i--, pos--) {
             final ItemInfo ii = mItems.get(i);
             while (pos > ii.position) {
                 offset -= mAdapter.getPageWidth(pos--) + marginOffset;
             }
             offset -= ii.widthFactor + marginOffset;
             ii.offset = offset;
             if (ii.position == 0) mFirstOffset = offset;
         }
         offset = curItem.offset + curItem.widthFactor + marginOffset;
         pos = curItem.position + 1;
         // Next pages
         for (int i = curIndex + 1; i < itemCount; i++, pos++) {
             final ItemInfo ii = mItems.get(i);
             while (pos < ii.position) {
                 offset += mAdapter.getPageWidth(pos++) + marginOffset;
             }
             if (ii.position == N - 1) {
                 mLastOffset = offset + ii.widthFactor - 1;
             }
             ii.offset = offset;
             offset += ii.widthFactor + marginOffset;
         }
    
         mNeedCalculatePageOffsets = false;
     }
    
    • 简单总结一下, page的offset计算也是挺繁琐的,这个玩意是来干嘛的, 有什么用呢? 它其实是用来布局子page元素的,定位每个page的位置,每个page的定位都和前面的page息息相关,这里用每个page的offset来标识。接下来看viewPager是如何给子page来布局, 就会明白这个offset的实际用途呢。
2. ViewPager的布局过程:
  • onLayout: 布局ViewPager的子page以及装饰的Decor.如title

    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;
    
        //先计算Decor这类的view的位置,这种view一般都是固定的,不会随之viewPager去移动的,
        //这种使用的不多,略吧....
        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) {//如果是Decor
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    switch (hgrav) {//如果是垂直填充
                        default:
                            childLeft = paddingLeft;
                            break;
                        case Gravity.LEFT:
                            childLeft = paddingLeft;
                         
                            paddingLeft += child.getMeasuredWidth();
                            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;
                    }
                    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;
                    }
                    //累计scrollx,可以看到,随着scroll移动,childLeft的位置是会跟着移动,以达到
                    //Decor保持在屏幕原来的位置;
                    childLeft += scrollX;
                    child.layout(childLeft, childTop,
                                 childLeft + child.getMeasuredWidth(),
                                 childTop + child.getMeasuredHeight());
                    decorCount++;
                }
            }
        }
    
       
        //如果没有Decor就是除去viewpager自己的左右padding,这个宽度就是child的宽度啦。
        final int childWidth = width - paddingLeft - paddingRight;
       
         //真正开始布局我们的page了;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                //从缓存中找到对应的view.因为有offset的数值呀,child中没有哦;
                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                    //看到了吗,offset乘以childWidth,来计算当前page的偏移量。
                    int loff = (int) (childWidth * ii.offset);
                    //每个page的left等于paddingleft + 自己的偏移量
                    int childLeft = paddingLeft + loff;
                    int childTop = paddingTop;
                    //当在第一次Populate时候,添加的子page,那个时候创建的page添加进去的page
                    //的needsMeasure是true.
                    if (lp.needsMeasure) {
                        // This was added during layout and needs measurement.
                        // Do it now that we know what we're working with.
                        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());
                    //根据他的left, top,然后测量的宽高就可以给page布局啦
                    child.layout(childLeft, childTop,
                                 childLeft + child.getMeasuredWidth(),
                                 childTop + child.getMeasuredHeight());
                }
            }
        }
        
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;
      //第一次布局会在这里滚动到指定位置;
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        mFirstLayout = false;
    }
    
    
    
    • 总结一下: 布局其实就是利用前面计算的page偏移量来和page的测量宽度来布局子page的位置哦。这里说下偏移量,比如第一个page的偏移量是0, 那么第二个page的偏移量就是第一个page的width + margiin , 后面的page就这样累计叠加。布局里面虽然有测量,但我认为这只是一个安全措施,第一次应该已经实现了对子page的测量了。这里的测量结果和前面应该是一致的。
3. 绘制的地方
  • onDraw:主要是绘制view本身,因为Viewpager本身并没有什么东西,他的子view由子child本身绘制。但是当我们设置了marginDrawable的时候,这个drawable就要由我们的ViewPager来绘制啦,我们看看他的实现。
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 存在着margin,并且设定了drawable.
    if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 
        && mAdapter != null) {
        final int scrollX = getScrollX();
        final int width = getWidth();
        // 计算margin与viewager的宽度的比例
        final float marginOffset = (float) mPageMargin / width;
        int itemIndex = 0;
        // 取出缓存的第一个子View.
        ItemInfo ii = mItems.get(0);
        float offset = ii.offset;
        final int itemCount = mItems.size();
        final int firstPos = ii.position;
        final int lastPos = mItems.get(itemCount - 1).position;
        //遍历缓存中所有的view.
        for (int pos = firstPos; pos < lastPos; pos++) {
            // 这个写法其实有点不好看,意思就是不停地从mItems缓存中取出新的View.
            while (pos > ii.position && itemIndex < itemCount) {
                ii = mItems.get(++itemIndex);
            }

            float drawAt;
            //通过view的宽度因子和左边的便宜来计算marginDrawable绘制的开始位置;
            if (pos == ii.position) {
                drawAt = (ii.offset + ii.widthFactor) * width;
                offset = ii.offset + ii.widthFactor + marginOffset;
            } else {
                float widthFactor = mAdapter.getPageWidth(pos);
                drawAt = (offset + widthFactor) * width;
                offset += widthFactor + marginOffset;
            }

            if (drawAt + mPageMargin > scrollX) {
                mMarginDrawable.setBounds((int) drawAt, mTopPageBounds,
                                          (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds);
                mMarginDrawable.draw(canvas);
            }
            //其实前面已经绘制过了,这个忽略的绘制本意却没有达到
            if (drawAt > scrollX + width) {
                break; // No more visible, no sense in continuing
            }
        }
    }
}


说完了基本的测量、布局、绘制,就要来看看viewPager的内容滚动吧,毕竟这不只是一个静态的容器.

4. 事件的拦截与触摸消耗
  • onInterceptTouchEvent: 表示在什么情况下的用户操作,会将手势操作拦截下来给到我们viewpager来用的意思。 在一次手势中如果拦截成功后面就不会再触发该方法,如果没有拦截成功会不停地调用该方法来检测拦截策略.

    public boolean onInterceptTouchEvent(MotionEvent ev) {
      
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        //4.1 up,cancel不拦截;
      //在View的拦截机制中啊, 如果发生了拦截,那么当次手势是不会再触发onInterceptTouchEvent啦
        //来到这里,说明down,move事件都没有发生过拦截,这里cacel,up自然不要拦截啦,
        //其次这里主要做了一些viewpager任务清理工作.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the drag.
            if (DEBUG) Log.v(TAG, "Intercept done!");
            //清理工作;
            mIsBeingDragged = false;
            mIsUnableToDrag = false;
            mActivePointerId = INVALID_POINTER;
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            //down, move没有拦截,这里自然不会拦截。
            return false;
        }
      
        //4.2, 如果是move事件,
        if (action != MotionEvent.ACTION_DOWN) {
                  //虽然没拦截,但是vp如果在ontouch中被认为是拖拽了。这里就拦截下来了。毕竟也不一定拦截
              //才能消耗的,如果vp没有子view或者子view不消耗,那么vp就有机会消耗啦呀。
        
                if (mIsBeingDragged) {
                    if (DEBUG) Log.v(TAG, "Intercept returning true!");
                    return true;
                }
                //如果之前是纵行滚动,当次手势是不会被Viewpager去拦截的;
                if (mIsUnableToDrag) {
                    if (DEBUG) Log.v(TAG, "Intercept returning false!");
                    return false;
                }
          }
    
        //下面看看,如果第一次来到vp中,什么时候会主动拦截
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
       
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }
    
                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                final float x = MotionEventCompat.getX(ev, pointerIndex);
                final float dx = x - mLastMotionX;
                final float xDiff = Math.abs(dx);
                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);
    
                
                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                    canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }
                //在这里检测到拦截了,条件是横向的move达到了滚到阈值,
                //并且横向滚动值达到超过了纵向滚动的两倍;
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;
                    //申请父容器不要拦截vp
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                    mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                    //纵向滚动了,该次手势后面就不会让viewpager尝试拦截哦;
                    //所以识别到了纵行滚动,该次就不会尝试viewPager拦截事件了;
                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                    //看这里;
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }
    
            case MotionEvent.ACTION_DOWN: {//down手势会拦截吗?会的啊
                /*
                     * Remember location of down touch.
                     * ACTION_DOWN always refers to pointer index 0.
                     */
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                //down说明是一次新的手势啦,要清除掉请面的纵向滚动标记;
                mIsUnableToDrag = false;
    
                mScroller.computeScrollOffset();
                //当前viewPager还在滚动没停下来,还没靠边,down手势下来了,就要拦截呢。
                if (mScrollState == SCROLL_STATE_SETTLING &&
                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough){
                    // 终止滚动
                    mScroller.abortAnimation();
                    mPopulatePending = false;
                    //计算页面
                    populate();
                    //当前要拦截了
                    mIsBeingDragged = true;
                    //请求父容器不要拦截。这里可以看出vp后面来消耗滚动事件,因此就让他的父容器不要
                    //拦截后续的move事件,让他们能顺利地来到vp中.
                    requestParentDisallowInterceptTouchEvent(true);
                    //装态为dragging
                    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);
    
        //vp认为是否是拖拽,是否要拦截下来
        return mIsBeingDragged;
    }
    
    
    
    • 总结一下,啥情况下会拦截呢, 我认为主要有以下三种情况:

      1. 在down事件的时候,一般情况呢,view体系的拦截策略是不应该在down中设定的,因为在down事件中拦截的话,后续子view请求父容器不要拦截是无效的, 这样就限定了子view的功能了。但是我们的ViewPager中却在这拦截了: 如果当前还在滚动状态并且还没靠边,手势down来了, 那么就要拦截下来,这个时候就vp就想自己使用后面的move, up事件了,而且子view也不可能在档次手势中有机会使用了。

      2. 在move事件时候, 如果move达到了滚到阈值,并且横向滚动值达到超过了纵向滚动的两倍,就会将事件拦截下来自己使用了。
      3. 如果vp的子view不消耗相应滚动, 在vp的onTouchEvent中消耗了滚动事件,并且认为是横向拖拽那么这里就会直接拦截下来,不做多余地判断;

  • onTouchEvent: Viewpager对滚动事件的消耗,主要逻辑是处理页面的滚动,滚动计算和发起都在这里面;

    //当事件由vp消耗有两种可能,其一是被vp拦截,其二vp的子view不能消耗对应的事件。
    public boolean onTouchEvent(MotionEvent ev) {
        
        ......
    
         //没有子pager,那还滚动啥子哟,直接返回false;
        if (mAdapter == null || mAdapter.getCount() == 0) {
            // Nothing to present or scroll; nothing to touch.
            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: {
                //手指落下就要终止滚动
                mScroller.abortAnimation();
                mPopulatePending = false;
                //重新计算页面量
                populate();
                // 记录down位置的x, y位置;
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                break;
            }
            case MotionEvent.ACTION_MOVE://
                if (!mIsBeingDragged) {//走到这里说明并未发生拦截,且vp子view不能来识别这个
                    //down-move事件。那就上报给我们的vp来处理了,后面肯定要将mIsBeingDragged设定
                    //true表示vp自己来处理滚动事件。
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    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);
                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    //当达到了我们的阈值,横向大于纵向.那么我们就认为是拖拽,这个比拦截的条件松,
                    //毕竟我是第一个处理的,就不需要等到是纵行的两倍在认为是拖拽;
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        if (DEBUG) Log.v(TAG, "Starting drag!");
                       //vp拖拽状态,后面的move就直接来使用
                        mIsBeingDragged = true;
                        //move已经达到了vp可以识别的滚动了,那么就告诉父容器后面的滚动事件就不能拦截了
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                        mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);
    
                        // Disallow Parent Intercept, just in case
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // 少废话,这里执行拖拽动作;
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
                        ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    //当viewPager滚动page的时候就是通过performDrag来实现滚动到当前滑动的位置的;
                    needsInvalidate |= performDrag(x);
                }
                break;
            case MotionEvent.ACTION_UP://这里主要是根据vp滑动的位置来计算最后要滚到vp的哪个子page
                if (mIsBeingDragged) {//只有是vp识别的拖拽,才会计算vp最后停靠的页面。
                    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();
                    //计算出的这个item是vp显示区域最左边的page
                    final ItemInfo ii = infoForCurrentScrollPosition();
                    //在viewpager中显示的最左边的page
                    final int currentPage = ii.position;
                    //计算当前scrollX在当前page中的偏移与当前page的with的比例,来看看后面该滚动到哪一
                    //页。
                    final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
                    final int activePointerIndex =
                        MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    //从down-up过程中移动的距离
                    final int totalDelta = (int) (x - mInitialMotionX);
                    //判断即将停靠的页面
                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                                                       totalDelta);
                    //设置滚动到对应的页面;
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);
    
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                }
                break;
           
                .... 
                    
         //viewpager默认都返回true;就是说滚动事件只要来到我身上了,那么我肯定不会拒绝的,来吧!
      return true;
    }
    
    
    • infoForCurrentScrollPosition: 这个函数看得有点烦,在前面手指抬起的时候会计算出当前vp最左边界处出现的page的缓存对象,就是通过这个方法来实现的。绘个图吧:

      vp_滑翔滚动图.png

上面情形一,向右滑动,计算出来的item是page0, 它是vp左边界中显示的页面。情形二,向左滑动,计算出来的item是page1.

  • determineTargetPage:顾名思义就是计算出手指抬起后,vp将要停靠的页面; 看下实现吧。
//currentPage指的是vp最左边对应的页面哦,不是当前mCurItem哦;
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
    int targetPage;
    //这是快速滑动的判断,当速度达到了滑翔条件(view往右滑动速度为负,向左滑动速度才是正数。)
    if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
        //向左快速滑的话,就停靠在当前vp左边界的page位置。向右快速滑,就停靠在下一个页面上。
        //参照上图,向右快速滑停靠的页面是page0,向左快速滑动停靠的页面是page2
        targetPage = velocity > 0 ? currentPage : currentPage + 1;
    } else {
        //从这里看到,如果往右边滑动,truncator = 0.4f,要想选中下一个page,必须要划过下一个page
        //0.6的宽度因子哦;如果往左边滑动currentPage会小于mCurItem,那么必须也要划出来0.6因子
        //那么余下的pageOffset会小于0.4,这样家起来小于1,会跳到前面的页面;
        final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
        targetPage = (int) (currentPage + pageOffset + truncator);
    }

    if (mItems.size() > 0) {//这里是确保page都是我们缓存中的page.
        final ItemInfo firstItem = mItems.get(0);
        final ItemInfo lastItem = mItems.get(mItems.size() - 1);

        // Only let the user target pages we have items for
        targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
    }

    return targetPage;
}
5. page页面的滚动处理:
  1. 当手指慢慢滑动,页面需要跟随手指去滑动,它是由 performDrag 来负责的, 来看源代码吧:

    //参数x是将要滚动到的x坐标;
    private boolean performDrag(float x) {
        boolean needsInvalidate = false;
     //需要滚动的距离
        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;
    
        float oldScrollX = getScrollX();
        //计算最终的scrollX, vp的滚动是通过scoll内容来实现的哦;
        float scrollX = oldScrollX + deltaX;
        final int width = getClientWidth();
     //这里的firstoffset并不是指第一个全局的page,而是内存中缓存的第一个page,mLastOffset同理如此;
        float leftBound = width * mFirstOffset;
        float rightBound = width * mLastOffset;
        boolean leftAbsolute = true;
        boolean rightAbsolute = true;
    
        final ItemInfo firstItem = mItems.get(0);
        final ItemInfo lastItem = mItems.get(mItems.size() - 1);
        if (firstItem.position != 0) {//如果不是第一个全局page.
            leftAbsolute = false;//就不会绘制边缘拖拽效果
            leftBound = firstItem.offset * width;
        }
        if (lastItem.position != mAdapter.getCount() - 1) {//如果不是最后一个全局page.
            rightAbsolute = false;//就不会绘制边缘拖拽效果
            rightBound = lastItem.offset * width;
        }
    
        if (scrollX < leftBound) {
            if (leftAbsolute) {//如果到了第一个的顶边了,就要绘制拖拽边缘效果
                float over = leftBound - scrollX;
                needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
            }
            scrollX = leftBound;
        } else if (scrollX > rightBound) {
            if (rightAbsolute) {//如果到了最后一个的顶边了,就要绘制拖拽边缘效果
                float over = scrollX - rightBound;
                needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
            }
            scrollX = rightBound;
        }
        
        mLastMotionX += scrollX - (int) scrollX;
        //通过View.scollTo来滚动到指定的位置;触发之后,系统会不停地调用我们vp中重写的computeScroll
        //方法,在该方法中会调用completeScroll(true),他做了一件重要的事情,就是
        //重新计算内存中应该缓存的page,即populate方法触发。
        scrollTo((int) scrollX, getScrollY());
        //这里会不停地回调onPageScrolled,告诉使用者当前在滚动的位置是多少.....
        pageScrolled((int) scrollX);
     
        //返回数值表示是否需要重绘制,即调用vp自身的onDaw方法。从前面看到只有到达了边缘才需要重绘制,难道
        //我们滚动的时候不需要重新绘制ui吗,不符合view绘制策略呀。实际上vp的ondraw只负责marginDrawable
        //和边缘滚动效果,vp自身内容的绘制是交给View来做的,所以在边缘触发只是绘制边缘效果。其他的绘制会在
        //scrollTo中主动触发呢。
        return needsInvalidate;
    }
    
    
    • 总结一下performDrag方法吧: 当在滚动过程中,即onTouch的move中会不停地调用该方法来实现内容的滚动,它根据手势的位置计算滚动的距离,然后还会不断地去计算内存中应该重新存储哪些新的page页面。这就是他的主要目的啦......
  1. 手动设置滚动的页面或者手指抬起要停靠的页面,由 setCurrentItemInternal,setCurrentItem这类方法族来实现, 在onTouchEvent中的手指抬起的时候会有这么一段,

    //等待计算page内存页
    mPopulatePending = true;
    
    //计算抬起手指后要滚动到的页面
    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                                                       totalDelta);
    //设置滚动到对应的页面;
    setCurrentItemInternal(nextPage, true, true, initialVelocity);
    
    

    来看看setCurrentItemInternal的源码吧:

    //决定是否回调onPageSelected方法,可以看出只有不等的时候才会回调,因此
    //第一次显示page时候是不会调的哦;
    final boolean dispatchSelected = mCurItem != item;
    if (mFirstLayout) {
        mCurItem = item;
        if (dispatchSelected && mOnPageChangeListener != null) {
            mOnPageChangeListener.onPageSelected(item);
        }
        if (dispatchSelected && mInternalPageChangeListener != null) {
            mInternalPageChangeListener.onPageSelected(item);
        }
        requestLayout();
    } else {
        //重新计算page内存页面集合,但是由于前面mPopulatePending=true,up这里其实会跳过内部的计算的。
        populate(item);
        //滚动到特定的页面,这里会利用到Vp自带的Scroller去实现平滑滚动效果;
        scrollToItem(item, smoothScroll, velocity, dispatchSelected);
    }
    

    继续来看看scollToItem怎么来实现滚动页面的吧:

    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)));
        }
        if (smoothScroll) {//up手势走的是这里;
            //根据距离和初速度来实现平滑地滚动;
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected && mOnPageChangeListener != null) {
                //告诉使用者我们的变化到了哪个页面;
                mOnPageChangeListener.onPageSelected(item);
            }
            if (dispatchSelected && mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageSelected(item);
            }
        } else {//非平滑滚动
            if (dispatchSelected && mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageSelected(item);
            }
            if (dispatchSelected && mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageSelected(item);
            }
         
            completeScroll(false);
             //调用View.scrollTo来实现滚动
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }
    
    

    来吧,接着看smoothScrollTo方法, 看他怎么来实现平滑滚动:

    void smoothScrollTo(int x, int y, int velocity) {
         .......
            //如果已经滚动结束了,就设置SCROLL_STATE_IDLE状态, 然后使用populate计算内存页
            //如果还没到滚动结束点呢?
        if (dx == 0 && dy == 0) {
            completeScroll(false);
            populate();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }
     
        setScrollingCacheEnabled(true);
        //设置滚动状态SCROLL_STATE_SETTLING,表示还在自己滑动
        setScrollState(SCROLL_STATE_SETTLING);
    
        //下面就是计算慢性滑动的时间,最终的x,y坐标:
        final int width = getClientWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth *
            distanceInfluenceForSnapDuration(distanceRatio);
    
        int duration = 0;
        //根据速度来计算时间
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);
     //调用辅助来Scoller来计算不同时间的坐标
        mScroller.startScroll(sx, sy, dx, dy, duration);
        //发命令给系统做重绘制操作,系统接着会调用computeScroll方法,来根据滚动位置来滑动内容到指定位置;
        ViewCompat.postInvalidateOnAnimation(this);
    }
    
    

    来吧,来看看ViewPager重写的computeScroll方法;

        public void computeScroll() {
            //当是我们的滚动Scroller来负责计算,这里如果还没有滚动结束
            if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
                int oldX = getScrollX();
                int oldY = getScrollY();
                int x = mScroller.getCurrX();
                int y = mScroller.getCurrY();
             //滚动到指定的位置
                if (oldX != x || oldY != y) {
                    scrollTo(x, y);
                    if (!pageScrolled(x)) {
                        mScroller.abortAnimation();
                        scrollTo(0, y);
                    }
                }
    
                // 执行重新绘制操作,这里保证边缘效果能有机会绘制,vp的滚动位置绘制由scrollTo
                //自己去负责的;
                ViewCompat.postInvalidateOnAnimation(this);
                return;
            }
    
            // 如果滚动结束了,那么要干什么呢?
            completeScroll(true);
        }
    

    继续,快结束了, completeScroll:

    private void completeScroll(boolean postEvents) {
        
        //如果是还在滚动状态,就要计算page内存内容啦;
        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
    
        .......
        
        mPopulatePending = false;
        for (int i=0; i<mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            if (ii.scrolling) {
                needPopulate = true;
                ii.scrolling = false;
            }
        }
        //这下面两个,一个是触发重绘,一个不是,但是都要执行mEndScrollRunnable,这个就是
        //调用我们的populate大法了,真不容易。
        if (needPopulate) {
            if (postEvents) {
                ViewCompat.postOnAnimation(this, mEndScrollRunnable);
            } else {
                mEndScrollRunnable.run();
            }
        }
    }
    
    

    看看mEndScrollRunnable实现,在前面手指抬起的时候,我们其实是没有计算内存中的page页的,有一个mPopulatePending状态跳过了实际计算,所以在最后页面滚动结束的时候来一次最终的计算,就是在这里了。

    private final Runnable mEndScrollRunnable = new Runnable() {
        public void run() {
            //设置SCROLL_STATE_IDLE状态
            setScrollState(SCROLL_STATE_IDLE);
            //计算内存中的page缓存内容;
            populate();
        }
    };
    
    
6. 存在的问题
  1. 当同时设置viewpager的padding和page item之间的margin, page的marginDrawable会绘制在错误的地方,他累计了对应的对应的padding,这是错误的计算;

2. 在ScrollView中直接使用viewager,宽高不生效。原因是ScrollView给子view的测量规格模式是UNSPECIFIED,而我们的Viewpager测量又是setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), etDefaultSize(0, heightMeasureSpec))组合。解决也不是很难,只不过要针对不同的模式进行自定义测量策略,后面如果有时间,综合写一下系统控件各种测量存在的问题吧....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343