Android ViewPager源码★★★★★

1.ViewPager

ViewPager是一个允许用户左右翻转数据页的布局管理器,具有出色的缓存机制。

ViewPager继承自ViewGroup:

public class ViewPager extends ViewGroup {}

 

ViewPager有一个内部类ItemInfo,它包含了一个页面的基本信息,调用Adapter的instantiateItem方法时,在ViewPager内部就会创建这个类的对象,结构如下:

static class ItemInfo { 

    Object object; //页卡展示的页卡对象,也就是instantiateItem方法返回的对象

    int position; // 页卡下标

    boolean scrolling; // 是否正在滑动

    float widthFactor; // 当前页面宽度和ViewPager宽度的比例[0-1](默认是1,可通过重写adapter的getPageWidth(int position) 方法自定义页卡宽度)这个值可以设置一个屏幕显示多少个页面

    float offset; //当前页面在所有已加载的页面中的索引(用于页面布局)

}

 

2.ViewPager缓存处理

通常在布局文件中使用ViewPager,当Activity中setContentView方法将Xml布局设置给视图的时候,ViewPager会被初始化,按照如下方法进行初始化:

ViewPager构造方法 -> initViewPager -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> draw -> onDraw

注意:以上方法有的不止调用一次,调用整体顺序按照上述步骤。

(1)ViewPager构造方法

public ViewPager(Context context) {

    super(context);

    initViewPager();

}

public ViewPager(Context context, AttributeSet attrs) {

    super(context, attrs);

    initViewPager();

}

ViewPager在构造方法里调用了initViewPager初始化方法。

void initViewPager() {

    setWillNotDraw(false);//重写onDraw需要调用,ViewGroup默认true 

   …

}

在初始化方法中调用了setWillNotDraw()方法,设置为false(ViewGroup中这个变量默认是true),意味着ViewPager会执行draw()的绘制方法。

初始化完成后还没有粘贴到真正的窗口Window上,所以之后会回调onAttachedToWindow方法。这个方法只是重置变量mFirstLayout。

protected void onAttachedToWindow() {

    super.onAttachedToWindow();

    mFirstLayout = true;

}

mFirstLayout变量是一个标记位,对是否第一次执行布局操作layout进行标记。这个变量在布局方法、滚动判断和设置适配器时会用到。

接下来就是View绘制三部曲onMesure、onLayout、onDraw,由于打开了允许draw调用,所以在onDraw前还会调用draw方法。

(2)onMeasure

onMeasure方法主要做了四件事:

①对整个ViewPager的大小进行设置。设置的大小为父类传递过来的大小,也就是剩余的空间。

②测量DecorView,并对其设置大小。使用ViewPager.DecorView注释的视图被视为ViewPager“装饰”的一部分。

③通过populate()方法确保要显示的fragment已经被创建好了。

④测量Adapter的所有View,也就是每个Item,并设置其大小。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //①测量ViewPager自身大小,根据布局文件设置尺寸信息,默认大小为0(与普通自定义ViewGroup不同,普通的会先去测量子view)

    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));

    final int measuredWidth=getMeasuredWidth();

    final int maxGutterSize = measuredWidth / 10;

    mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

    //ViewPager的显示区域只能显示一个view,childWidthSize和childHeightSize为一个view的宽高大小,即去除了ViewPager的内边距后的宽高

    int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();

    int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    //②对DecorView进行测量

    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) { //是DecorView

                //判断是不是设置了Gravity.LEFT或Gravity.RIGHT

                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                 //判断是不是设置了Gravity.TOP或Gravity.BOTTOM

                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;

                //如果在垂直方向上占用空间,那水平方向就是match_parent,即EXACTLY,而垂直方向上具体占用多少空间,由DecorView决定,consumeHorizontal同理

                if (consumeVertical) {

                    widthMode = MeasureSpec.EXACTLY;

                } else if (consumeHorizontal) {

                    heightMode =MeasureSpec.EXACTLY;

                }

                //DecorView宽高大小,初始化为ViewPager可视区域中页卡可用空间

                int widthSize = childWidthSize;

                int heightSize = childHeightSize;

                //如果宽度不是wrap_content,那么width的测量模式就是EXACTLY,如果宽度既不是wrap_content又不是match_parent,那说明用户在布局文件写的具体尺寸,直接将widthSize设置为这个具体尺寸

                if (lp.width != LayoutParams.WRAP_CO NTENT) {

                    widthMode = MeasureSpec.EXACTLY;

                    if (lp.width != LayoutParams.FILL_PA RENT) {

                        widthSize = lp.width;

                    }

                }

                if (lp.height != LayoutParams.WRAP_CO NTENT) {

                    heightMode =MeasureSpec.EXACTLY;

                    if (lp.height != LayoutParams.MATCH _PARENT) {

                        heightSize = lp.height;

                    }

                }

                //对DecorView进行设置长和宽

                final int widthSpec = MeasureSpec. makeMeasureSpec(widthSize, widthMode);

                final int heightSpec = MeasureSpec. makeMeasureSpec(heightSize, heightMode);

                child.measure(widthSpec, heightSpec);

                //如果DecorView占用了ViewPager的垂直方向的空间,需要将页卡的竖直方向可用的空间减去DecorView的高度,水平方向上同理

                if (consumeVertical) {

                    childHeightSize -= child.getMeasuredHeight();

                } else if (consumeHorizontal) {

                    childWidthSize -= child.getMeasuredWidth();

                }

            }

        }

    }

   //ViewPager剩余的宽度MeasureSpec

    mChildWidthMeasureSpec = MeasureSpec. makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);

    //ViewPager剩余的高度MeasureSpec

    mChildHeightMeasureSpec = MeasureSpec. makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

   //③确保要显示的fragment已经被创建好了。mInLayout标记是为了在计算排列子View的时候避免与添加和删除子View产生冲突

    mInLayout = true;

    populate(); //ViewPager原理的核心关键方法

    mInLayout = false;

    //④开始遍历设置每个item的宽高

    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) {

                //生成每个item宽的MeasureSpec。(childWidthSize * lp.widthFactor)表示当前页卡的实际宽度

                final int widthSpec = MeasureSpec. makeMeasureSpec((int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);

                //对每个item的宽高进行测量

                child.measure(widthSpec, mChildHeightMeasureSpec);

            }

        }

    }

