当前位置: 移动技术网 > 移动技术>移动开发>Android > Android ViewPager源码详细分析

Android ViewPager源码详细分析

2019年07月24日  | 移动技术网移动技术  | 我要评论
1.问题 由于android framework源码很庞大,所以读源码必须带着问题来读!没有问题,创造问题再来读!否则很容易迷失在无数的方法与属性之中,最后无功而返。

1.问题

由于android framework源码很庞大,所以读源码必须带着问题来读!没有问题,创造问题再来读!否则很容易迷失在无数的方法与属性之中,最后无功而返。

那么,关于viewpager有什么问题呢?
1). setoffsreenpagelimit()方法是如何实现页面缓存的?
2). 在布局文件中,viewpager布局内部能否添加其他view?
3). 为什么viewpager初始化时,显示了一个页面却不会触发onpageselected回调?

问题肯定不止这三个,但是有这三个问题基本可以找到本次分析的重点了。读者朋友也可以自己先提出一些问题,再看下面的分析,看看是否可以从分析过程中找到答案。

2.从onmeasure()下手

viewpager继承自viewgroup,是android framework提供的一个控件,而android系统显示控件的流程就是: activity加载布局实例化所有控件 —> rootview遍历所以控件 —> 对需要重绘的控件执行测量,布局,绘制的操作。

而转化到某个控件来说,它的流程就是:构造方法 —> onmeasure —> onlayout —> ondraw
由于viewpager的构造方法中只是初始化了一些与本文主题无关的属性就略过不讲,那么自然而然onmeasure方法就来到了我们眼前。

那么在onmeasure中viewpager做了些什么呢?先把源码摆出来,我进行了一些删减。

@override
  protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
    //测量viewpager自身大小
    setmeasureddimension(getdefaultsize(0, widthmeasurespec),
        getdefaultsize(0, heightmeasurespec));

    final int measuredwidth = getmeasuredwidth();

    // child的宽高,占满父控件
    int childwidthsize = measuredwidth - getpaddingleft() - getpaddingright();
    int childheightsize = getmeasuredheight() - getpaddingtop() - getpaddingbottom();

    //1.测量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进行测量
          //省略若干代码,主要负责对decor控件的测量
          ...
        }
      }
    }

    mchildwidthmeasurespec = measurespec.makemeasurespec(childwidthsize, measurespec.exactly);
    mchildheightmeasurespec = measurespec.makemeasurespec(childheightsize, measurespec.exactly);

    // 2.从adapter中获取childview
    minlayout = true;
    populate();
    minlayout = false;

    // 3.测量非decor的childview
    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) {
          final int widthspec = measurespec.makemeasurespec(
              (int) (childwidthsize * lp.widthfactor), measurespec.exactly);
          child.measure(widthspec, mchildheightmeasurespec);
        }
      }
    }
  }

简单总结就是三件事情。

2.1 测量decor控件

可能很多人有些懵x了,decor是个啥?
其实decor是一个接口,在viewpager内部定义的,并且该接口是没有定义任何内容的。唯一的作用就是如果你的控件实现了decor接口,那么你的控件就属于decorview了。
我们知道viewpager的数据是通过adapter管理的,但其实还有一种方式给viewpager添加childview.

#layout.xml
<viewpager>
  <decorview />
</viewpager>

上面这种直接在viewpager布局内部添加控件也是可以的,但是要求decorview必须实现decor接口,否则将不予显示。
在viewpager的addview方法中会对childview进行判断,也看一下代码吧!

 @override
  public void addview(view child, int index, viewgroup.layoutparams params) {
    if (!checklayoutparams(params)) {
      params = generatelayoutparams(params);
    }
    final layoutparams lp = (layoutparams) params;
    lp.isdecor |= child instanceof decor; //在此处给isdecor赋值

    //省略无关代码
    ...
  }

至于addview()方法是如何调用,可以参考本人博客 viewgroup如何加载布局中的view?
而上面的代码我们要注意的是lp.isdecor,这是viewpager为它的childview准备的layoutparams,在onmeasure的第一步中就是根据lp.isdecor来挑选出decor控件来测量的。
至于decor的测量过程与本文主题无关,在此就不详述了,有兴趣的可以自己去查看源码。

