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的滑动与事件分发相关。

你可能感兴趣的:(ViewPager源码解析(二):setAdapter,notifyDataSetChanged)