(3)onSizeChanged

protected void onSizeChanged(int w, int h, int oldw, int oldh) {

    super.onSizeChanged(w, h, oldw, oldh);

    if (w != oldw) {

     //如果两值不同,就重新计算滚动位置

        recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);

    }

}

完成size改变的方法后,会触发onLayout执行操作,这是三部曲里边的布局方法。

(4)onLayout

onLayout主要做了三件事:

①设置Decor View的位置。先layout DecorView,它会占用一定空间,计算出四个padding值 提供给后面layout普通子View使用;

②设置普通View的位置。利用第一步得出的padding值得到一个可用的空间对子View进行布局,这里就涉及到对多个子View横向排列顺序的问题,这里就根据ItemInfo中的offset值来决定的,通过offset计算每个子View的Left值。

③将页面移动到第一个Item位置

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;

    //①设置DecorView的位置

    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) {

                final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;

                //根据水平方向上的Gravity,确定childLeft的值

                switch (hgrav) {

                    default:

                        childLeft = paddingLeft;

                        break;

                    case Gravity.LEFT:

                        childLeft = paddingLeft;

                        //累加左内边距(多个DecorView都居左边,肯定要累加啦

                        paddingLeft += child.getMeasuredWidth();

                        break;

                    case Gravity.CENTER_HORIZONTAL:

                        childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft);

                        break;

                    case Gravity.RIGHT:

                        //计算居右侧时的左边距=(viewPager可见宽-右边距-child测量宽)

                        childLeft = width - paddingRight - child.getMeasuredWidth(); 

                        //累加右内边距

                        paddingRight += child.getMeasuredWidth();

                        break;

                }

               //与上面水平方向的同理,据水平方向上的Gravity,确定childTop的值

                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());

                decorCount++; /DecorView的计数器加1

            }

        }     

    }

    //普通页面(Adapter的View)可用宽度

    final int childWidth = width - paddingLeft - paddingRight;

    //②设置非Decor View的位置

    //下面针对普通页面布局,在此onLayout之前已经得到正确的偏移量offset了

    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不是DecorView,并且子View保存在mItems中 

              if (!lp.isDecor && (ii = infoForChild(child)) != null) {

                  计算左边偏移量。loff计算页面的左面大小。第一个View的ii.offset为0,loff为0;第二个View的ii.offset为1.0,loff为childWidth * ii.offset

                  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);

                  }

                 //child调用自己的layout方法来布局自己

                  child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());

              }

          }

    }

    //mTopPageBounds全局变量,是普通View的Top(刨除掉Decor View)

    mTopPageBounds = paddingTop;

    //mBottomPageBounds全局变量,是普通View的Bottom(刨除掉Decor View) 

    mBottomPageBounds = height - paddingBottom;

    //Decor View的个数

    mDecorChildCount = decorCount;

    //③第一次布局,将页面移动到第一个Item位置

    if (mFirstLayout) {

        scrollToItem(mCurItem, false, 0, false);

    }

    mFirstLayout = false; //第一次布局变量设置为false

}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 onLayout执行完成之后,就完成了布局,测量、布局都完成之后,就可以绘制了。

(5)onDraw

绘制各个页面之间间隔和viewpager的边缘效应效果。

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

    // Draw the margin drawable between pages if needed.如有需要,在页与页之间绘制drawable

    if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {

        //获取当前X轴方向滚动偏移量

        final int scrollX = getScrollX();

        //viewPager的测量宽度

        final int width = getWidth();

        //页面间距比例

        final float marginOffset = (float) mPageMargin / width;

        int itemIndex = 0;

        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;

        //遍历元素

       for (int pos = firstPos; pos < lastPos; pos++) {

            while (pos > ii.position && itemIndex < itemCount) {

                ii = mItems.get(++itemIndex);

            }

            float drawAt;

            //计算绘制区域的left,偏移量累加用于下一个元素

            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;

            }

            //mTopPageBounds为顶部top,mBottomPageBounds为底部bottom,即普通View可用高度区间

            //如果绘制区域在可见范围内,根据计算出来的区域,绘制页面间隔drawable

            if (drawAt + mPageMargin > scrollX) {

                mMarginDrawable.setBounds( Math.round(drawAt), mTopPageBounds, Math.round(drawAt + mPageMargin), mBottomPageBounds);

                mMarginDrawable.draw(canvas);

            }

            //绘制区域超出 滚动偏移量+viewPager的宽,结束后序没意义的绘制(不可见)

            if (drawAt > scrollX + width) {

                break; // No more visible, no sense in continuing

            }

        }

    }

}

(6)setAdapter

调用ViewPager的setAdapter函数可将ViewPager与PagerAdapter关联起来。这是最重要的一部分内容,涉及ViewPager的缓存机制,预加载机制,以及加载与销毁的时机。

这个方法的主要作用就是:

①清除旧的Adapter,对已加载的item调用destroyItem

②将自身滚动到初始位置this.scrollTo(0, 0)

③设置PagerObserver: mAdapter.setViewPagerObserver(mObserver);

④调用populate()方法计算并初始化View

⑤如果设置了OnAdapterChangeListener,进行回调

下面来看源码:

①setAdapter 销毁旧的Adapter数据,用新的Adaper更新UI