2.2 从adapter中创建childview(populate方法)

viewpager也是采用observable模式来设计的,数据通过pageradapter来管理,并且childview也是通过pageradapter来创建的,viewpager主要负责界面交互相关的工作。
对pageradapter并不会做太详细的介绍,直接给一个示例代码吧。

public class autoscrolladapter extends pageradapter {

  //省略构造方法代码
  ...

  @override
  public void destroyitem(viewgroup container, int position, object object) {

  }

  @override
  public int getcount() {
    return mdata.size();
  }

  @override
  public boolean isviewfromobject(view view, object object) {
    return view == object;
  }

  @override
  public object instantiateitem(viewgroup container, int position) {
    view itemview = new textview(mcontext); //通过各种方法新建一个childview
    container.addview(itemview);//将childview添加到viewpager中
    return itemview;
  }
}

这四个方法是必须要重写的,方法的含义根据方法名就能看出来。这里主要要讲一下最后这个方法instantiateitem()。它负责向viewpager提供childview,这里调用的addview方法是被viewpager重写过的,所以会对lp.isdecor赋值,并且我们可以知道,这里的isdecor=false。

有些人可能要问,这一步的主角不应该是populate()方法吗?的确应该是populate方法,但是由于这个方法比较复杂,为了阅读的连贯性考虑,博主决定单独提出来,一会儿再讲它。
在这里主要告诉大家,populate()方法内部会调用adapter.instantiateitem()方法,也就是将adapter中的childview添加到viewpager中来,为下一步做准备。

2.3 测量childview

有了上面的分析,这一步的内容就很好理解了。
简单来说就是,遍历所有的childview,挑选出lp.isdecor==false的childview,然后调用view.measure()方法让childview自己去完成测量。
还有一点需要注意,就是childview的宽度 width= childwidthsize * lp.widthfactor。
childwidthsize就是viewpager的宽度,lp.widthfactor代表这个childview占几个页面。
lp.widthfactor默认情况下是1.0,可以重写pageradapter.getpagewidth(pos)方法来修改这个值。
到此,viewpager的测量过程就完成了。

3.populate()方法

可以说这是viewpager最核心的一个方法,所以单独作为一个小节来分析。
在分析源码之前,必须先介绍一个类——iteminfo

3.1 iteminfo是什么?

static class iteminfo {
    object object; //childview
    int position;  //childview在adapter中的位置
    boolean scrolling; //是否在滚动
    float widthfactor; //宽度的倍数,默认情况下是1
    float offset;    //页面的偏移参数,粗暴的理解就是第几个页面
  }

这是viewpager内部定义的一个静态类,将childview相关的属性进行了包装,主要是为了方便对childview的管理。
并且在viewpager内部还维护了一个arraylist,由iteminfo对象组成,属性名是mitems。
这个list的长度就是由moffscreenpagelimit来决定的,这个在后面的代码分析中会看到。
好了,了解了基本对象之后,就可以开始分析populate方法了。
注意:由于代码比较长,为了方便阅读博主打算将populate()方法的代码分段讲解,如过代码中没有方法声明,则表示该段代码属于populate()方法。

3.2 获取当前的iteminfo对象

从这里开始,对populate()方法的源码进行分析,分析内容主要在代码的注释中编写。

  void populate(int newcurrentitem) {
    iteminfo oldcurinfo = null;
    int focusdirection = view.focus_forward;
    if (mcuritem != newcurrentitem) {
      focusdirection = mcuritem < newcurrentitem ? view.focus_right : view.focus_left;
      oldcurinfo = infoforposition(mcuritem); //获取旧的iteminfo对象
      mcuritem = newcurrentitem;  //更新mcuritem的值,就是在adapter中的position
    }
    //省略无关代码
    ...
    //moffscreenpagelimit就是setoffscreenpagelimit方法设置的值
    final int pagelimit = moffscreenpagelimit;

    //根据下面三行代码可知:mitems的长度就是 2 * pagelimit + 1
    //这里声明的startpos和endpos在后面会起作用,大家注意一下
    final int startpos = math.max(0, mcuritem - pagelimit);
    final int n = madapter.getcount();
    final int endpos = math.min(n-1, mcuritem + pagelimit);

    // 遍历mitems列表,找出mcuritem对应的iteminfo对象,是根据position来判断的
    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;
      }
    }
    // 如果mitems中还未保存该iteminfo,则创建一个inteminfo对象
    if (curitem == null && n > 0) {
      curitem = addnewitem(mcuritem, curindex);
    }
    ...

