ViewPager源码分析(3):与PagerAdapter 交互

我的CSDN博客同步发布:ViewPager源码分析(3):与PagerAdapter 交互

转载请注明出处:【huachao1001的简书:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】

我们知道,ViewPager显示的页面离不开我们定义的适配器,正是因为我们编写了自己的适配器,才让ViewPager显示出满足你的需求的内容,那么ViewPager是如何与适配器(PagerAdapter)进行交互的呢?我们今天来研读一下ViewPager中与PagerAdapter交互的部分代码。本文对学习ViewPager很重要,请耐心往下仔细研读 ...O(∩_∩)O~~

在分析源码之前,我们先看看ViewPager的最简单的示例用法,当然了,你也可以把你的所有View保存到一个List中。

先看看ViewPager简单使用

通过用法找到切入点,ViewPager的比较典型的示例用法如下:

public class MainActivity extends AppCompatActivity {

private List<String> data;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    init();


    ViewPager viewPager = (ViewPager) findViewById(R.id.vp);
    viewPager.setAdapter(new MyAdapter());
}

private void init() {
    data = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        data.add("str" + i);
    }
}

class MyAdapter extends PagerAdapter {

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {

        container.removeView((View) object);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        TextView textView = new TextView(MainActivity.this);
        String s = data.get(position);
        textView.setText(s);
        container.addView(textView);
        
        return textView;
    }
}
}

可以看到ViewPager与我们的数据源之间是需要通过适配器来适配的。接下来我们去看看ViewPager源码中,是如何与PagerAdapter交互。

从setAdapter切入

上面简单示例代码中可用看到,调用ViewPagersetAdapter函数即可将ViewPagerPagerAdapter关联起来,我们先去查看ViewPagersetAdapter方法。

public void setAdapter(PagerAdapter adapter) {
    //1.如果已经设置过PagerAdapter,即mAdapter != null,
    // 则做一些清理工作
    if (mAdapter != null) {
        //2.清除观察者
        mAdapter.setViewPagerObserver(null);
        //3.回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面
        mAdapter.startUpdate(this);
        //4.如果之前保存有页面,则将之前所有的页面destroy掉
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        //5.回调finishUpdate,告诉PagerAdapter结束更新
        mAdapter.finishUpdate(this);
        //6.将所有的页面清除
        mItems.clear();
        //7.将所有的非Decor View移除,即将页面移除
        removeNonDecorViews();
        //8.当前的显示页面重置到第一个
        mCurItem = 0;
        //9.滑动重置到(0,0)位置
        scrollTo(0, 0);
    }

    //10.保存上一次的PagerAdapter
    final PagerAdapter oldAdapter = mAdapter;
    //11.设置mAdapter为新的PagerAdapter
    mAdapter = adapter;
    //12.设置期望的适配器中的页面数量为0个
    mExpectedAdapterCount = 0;
    //13.如果设置的PagerAdapter不为null
    if (mAdapter != null) {
        //14.确保观察者不为null,观察者主要是用于监视数据源的内容发生变化
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        //15.将观察者设置到PagerAdapter中
        mAdapter.setViewPagerObserver(mObserver);
        mPopulatePending = false;
        //16.保存上一次是否是第一次Layout
        final boolean wasFirstLayout = mFirstLayout;
        //17.设定当前为第一次Layout
        mFirstLayout = true;
        //18.更新期望的数据源中页面个数
        mExpectedAdapterCount = mAdapter.getCount();
        //19.如果有数据需要恢复
        if (mRestoredCurItem >= 0) {
            //20.回调PagerAdapter的restoreState函数
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            //21.标记无需再恢复
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) {//如果在此之前不是第一次Layout
            //22.由于ViewPager并不是将所有页面作为子View,
            // 而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面)
            //因此需要创建和销毁页面,populate主要工作就是这些
            populate();
        } else {
            //23.重新布局(Layout)
            requestLayout();
        }
    }
    //24.如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器
    // 则回调OnAdapterChangeListener的onAdapterChanged函数
    if (mAdapterChangeListener != null && oldAdapter != adapter) {
        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
    }
}