public void setAdapter(PagerAdapter adapter){

    //①如果已经设置过PagerAdapter,则清除旧的Adapter,对已加载的item调用destroyItem    

    if (mAdapter != null) {  

        mAdapter.setViewPagerObserver(null);//清除数据观察者

        // 开始更新标记。告诉PagerAdapter开始更新要显示的页面。如果在自定义Adapter里覆写这个方法,就可以在ViewPager正式显示子View之前干点什么事,比如在第一次加载特别耗时的情况下,显示进度条

        mAdapter.startUpdate(this);

        //遍历销毁视图

        for (int i = 0; i < mItems.size(); i++) {

            final ItemInfo ii = mItems.get(i);

            mAdapter.destroyItem(this, ii.position, ii.object);

        }

        mAdapter.finishUpdate(this);//结束更新

        mItems.clear();//清空子View容器

        //将所有的非Decor View移除,即将页面移除。DecorView是装饰ViewPager的,默认是一旦设置了就会一直存在

        removeNonDecorViews();

        mCurItem = 0;//当前显示页面重置到第一个

        //②将自身滚动到初始位置

        scrollTo(0, 0);

    }

  //将全局变量mAdapter保存到局部变量,即保存上一次的PagerAdapter

    final PagerAdapter oldAdapter = mAdapter;

    //把新设置的adapter赋值给全局变量mAdapter

    mAdapter = adapter;

    mExpectedAdapterCount = 0;

   //如果设置的adapter为空,就忽视,并不报错

    if (mAdapter != null) {     

        //③确保观察者不为null,观察者主要是用于监视数据源的内容发生变化

        if (mObserver == null) { 

            mObserver = new PagerObserver();

        }         

        //设置适配器数据监听,用于更新视图,外部类只能通过pagerAdapter.notifyDataSetChanged方法通知ViewPager更新视图           

        mAdapter.setViewPagerObserver( mObserver); 

        //mPopulatePending标志是为了避免下面populate这个方法状态冲突

        mPopulatePending = false;

        //全局变量mFirstLayout设为局部变量,即保存上一次是否是第一次Layout 

        final boolean wasFirstLayout = mFirstLayout;

        //设定当前为第一次Layout

        mFirstLayout = true;   

        mExpectedAdapterCount = mAdapter.getCount(); //期望显示的子View的数量

        //④设置并初始化子view

        if (mRestoredCurItem >= 0) { //如果有数据需要恢复,这个用于意外恢复数据

            //如果有上次保存,mRestoredCurItem就一定不小于0,这个时候就会进入恢复流程,主要是跳转到上次保存的item位置

            mAdapter.restoreState( mRestoredAdapterState, mRestoredClassLoader);

            setCurrentItemInternal( mRestoredCurItem, false, true);

            mRestoredCurItem = -1;//标记无需再恢复

            mRestoredAdapterState = null;

            mRestoredClassLoader = null;

        } else if (!wasFirstLayout) { //如果在此之前不是第一次Layout,也就是onLayout方法已经执行过了(因为在onLayout方法中会将赋值给wasFirstLayout的全局变量mFirstLayout设置为false,标志着第一次layout的结束),这个时候会调用到核心方法populate()进行缓存计算

            //由于ViewPager并不是将所有页面作为子View,而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面),因此需要创建和销毁页面,populate主要工作就是这些

            populate();

        } else {  //重新布局(Layout)

            requestLayout();  //requestLayout引起重新的测量和布局,所以会执行onMeasure(), onLayout()方法,在onMeasure()方法中会调用populate()方法

        }

    }

    //⑤如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器,则回调OnAdapterChangeListener的onAdapterChanged函数

    if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {

        for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {

            mAdapterChangeListeners.get(i ).onAdapterChanged(this, oldAdapter, adapter);

        }

    }

}

给适配器设置PagerObserver,是用于客户端在数据改变的情况下,可以通过调用notifyDataSetChanaged方法进行数据变更。这个方法就是调用PagerObserver的onChanged方法进行,onChanged方法内部则是调用ViewPager的方法dataSetChanged,从而实现ViewPager数据变更。ViewPager里边设置Adapter,Adapter里设置了Observer,实现了双向绑定。

(7)populate()

populate直译过来是“增添数据、输入数据”。该方法是ViewPager非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁的页面,然后通过调用Adapter的instantiateItem和destroyItem两个方法初始化新页面和销毁不需要的页面。

populate()主要做了这几件事:

①根据传入的当前界面更新mItems数组,该数组存放的是对应的ItemInfo对象

②如果左面超出了缓存限制,则删除左边,如果左边需要缓存却没有缓存,则在左边创建对应的ItemInfo

③如果右面超出了缓存限制,则删除右边,如果右边需要缓存却没有缓存,则在右边创建对应的ItemInfo

④设置当前选中item

⑤如果当前ViewPager可获得焦点,把焦点传递给子View

populate有两个方法,一个带参数,一个不带参数。

void populate() {

     populate(mCurItem);

}

参数newCurrentItem表示当前需要定位显示的界面(也就是传入的Position)