这里要注意的一点是,在新建iteminfo对象时,我们是调用的addnewitem方法,它的代码如下所示。

iteminfo addnewitem(int position, int index) {
    iteminfo ii = new iteminfo(); //新建一个iteminfo对象
    ii.position = position;
    ii.object = madapter.instantiateitem(this, position);//用adapter创建一个childview
    ii.widthfactor = madapter.getpagewidth(position);//默认返回1.0f
    if (index < 0 || index >= mitems.size()) { //添加到mitems中
      mitems.add(ii);
    } else {
      mitems.add(index, ii);
    }
    return ii;
  }

不管是从mitems中提取还是新建一个iteminfo对象,总之我们已经得到了curitem,即当前的inteminfo对象。

3.3 管理mitems中的其余对象

因为我们的mitems长度是有限的,并且与pagelimit有关,所以很可能出现页面总数大于mitems长度的情况。当显示的页面改变时,我们必须将一些iteminfo添加进来,将另一些iteminfo移除。
以保证我们的mitems中的iteminfo.position是这样的:
[ startpos … mcuritem … endpos ]

其中:
mcuritem = curitem.position
startpos = mcuritem - paglimit
endpos = mcuritem + paglimit

具体如何操作,我们来看代码

    if (curitem != null) {
      //1.调整curitem左边的对象
      float extrawidthleft = 0.f;

      // curindex是curitem在mitems中的索引
      // itemindex就是curitem左边的iteminfo的索引
      int itemindex = curindex - 1; 
      //获取左边的iteminfo对象
      iteminfo ii = itemindex >= 0 ? mitems.get(itemindex) : null;
      final int clientwidth = getclientwidth();
      //curitem左边需要的宽度,默认情况下为1.0f
      final float leftwidthneeded = clientwidth <= 0 ? 0 :
          2.f - curitem.widthfactor + (float) getpaddingleft() / (float) clientwidth;
      //遍历mitems左半部分,即curindex左边的对象
      //只有在pos < startpos时才能退出循环,否则会一直遍历到pos=0
      for (int pos = mcuritem - 1; pos >= 0; pos--) {
        // 建议大家先从下面的else if开始看,因为这里的逻辑是准备退出循环了
        if (extrawidthleft >= leftwidthneeded && pos < startpos) {
          //当pos < startpos,说明mitems左边部分已经调整完毕了
          //此时的ii代表的是,startpos左边的对象了
          if (ii == null) { 
            break;
          }
          //如果startpos左边还有对象,需要从mitems中移除
          if (pos == ii.position && !ii.scrolling) {
            mitems.remove(itemindex);
            madapter.destroyitem(this, pos, ii.object);
            itemindex--;
            curindex--;
            ii = itemindex >= 0 ? mitems.get(itemindex) : null;
          }

        //如果curindex左边的iteminfo对象不为null
        } else if (ii != null && pos == ii.position) {
          extrawidthleft += ii.widthfactor; //累加curitem左边需要的宽度
          itemindex--;           //再往curindex左边移一个位置
          ii = itemindex >= 0 ? mitems.get(itemindex) : null; //取出iteminfo对象

        //如果curindex左边的iteminfo为null
        } else {
          //新建一个iteminfo对象,添加到itemindex的右边
          ii = addnewitem(pos, itemindex + 1); 
          extrawidthleft += ii.widthfactor;  //累加左边宽度
          curindex++;  //由于往mitems中插入了一个对象,故curindex需要加1
          ii = itemindex >= 0 ? mitems.get(itemindex) : null; //去除iteminfo
        }
      }

      //2.调整curitem右边的对象,逻辑与上面类似
      //代码省略
      ...
      // 3.计算mitems中的偏移参数
      calculatepageoffsets(curitem, curindex, oldcurinfo);
    }