从第2条注解中看到,需要清除观察者,另外从第14条注释中看到,需要设定观察者,那么这个观察者是干嘛的呢?显然,这里是通过使用观察者模式。就是说,当我们编写代码时,如果数据源发生变化,需要在代码里调用PagerAdapter的notifyDataSetChanged函数,即通知ViewPager数据源发生变化,ViewPager就是一个观察者,通过观察者类PagerObserver做相关应对操作。

另外,前面多次提到页面的抽象描述类ItemInfo,我们看看ItemInfo的定义:

static class ItemInfo {
    //object为PagerAdapter的instantiateItem函数返回的对象
    Object object;
    //position为页面的序号,即第几个页面
    int position;
    //是否正在滚动
    boolean scrolling;
    //页面宽度,取值为0到1,表示为页面宽度与ViewPager显示区域宽度比例,默认为1
    float widthFactor;
    //偏移量,页面移动的偏移量,默认为0
    float offset;
}

最后在第22条注释中,调用了populate()函数,而populate()函数是做什么的呢?可以说,我们在使用ViewPager之所以流畅不卡,绝大部分功劳属于populate函数。

大功臣populate函数

细心的童鞋会发现,早在上一篇文章《ViewPager源码分析(2):滑动及冲突处理 》2.3 ViewPager 定义smoothScrollTo函数小节源码中的第33行中,就出现过populate函数,无参数的populate其内部是调用了有参的populate(int newCurrentItem)函数,而newCurrentItem表示当需要定位显示的页面。我们先看看源码:

void populate(int newCurrentItem) {
    ItemInfo oldCurInfo = null;
    if (mCurItem != newCurrentItem) {
        oldCurInfo = infoForPosition(mCurItem);
        mCurItem = newCurrentItem;
    }

    if (mAdapter == null) {
        //对子View的绘制顺序进行排序,优先绘制Decor View
        //再按照position从小到大排序
        sortChildDrawingOrder();
        return;
    }

    //如果我们正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建子View,
    // 直到滚动到最终位置再去创建,以免在这个期间出现差错
    if (mPopulatePending) {
        if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
        //对子View的绘制顺序进行排序,优先绘制Decor View
        //再按照position从小到大排序
        sortChildDrawingOrder();
        return;
    }

    //同样,在ViewPager没有attached到window之前,不要populate.
    // 这是因为如果我们在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突
    if (getWindowToken() == null) {
        return;
    }
    //回调PagerAdapter的startUpdate函数,
    // 告诉PagerAdapter开始更新要显示的页面
    mAdapter.startUpdate(this);

    final int pageLimit = mOffscreenPageLimit;
    //确保起始位置大于等于0,如果用户设置了缓存页面数量,第一个页面为当前页面减去缓存页面数量
    final int startPos = Math.max(0, mCurItem - pageLimit);
    //保存数据源中的数据个数
    final int N = mAdapter.getCount();
    //确保最后的位置小于等于数据源中数据个数-1,
    // 如果用户设置了缓存页面数量,第一个页面为当前页面加缓存页面数量
    final int endPos = Math.min(N - 1, mCurItem + pageLimit);

    //判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常
    if (N != mExpectedAdapterCount) {
        //resName用于抛异常显示
        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());
    }

    //定位到当前获焦的页面,如果没有的话,则添加一个
    int curIndex = -1;
    ItemInfo curItem = null;
    //遍历每个页面对应的ItemInfo,找出获焦页面
    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;
        }
    }
    //如果没有找到获焦的页面,说明mItems列表里面没有保存获焦页面,
    // 需要将获焦页面加入到mItems里面
    if (curItem == null && N > 0) {
        curItem = addNewItem(mCurItem, curIndex);
    }

    //默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数量,
    // 则将当前页面两边都缓存用户指定的数量的页面
    //如果当前没有页面,则我们啥也不需要做
    if (curItem != null) {
        float extraWidthLeft = 0.f;
        //左边的页面
        int itemIndex = curIndex - 1;
        //如果当前页面左边有页面,则将左边页面对应的ItemInfo取出,否则左边页面的ItemInfo为null
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        //保存显示区域的宽度
        final int clientWidth = getClientWidth();
        //算出左边页面需要的宽度,注意,这里的宽度是指实际宽度与可视区域宽度比例,
        // 即实际宽度=leftWidthNeeded*clientWidth
        final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
        //从当前页面左边第一个页面开始,左边的页面进行遍历
        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);
                    //回调PagerAdapter的destroyItem
                    mAdapter.destroyItem(this, pos, ii.object);
                    if (DEBUG) {
                        Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                " view: " + ((View) ii.object));
                    }
                    //由于mItems删除了一个元素
                    //需要将索引减一
                    itemIndex--;
                    curIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            } else if (ii != null && pos == ii.position) {
                //如果当前位置是需要缓存的位置,并且这个位置上的页面已经存在
                //则将左边宽度加上当前位置的页面
                extraWidthLeft += ii.widthFactor;
                //mItems往左遍历
                itemIndex--;
                //ii设置为当前遍历的页面的左边一个页面
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            } else {//如果当前位置是需要缓存,并且这个位置没有页面
                //需要添加一个ItemInfo,而addNewItem是通过PagerAdapter的instantiateItem获取对象
                ii = addNewItem(pos, itemIndex + 1);
                //将左边宽度加上当前位置的页面
                extraWidthLeft += ii.widthFactor;
                //由于新加了一个元素,当前的索引号需要加1
                curIndex++;
                //ii设置为当前遍历的页面的左边一个页面
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }
        //同理,右边需要添加缓存的页面
        //......
        
       // 省略右边添加缓存页面代码  
       
       //......

        calculatePageOffsets(curItem, curIndex, oldCurInfo);
    }

    if (DEBUG) {
        Log.i(TAG, "Current page list:");
        for (int i = 0; i < mItems.size(); i++) {
            Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
        }
    }
    //回调PagerAdapter的setPrimaryItem,告诉PagerAdapter当前显示的页面
    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    //回调PagerAdapter的finishUpdate,告诉PagerAdapter页面更新结束
    mAdapter.finishUpdate(this);


    //检查页面的宽度是否测量,如果页面的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;
            }
        }
    }
    //重新对页面排序
    sortChildDrawingOrder();
    //如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦
    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(View.FOCUS_FORWARD)) {
                        break;
                    }
                }
            }
        }
    }
}

