Android ViewPager源码详细分析

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的工作原理也有一定程度的了解了,相信再去读那些代码难度不会很大。
至于篇头提到的三个问题,相信各位也已经有了答案。

(0)

相关推荐

  • android配合viewpager实现可滑动的标签栏示例分享

    复制代码 代码如下: package com.example.playtabtest.view; import com.example.playtabtest.R; import android.app.Activity;import android.content.Context;import android.support.v4.view.ViewPager;import android.support.v4.view.ViewPager.OnPageChangeListener;impor

  • android教程viewpager自动循环和手动循环

    复制代码 代码如下: package com.imoyo.expert; import java.util.ArrayList; import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.support.v4.view.ViewPager;import android.support.v4.view.ViewPager.OnPageChangeListener;imp

  • Android App中ViewPager与Fragment结合的一些问题解决

    在了解ViewPager的工作原理之前,先回顾ListView的工作原理: ListView只有在需要显示某些列表项时,它才会去申请可用的视图对象:如果为所有的列表项数据创建视图对象,会浪费内存: ListView找谁去申请视图对象呢? 答案是adapter.adapter是一个控制器对象,负责从模型层获取数据,创建并填充必要的视图对象,将准备好的视图对象返回给ListView: 首先,通过调用adapter的getCount()方法,ListView询问数组列表中包含多少个对象(为避免出现数组

  • Android viewpager中动态添加view并实现伪无限循环的方法

    本文实例讲述了Android viewpager中动态添加view并实现伪无限循环的方法.分享给大家供大家参考,具体如下: viewpager的使用,大家都熟悉,它可以实现页面之间左右滑动的切换,这里有一个需求,就是viewpager里面加载的页数不是确定的,而是根据数据的多少来确定的.常见的应用就是在一个新闻的详细页面中,显示与此新闻有关的图片. 下面我们来看一下代码: activity_main.xml <RelativeLayout xmlns:android="http://sch

  • Android 使用ViewPager自动滚动循环轮播效果

    对Android 利用ViewPager实现图片可以左右循环滑动效果,感兴趣的朋友可以直接点击查看内容详情. 主要介绍如何实现ViewPager自动播放,循环滚动的效果及使用.顺便解决ViewPager嵌套(ViewPager inside ViewPager)影响触摸滑动及ViewPager滑动速度设置问题. 先给大家展示下效果图,喜欢的朋友可以下载源码: 1.实现 没有通过ScheduledExecutorService或Timer定期执行某个任务实现,而是简单的通过handler发送消息去

  • Android利用ViewPager实现滑动广告板实例源码

    •android-support-v4.jar,这是谷歌官方给我们提供的一个兼容低版本Android设备的软件包,里面包囊了只有在Android3.0以上可以使用的api.而ViewPager就是其中之一,利用它我们可以做很多事情,从最简单的导航,到页面切换菜单等等. •ViewPager的功能就是可以使视图滑动,就像Lanucher左右滑动那样. •本Demo向大家演示ViewPager的使用,并在用户未滑动View时,每隔5s钟自动切换到下一个View(循环切换),而当用户有Touch到Vi

  • Android 开发之BottomBar+ViewPager+Fragment实现炫酷的底部导航效果

    BottomBar BottomBar是Github上的一个开源框架,因为从1.3.3开始不支持fragments了,要自己配置,弄了很久,不管是app的fragment还是V4 的程序总是总是闪退.于是就用这种方式实现了,效果还不错.github有详细说明,多余的就不说了. 这个roughike是这个项目的所有者(大神致敬). 我用的是Android studio开发,fragment全部导的V4的包(以为最开始就支持的是v4的,后面也支持了app.fragment). 首先是dependen

  • Android ViewPager相册横向移动的实现方法

    当我们第一次下载QQ并且打开的时候,会有一个新手引导,引导是几张图片,再加上一些文字说明,向右滑动,直到结束,今天一大早起来研究了一下关于此种效果的实现之ViewPager控件. 下面这个例子将用ViewPager实现横向移动相册,ViewPager有一个对应的PagerAdapter,用于绑定数据:我们需要继承此类并实现自己的功能. 1.首先定义一个显示项所需要使用的数据对象ImageItem 复制代码 代码如下: public class ImageItem { private int id

  • Android App中使用ViewPager+Fragment实现滑动切换效果

    在android应用中,多屏滑动是一种很常见的风格,没有采用viewpager的代码实现会很长,如果采用ViewPager,代码就会短很多,但是使用ViewPager也有弊端:需要导入android-support-v4.jar.细节无法控制.不过现在情况已经不一样了,android-support-v4中提供了很多实用的功能,以至于现在新建一个android工程默认都会导入这个jar包.那我们就也采用viewpager来做滑动吧.另外一个概念就是Fragment和FragmentActivit

  • Android 使用viewpager实现无限循环(定时+手动)

    循环轮播的方法有两种,一种是使用定时器另外一种是使用手指拨动,相比较而言,定时器实现循环播放比较容易,只要在定时器的消息里加上简单几段代码即可,下面分别就这两种方法给大家详解,具体详情请看下文吧. int count = adapter.getCount(); if (count > 1) { // 多于1个,才循环 int index = viewPager.getCurrentItem(); index = (index + 1) % count; viewPager.setCurrentI

随机推荐