代码主要是一些逻辑,需要大家静下心来读,也不知道讲清除了没有。(发现要把代码翻译成文字真是累,一句代码要用一大段文字来说明)
对于calculatepageoffsets方法,就不贴源码分析了,主要说一下它做了哪些事情吧

根据olditem.position与curitem.position的大小关系,来确定curitem的offset值
再分别对curitem的左边和右边的item写入offset值
mpagemargin是页面之间的间隔, marginoffset = mpagemargin / childwidth
每个页面的offset = madapter.getpagewidth(pos) + marginoffset
参照上面的四点提示,大家去读源码应该也没啥难度的,关键是都是一些逻辑处理很难文字化说明。

3.4 一些收尾工作

    // 将iteminfo的内容更新到childview的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) {
        final iteminfo ii = infoforchild(child);
        if (ii != null) {
          lp.widthfactor = ii.widthfactor;
          lp.position = ii.position;
        }
      }
    }
    //根据lp.position的大小对所有childview进行排序,另外decorview是排在其他child之前的
    sortchilddrawingorder();

ok,populate方法分析到此就结束了。

4. onlayout

布局也是先布局decor,再布局adapter创建的childview,直接上源码吧。

  @override
  protected void onlayout(boolean changed, int l, int t, int r, int b) {
    final int count = getchildcount();
    int width = r - l;
    int height = b - t;

    //1.布局decor,根据lp.isdecor来筛选decorview
    //代码略
    ...

    final int childwidth = width - paddingleft - paddingright;

    for (int i = 0; i < count; i++) {
      final view child = getchildat(i);
      if (child.getvisibility() != gone) {
        final layoutparams lp = (layoutparams) child.getlayoutparams();
        iteminfo ii;
        //此处将decorview过滤掉,并且根据view从mitems中查找iteminfo对象
        //如果viewpager布局中添加了未实现decor接口的控件,将不会被布局
        //因为无法从mitems中查找到iteminfo对象
        if (!lp.isdecor && (ii = infoforchild(child)) != null) {
          //计算当前page的左边界偏移值,此处的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 = paddingtop;
    mbottompagebounds = height - paddingbottom;
    mdecorchildcount = decorcount;
    //如果是首次布局,则会调用scrolltoitem方法
    if (mfirstlayout) {
      scrolltoitem(mcuritem, false, 0, false);
    }
    mfirstlayout = false;
  }

布局这一块的代码相对来说要简单一些,就是根据offset偏移量来计算出left,right, top, bottom值,然后直接调用view.layout方法进行布局。
但是,这里需要插一句,在用viewpager实现轮播控件时,有一种方法是将adapter.getcount返回integer.max_value,已达到伪循环播放的目的。从上面的代码可以看到,此时这个offset值会不断的变大,那么

int loff = (int) (childwidth * ii.offset);

这个loff很可能会超出int的最大值边界。
所以,以后大家实现轮播控件时,还是不要采用这种方法了。

然后,回过头来再说下scrolltoitem方法
注意上面调用scrolltoitem时,最后一个参数传递的是false,而这个参数就是决定是否调用onpageselected回调函数的。
看代码:

  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) {
      smoothscrollto(destx, 0, velocity);
      if (dispatchselected) {
        dispatchonpageselected(item);
      }
    } else {
      if (dispatchselected) { //是否需要分发onpageselected回调
        dispatchonpageselected(item);
      }
      completescroll(false);
      scrollto(destx, 0);
      pagescrolled(destx);
    }
  }

也就是说,第一次布局viewpager时虽然会显示一个页面,却不会调用onpageselected方法。

onlayout的分析也到此结束了,至于ondraw方法viewpager并没有做什么,只是编写了绘制page之间间隔的代码,就不做分析了。

当然,viewpager的代码还不止这些,此文分析的仅仅是它的骨架,还有许多其他处理如onintercepttouchevent方法,pagescrolled方法等等,这些就留给读者自己去分析吧。

理解了这篇文章之后,对viewpager的工作原理也有一定程度的了解了,相信再去读那些代码难度不会很大。
至于篇头提到的三个问题,相信各位也已经有了答案。

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网