ViewPager源码解析(二):setAdapter,notifyDataSetChanged

在上一篇《ViewPager源码解析(一):onMeasure、onLayout、populate》中,我们梳理了ViewPager的onMeasure()与onLayout()流程,在onMeasure中调用了ViewPager的一个核心方法populate(),它实现了ViewPager的添加childView和删除childView的功能。
这一篇从我们使用入口出发,来分析ViewPager如何通过adapter来绑定数据,已经它是怎样完成刷新数据的。
我们知道在使用ViewPager时我们需要创建一个PagerAdapter适配器,我们在复写一个PagerAdapter时一般都会复写4个方法:
1、getCount():pager的总数量;
2、instantiateItem(ViewGroup container, int position):container其实就是我们的ViewPager,该方法需要添加ViewPager的pisition位置的childView,该方法需要返回一个object,我们一般都会返回这个childView。
3、isViewFromObject(View view, Object object):我们在使用该方法时一般都会返回 view==object,这里的view就是instantiateItem()中添加到container(ViewPager)中的childView,那这个object是什么呢?其实它就是上面instantiateItem()中我们返回的那个object,它会被存到ViewPager中的ItemInfo的object属性上。isViewFromObject()方法会被ViewPager用来判断childView与ItemInfo是否为对应关系,所以如果我们在instantiateItem()中返回的就是这个childView的话,那我们便可以通过view==object来判断他们的对应关系。(这里文字描述起来有些绕,可以结合《ViewPager源码解析(一):onMeasure、onLayout、populate》来进一步梳理这里的view,object与ItemInfo的关系)
4、destroyItem(ViewGroup container, int position, Object object):我们需要在该方法中将object对应的childView从container(ViewPager)上移除,这里的object也是instantiateItem()的返回值,所以我们的常规处理方式为container.removeView((View) object);
下面进入主题,看看ViewPager.setAdapter()中做了什么?

public void setAdapter(PagerAdapter adapter) {
        //若本身就存在mAdapter则先还原mAdapter状态
        if (mAdapter != null) {
            //清空Observer,防止内存泄漏
            mAdapter.setViewPagerObserver(null);
            mAdapter.startUpdate(this);
            //通知原mAdapter删除当前ViewPager上的所有非DecorView
            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();
            /**
             * 这里防止上面mAdapter.destroyItem()时使用者未正常清除掉所有非DecorView,
             * DecorView是ViewPager固定添加的子View,与Adapter无关,所以不用删除。
             */
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            //为新mAdapter设置观察者Observer
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            // 是否有layout过
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            //该字段用来存放mAdapter中的item数量,用来判断数量上发生的一些未知异常
            mExpectedAdapterCount = mAdapter.getCount();
            //mRestoredCurItem为ViewPager被系统内存不足回收再显示时保持的mCurItem,一般为-1
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {//layout过直接populate
                populate();
            } else {
                requestLayout();
            }
        }

        // 如果注册监听过AdapterChangeListener,通知监听者
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }

设置过程比较简单,大致为以下3个步骤:
1、判断是否已经存在adapter,若存在则清空原adapter绑定的数据,清空 mItems上的缓存,还原ViewPager的初始状态。
2、绑定设置进来的Adapter,根据是否layout过来执行populate()或者requestLayout(),其实根据《ViewPager源码解析(一):onMeasure、onLayout、populate》可以知道requestLayout()肯定也会执行populate();
3、回调ViewPager中添加的mAdapterChangeListeners监听(一般较少使用)。

setAdapter()最终会通过上面的步骤2中的populate()方法将adapter中的数据显示出来。

setAdapter后我们来看PagerAdapter.notifyDataSetChanged()怎样实现的刷新。
首先在上面setAdapter()时,ViewPager会调用Adapter.setViewPagerObserver(mObserver)方法,往PagerAdapter中添加一个mObserver观察者,这是一个典型的观察者模式,mObserver是一个PagerObserver,用来监听PagerAdapter中的数据变化,后续PagerAdapter.notifyDataSetChanged()便是通过mObserver通知ViewPager来进行刷新。