void populate(int newCurrentItem) {

    ItemInfo oldCurInfo = null;

    //如果要定位显示的位置不是当前位置

    if (mCurItem != newCurrentItem) {

        //保存之前旧的ItemInfo

        oldCurInfo = infoForPosition(mCurItem);

        //更新当前视图index

        mCurItem = newCurrentItem;

    }

    if (mAdapter == null) { 

        //对子View的绘制顺序进行排序,优先绘制Decor View,再按照position从小到大排序

        sortChildDrawingOrder(); 

        return;

    }

    //如果正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建子View,直到滚动到最终位置再去创建,以免在这个期间出现差错。即若滑动未停止,则暂停populate操作

    if (mPopulatePending) {

        if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");

        sortChildDrawingOrder();

         return;

    }

    //在ViewPager没有attached到window之前,不要populate。因为如果在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突。即若视图未依附于窗口则暂停populate操作

    if (getWindowToken() == null) {

        return;

    }

    mAdapter.startUpdate(this); //开始更新

    final int pageLimit = mOffscreenPageLimit;//预加载数量

    final int startPos = Math.max(0, mCurItem - pageLimit); //startPos是缓存页面的起始页,确保起始位置大于等于0

    final int N = mAdapter.getCount();//需要显示的子View的数量,即ViewPager的所有Item

    final int endPos = Math.min(N - 1, mCurItem + pageLimit);//endPos是缓存页面的结束页,确保最后的位置小于等于数据源中数据个数-1

    if (N != mExpectedAdapterCount) {//数量不一致,说明数据发生变化,但是没有提示ViewPager更新,则抛出异常

        throw new IllegalStateException("The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged!" ;

    }

    //①下面开始定位当前获焦的页面。先从内存中定,如果mCurItem在mItems里面,就返回对应的ItemInfo,否则,就创建一个ItemInfo放到mItems里面,并赋值给curItem

     int curIndex = -1;//存放mItems数组中的位置

    ItemInfo curItem = null;

    //遍历mItems中所有对象,找到当前获焦页面

    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;

        }

    } 

    //如果curItem为空,说明mItems没有任何东西(比如第一次进来),或者说当前界面ItemInfo没有保存到mItems里面,就通过addNewItem调用instantiateItem加载当前页面

    if (curItem == null && N > 0) {

        //mCurItem是position的位置,curIndex是mCurItem所对应的ItemInfo在mItems中的位置

        curItem = addNewItem(mCurItem, curIndex);

    }

    //默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数据,则将当前页面两边都缓存用户指定的数量的页面。如果当前没有页面,则啥也不做

    if (curItem != null) {

        float extraWidthLeft = 0.f;

        // 当前视图左边的页面,即mCurItem所对应的ItemInfo在mItems中的上一个位置

        int itemIndex = curIndex - 1;

       //如果当前页面左边有页面,则将左边页面对应的ItemInfo取出;否则左边页面ItemInfo为null

        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;

        //获取显示区域的宽度,即viewPager测量宽度-内边距

        final int clientWidth = getClientWidth();

        //算出左边页面需要的宽度。注意,这里的宽度指的是实际宽度与可视区域宽度的比例,即实际宽度=leftWidthNeeded*clientWidth

        final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;

        //②遍历当前视图左边的所有页面,不包括自身。主要是删除左边不在缓存范围内的ItemInfo,或者如果左边没有缓存,在左边创建缓存

        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);

                    mAdapter.destroyItem(this, pos, ii.object);

                    itemIndex--;//由于mItems删除了一个元素,需要将索引减一

                    curIndex--;//mItems里面位置变化

                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面

                }

            } else if (ii != null && pos == ii.position) {

                // 当前位置是需要缓存的界面,并且这个位置上的页面已经存在,则将左边宽度加上当前页面的宽度

               extraWidthLeft += ii.widthFactor;

                itemIndex--;//mItems往左遍历

                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面

            } else {

                //这个位置需要缓存,并且这个位置没有页面。即该左侧元素不在内存中,则重新添加

                ii = addNewItem(pos, itemIndex + 1);

                extraWidthLeft += ii.widthFactor;//将左边宽度加上当前位置的页面

                curIndex++;//由于新加了一个元素,当前的索引号需要加一

                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii设置为当前遍历的页面的左边一个页面

            }

        }

       //③来到当前视图右侧,思路大致和左侧相同

        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++) {

                if (extraWidthRight >= rightWidthNeeded && pos > endPos) {

                    if (ii == null) {

                        break;

                    }

                    if (pos == ii.position && !ii.scrolling) {

                        mItems.remove(itemIndex);

                        mAdapter.destroyItem(this, pos, 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);

        //④设置当前选中item

        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);

    }

    //结束更新,如果是PagerAdapter则为空实现;如果是FragmentStatePagerAdapter或者FragmentPagerAdapter,则会调用FragmentTransaction的commitNowAllowingStateLoss方法提交fragemnt,进行detach或者attach之类的操作

    mAdapter.finishUpdate(this);

    // 遍历子视图,将mItems里面ItemInfo信息更新到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) {

            //没有有效的宽度,则获取内存中保存的信息给子视图的LayoutParams

            final ItemInfo ii = infoForChild(child);

            if (ii != null) {

                lp.widthFactor = ii.widthFactor;

                lp.position = ii.position;

            }

        }

    }

    //重新将子视图排序

    sortChildDrawingOrder();

    //⑤如果当前ViewPager是具有焦点的,将焦点分发给当前显示的页面

    if (hasFocus()) { 

        //找到焦点View,如果存在则获取它的信息

        View currentFocused = findFocus();

        ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;

      //如果没找到或者找到的焦点view不是当前位置,则遍历元素,如果找到对应元素则请求焦点

        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) {

                    //找到view,请求焦点

                    if (child.requestFocus( View.FOCUS_F ORWARD)){

                        break;

                    }

                }

            }

        }

    }

}

populate()方法首先会根据newCurrentItem和mOffscreenPageLimit计算要加载的page页面,计算出startPos和endPos。然后根据startPos和endPos初始化页面ItemInfo,先从缓存里面获取,如果没有就调用addNewItem方法,同时将不需要的ItemInfo移除:mItems.remove(itemIndex),并调用mAdapter.destroyItem方法。然后设置LayoutParams参数(包括position和widthFactor ),根据position排序待绘制的View列表。最后一步是获取当前显示View的焦点。

可以看出,ViewPager里面有多少界面都不会卡,因为它会不断的销毁和创建页面,默认不仅会创建当前页面,还会创建相邻的offscreenPageLimit个页面。

在整个populate中,计算缓存前,有四次对特殊情况的处理,有三次是在Adapter的startUpdate()标记方法之前,一次在其之后。这四种情况分别是:

1)mAdapter == null,用户还未设置适配器,这个时候停止处理并返回,这种情况是在ViewPager初始化的时候,在调用onMeasure()方法中,会调用populate()方法,而此时populate()方法的执行没有意义,避免浪费性能及其他错误,直接返回。

2)mPopulatePending == true 的情况,这个值默认是false,而且在setAdapter中也会重置为false,只有在以下两种情况会为true:一种是在onTouchEvent当手指离开屏幕的时候,另外一种是结束虚拟滑动endFakeDrag的时候,看这个两个方法,可知都是和滑动相关的。而且,从调用位置可以发现,正好是手指离开屏幕,或者模拟手指离开屏幕,而产生的fling到新的位置,这个时候避免出错,延迟绘制。