从populate函数源码我们看到,ViewPager缓存当前显示的页面左右两边的页面,这个页面个数默认为左右两边各1个(如果左右都有至少1个的话),或者是用户通过调用ViewPager的setOffscreenPageLimit(int limit)函数来设定左右两边保持(好吧,原谅我从头到尾用缓存这个词)的页面个数。看看setOffscreenPageLimit(int limit)源码:

public void setOffscreenPageLimit(int limit) {
//DEFAULT_OFFSCREEN_PAGES=1
  if (limit < DEFAULT_OFFSCREEN_PAGES) {
      Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
              DEFAULT_OFFSCREEN_PAGES);
      limit = DEFAULT_OFFSCREEN_PAGES;
  }
  if (limit != mOffscreenPageLimit) {
      mOffscreenPageLimit = limit;
      populate();
  }
}

可以看到如果我们设置的值小于1,那么ViewPager会将缓存页面数量设置为1,即,缓存的页面数量至少为1,并且每次改变缓存数量后也会调用populate函数。

既然ViewPager真正的子View个数只是两边"缓存"的页面个数+1(当前显示的页面),那么ViewPager是如何做到无阻碍的从头滑到尾而不出问题呢?前面我们提到,smoothScrollTo函数里面也调用了populate函数,而populate函数维护了当前显示的页面和左右两边“缓存”的页面,这样就做到了能滑到结尾。那么populate是如何让ViewPager的子View一直保持为两边"缓存"的页面+当前显示的页面呢?其实,源码上面很明显,populate先判断页面是否不在缓存的范围内,如果不在缓存范围内,则Destroy掉(调用PagerAdapterdestroyItem),而如果在缓存范围,但是这个位置上页面不存在(即没有加入到ViewPager,作为ViewPager子View),则调用PagerAdapterinstantiateItem来添加新页面(通过addNewItem来调用)。假设我执行了setOffscreenPageLimit(2)函数,那么我们看看ViewPager的简单示意图:

ViewPager子View

如果你仔细想想,你会发现,这原理跟《打造属于你的LayoutManager 》一文中的RecyclerView很像,不同的是,RecyclerView有回收利用,而ViewPager没有View的回收利用。

好啦,今天的学习就到这里啦~

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

推荐阅读更多精彩内容