PagerAdapter.notifyDataSetChanged()

    public void notifyDataSetChanged() {
        synchronized (this) {
            //这里的mViewPagerObserver是ViewPager设置进来的观察者,这里先通知该观察者数据changed
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        
        /**
         * 这里通知单独注册PagerAdapter数据变化的观察者
         * 另外可以通过registerDataSetObserver(DataSetObserver observer)注册观察者来监听PagerAdapter的数据变化
         */
        mObservable.notifyChanged();
    }

方法中的mViewPagerObserver就是上面设置进来的mObserver,所以notifyDataSetChanged()时,会调用ViewPager中的PagerObserver.onChanged()方法。

ViewPager.PagerObserver

    private class PagerObserver extends DataSetObserver {
        PagerObserver() {
        }

        @Override
        public void onChanged() {
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

然后调用ViewPager.dataSetChanged(),dataSetChanged()便是真正刷新ViewPager的实现了。

    void dataSetChanged() {
        final int adapterCount = mAdapter.getCount();
        //保留安全adapterCount
        mExpectedAdapterCount = adapterCount;
        /**
         * needPopulate表示是否需要populate()来重新计算items以及刷新页面
         * 如果mItems能缓存更多的item则需要populate
         */
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                && mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

        boolean isUpdating = false;

        /**
         * 这里遍历当前加载在ViewPager上的items,
         * 为什么这里只用遍历当前加载在ViewPager上的items?因为ViewPager在加载mItems以外的
         *      item时都会重新执行Adapter的instantiateItem()方法,都会展示出最新的页面数据,
         *      所以如果我们是改变mItems以外的item时,不用调用notifyDataSetChanged()方法
         *      也能达到刷新效果
         */
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            /**
             * 取出ItemInfo.object对应的position,该position决定接下来是否需要调用populate
             * 进入mAdapter.getItemPosition(ii.object)发现返回值默认为:POSITION_UNCHANGED
             */
            final int newPos = mAdapter.getItemPosition(ii.object);

            //由于newPos默认为POSITION_UNCHANGED,可以看到该循环默认未做任何处理
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            /**
             * 如果我们复写PagerAdapter的getItemPosition()方法,并返回POSITION_NONE时,
             *      不管对应数据源是被删除还是内容发生改变,ViewPager将会先删除该item,然后
             *      通过populate来重新计算mItems并显示当前ViewPager的最新数据,所以如果我们
             *      只是内容发生改变时也是通过先删除后添加的方式来实现刷新。
             */
            if (newPos == PagerAdapter.POSITION_NONE) {
                //删除对应位置的ItemInfo
                mItems.remove(i);
                i--;
                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }

                //通知PagerAdapter解绑对应childView
                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                /**
                 * 如果是当前显示的item,重置newCurrItem,这里主要防止mCurItem是adapter中最后一个
                 *      item时,后面刷新页面时取newCurrItem位置的ItemInfo出现下标越界
                 */
                if (mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }

            /**
             * 这里提供了另外一种触发刷新的机制,用mAdapter.getItemPosition(ii.object)与ii.position
             *      进行比较,如果不一样也将触发刷新,他会定位到newPos,并将该ItemInfo的位置也调整到newPos。
             *
             * (我在PagerAdapter.instantiateItem尝试为每一个childView设置tag为当前的position,
             * 然后针对某一个特定的childView设置了不同于当前position的tag,之后在复写了PagerAdapter.getItemPosition(Object obj)
             * 方法,并返回了tag,这时那个特定的childView的tag将会触发该条件,这时候出现也一些混乱的跳动,
             * 首先页面上未发生任何变化,但是在滑动时发现此时的childView已经处于设置的tag的位置(由 ii.position = newPos导致),
             * 同时我回到那个特定的childView原来的位置,发现它还在那,同时页面数据已发生了改变。)
             *
             * 这里暂未想到使用这种刷新方式的场景
             */
            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);

        //根据以上的循环,判断needPopulate是否需要重新populate,需要的话则通过requestLayout()刷新view
        if (needPopulate) {
            //下面会通过requestLayout()刷新页面,这里先将lp.widthFactor置为0
            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;
                }
            }

            setCurrentItemInternal(newCurrItem, false, true);
            //最终也会调用populata()
            requestLayout();
        }
    }

在看完ViewPager.dataSetChanged()后对ViewPager的刷新机制反而有些摸不着头脑了,首先,调用PagerAdapter.notifyDataSetChanged()方法默认不会产生任何效果,它不会对当前加载的childView有刷新作用(这是一个奇怪的地方,开始以为是一个bug,后面一下觉得可能是有意这么设计,考虑到刷新时需要先移除childView再添加childView,频繁的刷新对整体影响较大。所以这里刷新默认不做任何处理,且提供了刷新的方式)。需要实时刷新childView时需要在getItemPosition(Object object) 对需要刷新的childView返回POSITION_NONE,不建议全部返回POSITION_NONE,可以通过view绑定tag,对特定的childView实现可实时刷新功能。
到这里ViewPager的数据绑定与刷新就已经完成了,下一篇会进一步分析ViewPager的滑动与事件分发相关。

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