3)getWindowToken() == null,这个方法获得的token正是当前View粘贴到(attach)到的window的token,如果为空,则说明此时View树还未与window产生联系,主页面还未调用onResume方法。这个时候也不能计算缓存。

4)N != mExpectedAdapterCount ,上边刚刚在setAdapter中提到这个变量的赋值,是调用Adapter的getCount()方法,而N也是用相同的方法获得的,如果同一个方法获得的数据个数不同,那说明用户修改了数据源,而并未调用notifyDataSetChanged()方法来告知ViewPager同步,所以给开发者抛出了异常,从而让开发者意识到错误的原因并及时修改。

一个关键方法addNewItem(),来看源码:

ItemInfo addNewItem(int position, int index) {

   ItemInfo ii = new ItemInfo();

   ii.position = position;

   ii.object = mAdapter.instantiateItem(this, position);//在adapter中重写,新创建的对象

   ii.widthFactor = mAdapter.getPageWidth( position);

   if (index < 0 || index >= mItems.size()) {

       mItems.add(ii); //在mItems末尾依次添加

   } else {

       mItems.add(index, ii);//添加到指定位置

   }

   return ii;

}

里边有我们熟悉的方法instantiateItem,从而证明创建的子View确实就是从这个方法获得的,所以,我们在实现这个方法的时候,返回什么,他这就缓存什么。

另外还调用了getPageWidth,这个方法设置当前item的显示宽度,是个比例值。(这个宽度因子,不能设置2或者更大,否则就会显示异常,而如果设置太小的话,所有item能在一屏内显示的时候,就会发生鬼畜的现象)

刚开始,这部分可能不好理解,那我们来动态分析一下(以下示意图展示的都是mItems变化)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_16,color_FFFFFF,t_70,g_se,x_16

 第一种分析:假如现在是第一次设置Adapter,这个时候mItem除了有当前位置一个元素外,没有其他。并且,循环一次都不会执行。

按照缓存策略,默认缓存为1,可以知道这次populate之后会缓存第二个Item。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第二种分析:滑到下一页,这时候就到了第二个页面,当前的position为1,也就是第二个位置,这个时候左侧有一个item,可以进入循环,我们发现符合第二个条件,因为ii已经缓存到Items里边了,所以不为null,并且position和pos均为0,这时我们看下符合条件我们做了什么:

else if (ii != null && pos == ii.position) {

     extraWidthLeft += ii.widthFactor;

     itemIndex--;

     ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;

将widthFactor,给extraWidthLeft变量加上去,此时itemIndex为0,再自减1的话,就是-1,所以下边的ii为null。然后进入下次循环,发现循环变量为-1,不符合循环条件,退出循环。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第三种分析:然后来到第三页,这个时候的curIndex = 2,并且itemIndex = 1,循环可以执行,第一次循环pos=1,这个时候命中第二个条件,把宽度因子叠加到extraWidthLeft 上,将itemIndex自减,这个时候itemIndex为0了,并且也是可以从mItems里边获得到的;然后下一次循环pos=0,这个时候命中了第一个条件(extraWidthLeft >= leftWidthNeeded满足,并且pos < startPos也满足,startPos此时为1),所以看第一个条件里边做了些什么:

if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {

    if (ii == null) {

        break;

    }

    if (pos == ii.position && !ii.scrolling) {

        mItems.remove(itemIndex);

        mAdapter.destroyItem(this, pos, ii.object);

        itemIndex--;

        curIndex--;

        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;

    }

 } 

首先判断ii是否空,为空就退出当前循环,不为空继续向下。又一个判断,&&左边的是相等的,假设这个时候已经结束滑动了,所以&&右边的也符合,执行判断内部逻辑,首先移除了mItems中itemIndex的位置的缓存,这个时候ItemIndex为0。然后看到了另一个熟悉的方法destroyItem(),这个方法也很重要,正是实现适配器的重要方法,可以在元素销毁的时候干点什么,定义销毁策略。然后是两个变量的自减,所以这个时候curIndex为1(因为mItems进行了remove操作,导致index和position错位,为了下边右缓存计算不出现问题,需要矫正这个变化,差距是1,所以这里curIndex进行了自减),itemIndex为-1。获取到的ii为null。然后循环不符合条件了,所以循环结束。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 第四种分析:那什么时候执行第三个条件呢?这个时候在第三种分析的基础上,我们换方向,这个时候我们假设从第三页回到了第二页,这个时候由于我们之前删除了一个元素,所以我们回到之前确定curIndex的代码的地方,由于这段代码很容易让人不理解,所以这里我们不厌其烦的再copy一下:

int curIndex = -1;

ItemInfo curItem = null;

for (curIndex = 0; curIndex < mItems.size(); curIndex++) {

    final ItemInfo ii = mItems.get(curIndex);

    if (ii.position >= mCurItem) {

        if (ii.position == mCurItem) curItem = ii;

        break;

    }

}

就是这个for循环,因为我们在第三种分析的时候将本来mItems的第0个元素remove掉了,导致原来实际位置为1的元素,在mItems集合中,放到了第0个位置,所以我们此时要显示实际位置为1的item,循环的第一个curIndex为0的情况获取到的ii就会命中(他的位置position恰好和mCurItem一致,这个时候mCurItem为1)。所以局部变量在进入下边的左右缓存计算处理时的curIndex =0;

接着进入左右缓存,此时curIndex = 0,itemIndex = -1,所以准备就绪后的ii,为null,所以左边缓存的条件判断,前两个条件都将不符合,那么就会命中第三个条件,下面我们来看,最后一个条件的源码:

else {

   ii = addNewItem(pos, itemIndex + 1);

   extraWidthLeft += ii.widthFactor;

   curIndex++;

   ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;

}

由于此时itemIndex为-1,而实际上应该0,所以为了纠正,这里加了个1,刚好重新获得了在第三种分析中被销毁的实际位置是0的Item。另外还需要注意一点是,由于此时curIndex是0,而实际上应该是1,所以这里又做了一次自增调整,而下边的ii是null,此时循环变量pos是0,下次循环是-1,不满足循环条件,退出循环。这里需要提醒的是,从右往左返回时,由于mItems数组会删除前边的元素,导致错位,所以,这里在第三个条件里边做了矫正。也正因为上边的矫正,才会使得下边的右边缓存的计算才不会出现错误。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第五种分析:其实第五种分析是针对开发者在代码中调用setOffscreenPageLimit,改变缓存配置变量mOffscreenPageLimit,这个值默认是1,正如我们前面四种分析的采用的缓存策略一样,但是我们想象这样一种情况,如果我们当前显示的是第四页,也就是mCurIndex = 3,这个时候我们设置了缓存配置变量为2,这个时候mItems中应该保存有序号为1,2,3,4,5,而由于默认的策略是1,导致mItems中保存的仅是序号为2,3,4三个Item元素,这时会先触发第二个条件,从缓存中获取,然后将itemIndex自减,针对左边接下来就会触发第三个条件,导致创建元素并插入到正确的位置。而如果之后又重新设置了缓存配置变量为1,这个时候针对左边缓存又需要销毁一个元素,还是会先进入第二个条件,然后进入第一个条件,进行remove。这样我们就知道了这个setOffscreenPageLimit是如何影响到ViewPager的缓存策略的。

 

9.calculatePageOffsets

对于缓存的分析是很重要的一块,也是一个理解的难点。接下来就是计算偏移值,调用calculatePageOffsets方法。该方法主要分两大块,oldCurInfo != null和oldCurInfo == null,就是初始化和滑动的两个过程,实际的逻辑取决于mCurItem这个属性变量与newCurrentItem这个参数是否相等。

calculatePageOffsets方法主要用于计算每个页面对应ItemInfo的offset变量,这个变量用于记录当前view在所有缓存View中(包含当前显示页)的索引,用于布局的时候计算该View应该放在哪个位置。在populate方法中更新完页面数据后,会调用该方法计算所有页面的offset。

private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {

    final int N = mAdapter.getCount();

    final int width = getClientWidth();//子View的可用宽大小,即viewPager测量宽度-内边距

    final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; //mPageMargin是页面之间的间隔,marginOffset间隔比例,默认0

    --------第一部分 计算当前位置的偏移offset-----

    //根据上一次展示的页面,来确认此次当前页面的offset。只有在使用ViewPager.setCurrentItem的方法直接跳转到指定页面时才条件成立,靠滑动切换页面不会成立 

    if (oldCurInfo != null) {

        final int oldCurPosition = oldCurInfo.position;

        //根据oldItem.position与curItem.position的大小关系,来确定curItem的offset值是等于oldItem.offset加上还是减去它们之间间隔的页面(页面宽度+ marginOffset)之和

        if (oldCurPosition < curItem.position) {//滑到下一张,加上

            int itemIndex = 0;

            ItemInfo ii = null;

            //根据 old页面的offset+old页面的宽比(0f-1f)+每个页面的间隔比例 计算出old页面的下一个页面的offset值。例如假设marginOffset=0.2,widthFactor=1,一个5个页面,则每个页面的offset分别为0;1.2;2.4;3.6;4.8  

            float Offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;

           //从old页面的下一个页面开始遍历,一直到当前要展示的页面的位置 

            for(int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size;pos++) {

                ii = mItems.get(itemIndex);

                //循环,直到获取到pos当前位置的元素 

                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;//设置当前页面的offset

                offset += ii.widthFactor + marginOffset; //计算下一个页面的offset

            }

        } 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) {

                    offset -= mAdapter.getPageWidth( pos) + marginOffset;

                    pos--;

                }

            offset -= ii.widthFactor + marginOffset;

            ii.offset = offset;

        }

    }

}

    //根据当前元素再次计算缓存列表中所有元素的偏移量。初始化操作或者刷新当前页从这里开始  

    final int itemCount = mItems.size();

    float offset = curItem.offset;//当前要显示的页面的偏移量

    int pos = curItem.position - 1;//前一个页面

    mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;//如果当前要展示的页面是第0个位置,则设置mFirstOffset=curItem.offset 

    mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;//如果当前要展示的页面是最后一个位置,则设置mLastOffset =curItem.offset 

    --------第二部分 计算缓存列表中当前页面左边页面的偏移量(根据当前页面计算)----- 

    for (int i = curIndex - 1; i >= 0; i--, pos--) {

        final ItemInfo ii = mItems.get(i);

        //如果pos跟ii.position之间有间隔页面,则减去间隔偏移量 

        while(pos > ii.position) {

            offset -= mAdapter.getPageWidth(pos--) + marginOffset;

        }

        //ii元素的偏移量=它的下一个元素的偏移量-ii页面的宽比-每个页面的间隔比例 

        offset -= ii.widthFactoor + marginOffset;

        ii.offset = offset;

        //如果ii是第一个元素,则设置mFirstOffset值

        if (ii.position == 0) mFirstOffset = offset;

    }

    //令offset=当前元素的下一个元素的偏移量 

    offset = curItem.offset + curItem.widthFactor + marginOffset;

    pos = curItem.position + 1;//下一个元素的pos

    --------第三部分 计算缓存列表中当前页面右边页面的偏移量(根据当前页面计算)------

    for (int i = curIndex + 1; i < itemCount; i++, pos++) {

        final ItemInfo ii = mItems.get(i);

        while(pos < ii.position) {

            offset += mAdapter.getPageWidth(pos++) + marginOffset;

        }

        //如果ii是最后一个元素,则设置mLastOffset值 

        if (ii.position == N - 1) {

            mLastOffset = offset + ii.widthFactor - 1;

        }

        ii.offset = offset;

        offset += ii.widthFactor + marginOffset;

    }

    mNeedCalculatePageOffsets = false;

}

以上代码分了三部分,第一部分是对当前item的offset偏移量进行计算,第二部分是对当前位置的左边所有item的offset偏移量依次进行计算赋值,第三部分是对当前位置的右边所有item的offset偏移量依次进行计算赋值。

有个ViewPager属性关注下:mPageMargin 这个变量是通过setPageMargin方法设置的,默认0;

如果第一次设置或者刷新当前页,即oldCurInfo == null。这个时候当前位置不变,但是由于有一种情况是针对缓存策略的改变,所以即使当前位置不变,由于左右两边的偏移会有变化,所以需要重新计算。

这里需要注意的是,代码中出现的curIndex和pos两个变量,这里解释下:

curIndex:是针对mItems的序号,是当前要显示的item在List里对应的位置。

pos:是针对ViewPager的所有Item,是真实的位置。

只有清楚的了解以上两点,才不会被不断的while循环搞混,而while循环只是在不断矫正list的curIndex和pos的匹配。他两虽然没有关系,但是通过mItems集合获取curIndex位置的ItemInfo,这个对象的position属性记录的正是真实位置。所以这样就建立起了联系,所以curIndex位置获取到的ItemInfo的postion属性比pos变量小,那就增大curIndex或者减小pos;反之,curIndex位置获取到的ItemInfo的postion属性比pos变量大,那就减小curIndex或者增大pos,之所以可以连续处理,是因为mItems存的item虽然对不上号,但是他们是连续的。

有了这个算法的基础,来看calculatePageOffsets的源码,计算当前位置的偏移offset的时候先判断了下是向左滑,还是向右滑,这个是通过对比oldCurInfo的position和curItem的position的大小,分成了两种情况,里边都是根据上边的算法矫正之后,给当前的item赋上正确的offset的值。

然后第二部分和第三部分是计算左边右边的偏移量。其实都是直接算mItems里保存的item的偏移量,但是都是相对真实的第0个item开始计算的,虽然有可能缓存策略会把前边的回收,但相对位置还是按照有他计算的。举个栗子:如果缓存是1的话,当前位置是3,那么mItems里边,这时会只有实际位置2,3,4。这个时候0,1会回收,但是计算的实际偏移值offset,这三个分别是:2,3,4,而不是0,1,2。(这个offset是宽度倍数关系)

随后在calculatePageOffsets还需要关注两个属性变量:

private float mFirstOffset = -Float.MAX_VALUE;//用来标记是否到达了最左边

private float mLastOffset = Float.MAX_VALUE;//用来标记是否到达了最右边

这两个变量会在这个时候做矫正,而在滑动的时候做判断

mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;

mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;

只要当前显示的不是第一个或者最后一个,他们的值会和curItem的offset设置了相应的计算关系。这个时候在滑动的时候就能很快判断是否到达了边缘,只要这两个变量不是默认值,就没有到边。

之后调用mAdapter.setPrimaryItem() 方法用来设置ViewPager要显示的Item信息。

在适配器创建对象的方法是instantiateItem,但是由于ViewPager的缓存策略导致创建的并不一定是要显示的,那么适配器为了解决用户回调当前显示的信息,而且是最及时的获取,就有了setPrimaryItem这个回调方法。

(8)ViewPager.dataSetChanged()

当调用Adapter的notifyDataSetChanged时,会触发这个方法,该方法会重新计算当前页面的position,移除需要销毁的页面的ItemInfo对象,然后再调用populate方法刷新页面

//PagerAdapter

public void notifyDataSetChanged() {

    synchronized (this) {

        //这个监听器就是ViewPager内的PagerObserver,是在setAdapter的时候通过Adapter.setViewPagerObserver()传入的

        if (mViewPagerObserver != null) {

            mViewPagerObserver.onChanged();

        }

    }

    mObservable.notifyChanged();

}

//ViewPager

private class PagerObserver extends DataSetObserver {

    PagerObserver() {

    }

   @Override

    public void onChanged() {

        //调用ViewPager的dataSetChanged方法

        dataSetChanged();

    }

    @Override

    public void onInvalidated() {

        dataSetChanged();

    }

}

void dataSetChanged() {

    // This method only gets called if our observer is attached, so mAdapter is non-null.

     final int adapterCount = mAdapter.getCount();

    mExpectedAdapterCount = adapterCount;

    //是否需要刷新页面,此处如果元素个数小于缓存页数,也小于适配器元素个数,则为true

    boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && mItems.size() < adapterCount;

    int newCurrItem = mCurItem;

    boolean isUpdating = false;

    //遍历容器中的元素

    for (int i = 0; i < mItems.size(); i++) {

        final ItemInfo ii = mItems.get(i);

        // 返回元素相应位置是否发生变化的标志。POSITION_UNCHANGED = -1表示当前页面不需要更新,不用销毁;POSITION_NONE = -2需要更新,销毁。可以在初始化时为页面设置tag,在getItemPosition方法中根据tag判断仅更新当前页面视图。

        final int newPos = mAdapter.getItemPosi tion(ii.object);

        // 若返回POSITION_UNCHANGED,跳过

        if (newPos == PagerAdapter.POSITION_UNCHANGED) {

            continue;

        }

        if (newPos == PagerAdapter.POSITION_ NONE) {

            // 返回POSITION_NONE时移除元素并记录标志。这里对元素先移除,后重新加载

            mItems.remove(i);

            i--;

            if (!isUpdating) {//开始更新

                mAdapter.startUpdate(this);

                isUpdating = true;

            }

            mAdapter.destroyItem(this, ii.position, ii.object);    //销毁视图

            needPopulate = true;//设置为需要刷新页面

            //如果当前位置元素被删除,则重新选出新的当前元素位置

            if (mCurItem == ii.position) {

                // Keep the current item in the valid range

                newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));

                needPopulate = true;

            }

            continue;

        }

        //newPos不是默认的这两种的情况下,并且当前元素的position不等于它,则设置为它,需要刷新

        if (ii.position != newPos) {

            if (ii.position == mCurItem) {

                // Our current item changed position. Follow it.

                newCurrItem = newPos;

            }

            ii.position = newPos;

            needPopulate = true;

        }

    }

    if (isUpdating) {//isUpdating=true结束更新操作

        mAdapter.finishUpdate(this);

    }

    //重新排序

    Collections.sort(mItems, COMPARATOR);

    //如果需要刷新,遍历子view,重置页面宽度,在populate方法中将重新计算它们

    if (needPopulate) {

        // Reset our known page widths; populate will recompute them.

        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) {

                lp.widthFactor = 0.f;

            }

        }

        //更新UI

        setCurrentItemInternal(newCurrItem, false, true);

        //请求布局

        requestLayout();

    }

}

①循环mItems(每个page对应的ItemInfo对象),调用int newPos = mAdapter.getItemPosition方法。

②newPos等于PagerAdapter.POSITION_ UNCHANGED表示当前页面不需要更新,不用销毁;newPos等于PagerAdapter.POSITION_NONE时,需要更新,移除item,调用mAdapter.destroyItem。

③循环完成后,计算出显示页面的newCurrItem,调用setCurrentItemInternal( newCurrItem, false, true)方法更新UI(实际调用populate方法重新计算页面信息)

 

3.ViewPager的滑动控制

缓存处理是ViewPager很重要的一点,然而ViewPager不是一个静态控件,而是一个动态展示控件,所以需要了解它是如何在滑动中保证依然高效流畅的体验。

ViewPager是个ViewGroup,根据touch事件分发机制,事件会先传递到dispatchTouchEvent,该方法ViewPager没有重写,根据父类ViewGroup的dispatchTouchEvent方法可知,如果child没有主动调用requestDisallowInterceptTouchEvent,就会执行onInterceptTouchEvent(ev)方法,ViewPager重写了这个方法:

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

            final int activePointerId = mActivePointerId;//使用触摸点Id,主要是为了处理多点触摸

            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);

            //判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理。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

                requestParentDisallowInterceptTouchEv ent(true);

                //设置滚动状态

              setScrollState(SCROLL_STATE_DRA GGING);

                //保存当前位置

                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.postInvalidateOnAni mation(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) {//可以看出在down事件里,ViewPager只是对如果之前是SCROLL_STATE_SETTLING这个状态的事件进行拦截,其余的不处理

            //如果此时用户手指按下,则立马暂停滑动

                mScroller.abortAnimation();

                mPopulatePending = false;

                populate();

                mIsBeingDragged = true;

                //如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager

                requestParentDisallowInterceptTou chEvent(true);

                //设置当前状态为正在拖拽

                setScrollState(SCROLL_STATE_DRA GGING);

            } else {

                //结束滚动

                completeScroll(false);

                mIsBeingDragged = false;

            }

            break;

        } 

        case MotionEventCompat.ACTION_POINT ER_UP:

            onSecondaryPointerUp(ev);

            break;

        }

    //添加速度追踪

    if (mVelocityTracker == null) {

        mVelocityTracker = VelocityTracker.obtain();

    }

    mVelocityTracker.addMovement(ev);

    //只有在当前是拖拽切换页面时才会去拦截事件

    return mIsBeingDragged;

}

 

 

                

 

 

 

 

 

 

 

 

 

ViewPager.scrollToItem

滑动到指定页面,内部会触发OnPageChangeListener 。 

private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) {

    //拿到对应位置的元素信息

    final ItemInfo curInfo = infoForPosition(item);

    int destX = 0;

    //如果元素信息不为空,则计算偏移的目的地

    if (curInfo != null) {

        //获取子View的可用宽大小,即viewPager测量宽度-内边距

        final int width = getClientWidth();

        destX = (int) (width * Math.max( mFirstOffset, Math.min(curInfo.offset, mLastOffset)));

    }

    //平稳的滑动到目的地

    if (smoothScroll) {

        smoothScrollTo(destX, 0, velocity);

        if (dispatchSelected) {

            dispatchOnPageSelected(item);

        }

    } else {

        if (dispatchSelected) {

             dispatchOnPageSelected(item);

        }

        completeScroll(false);

         //直接滑动到目的地

        scrollTo(destX, 0);

        pageScrolled(destX);

    }

}

 

 

 

 

 

 

sortChildDrawingOrder():

private void sortChildDrawingOrder() {

    //只有在设置transform的时候才会调用

   if (mDrawingOrder != DRAW_ORDER_DEFAULT){

        if (mDrawingOrderedChildren == null) {

            mDrawingOrderedChildren = new ArrayList<View>();

        } else {

            mDrawingOrderedChildren.clear();

        }

        final int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {

            final View child = getChildAt(i);

            mDrawingOrderedChildren.add(child);

        }

        //排序,位置关系及是否是decor

      Collections.sort(mDrawingOrderedChildren, sPositionComparator);

    }

}

整个方法执行的先决条件是mDrawingOrder != DRAW_ORDER_DEFAULT,只有满足这个条件才执行方法体的逻辑,mDrawingOrder默认就是DRAW_ORDER_DEFAULT,而只有当调用了setPageTransform函数设置transform的时候,才会被赋值成DRAW_ORDER_REVERSE或者DRAW_ORDER_FORWARD,不管是哪个,都会导致条件满足。

代码很简单,依次添加子View到mDrawingOrderedChildren集合中,然后排序,所以只要知道他排序的规则是什么就行,看sPositionComparator(LayoutParams的内部类ViewPositionComparator):

static class ViewPositionComparator implements Comparator<View> {

    @Override

    public int compare(View lhs, View rhs) {

        final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();

        final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();

        if (llp.isDecor != rlp.isDecor) {

            return llp.isDecor ? 1 : -1;

        }

        return llp.position - rlp.position;

    }

}

简单的说:如果比较的两个View一个是DecorView,一个不是,就按照参数前边的View的isDecor布尔值来排序。其他直接按照参数前边的位置减后边的位置,来决定排序。按照位置升序排序。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值