ViewPager 源码分析(二) —— 关于 notifyDataSetChanged()

来源:互联网 发布:淘宝众划算怎么样 编辑:程序博客网 时间:2024/06/08 17:06

写在前面


关于 ViewPager,我准备写一个系列。我水平也不咋地,估计有不少纰漏,各位爱看不看:

  • ViewPager 源码分析(一) —— setAdapter() 与 populate()


知识点


上回说到PagerAdapter这个类以及setAdapter()populate()如何在数据变化时重建和销毁页面。本节我们继续来看PagerAdapter,体味一下 android 中典型的观察者模式。其中涉及这几个类:

  • Observable
  • DataSetObservable
  • DataSetObserver
  • ViewPager.PagerObserver


源码分析


关于这个观察者模式的架构,我们来看张图:
这里写图片描述

我们可以在图中看出,PagerAdapter的观察者模式是基于ObservableDataSetObserver这两个基类。事实上,不管ListView还是RecyclerViewAdapter都是基于它们。考虑到ListViewRecyclerView也是极其常用的控件,了解该观察者模式的实现很有必要。

通常意义上的Observable,和Observer长这样:

public class Observable{    private List mObservers; // 一堆 observer    public void notifyChanged(); // 遍历 mObservers, 依次回调 observer.onChange()    public void registerObserver(Observer observer); // 注册: mObservers.add(observer)    public void unregisterObserver(Observer observer); // 移除: mObservers.remove(observer)}public abstract class Observer {    public void onChanged() {}}

在图中,你能发现一些类似的痕迹。但不知为何被分成了两层。notifyChanged()被移到了子类DataSetObservable中。注意到,PagerAdapter也有类似Observable的三个接口,并且持有一个DataSetObservable,事实上是一种代理模式Proxy

notifyDataSetChanged()

差不多说完背景,我们从notifyDataSetChanged()看起。一般我们改变了Adapter的数据之后,都要调用一下它。根据之前的分析,我们知道会回调到observer.onChange()这个函数。那这个observer又是啥呢?图中,我们看到PagerObserver的身影,结合代码看看:(想回调,肯定少不了注册的过程啦)

public class ViewPager extends ViewGroup {    private PagerObserver mObserver; // Observer    public void setAdapter(PagerAdapter adapter) {        ...        if (mAdapter != null) {            if (mObserver == null) {                mObserver = new PagerObserver();            }            mAdapter.registerDataSetObserver(mObserver); // 注册 Observer    }    private class PagerObserver extends DataSetObserver {        @Override        public void onChanged() { // 回调到这里            dataSetChanged();        }        @Override        public void onInvalidated() {            dataSetChanged();        }    }}


dataSetChanged()

注册完mObserver之后,一切都指向dataSetChanged()
先别急着看代码,你是否遇到过调用notifyDataSetChanged()数据不更新的情况(instantiateItem()未被回调)?其原因就在这个函数里。它留了一个口给我们——mAdapter.getItemPosition(),用于控制数据更新流程:

  • PagerAdapter.POSITION_UNCHANGED: 页面未改变,啥都不做(默认行为)
  • PagerAdapter.POSITION_NONE:页面已销毁,populate() 重建页面,然后间接回调instantiateItem()。不了解 populate() 的童鞋可以先看本系列的第一篇。
    void dataSetChanged() {        // This method only gets called if our observer is attached, so mAdapter is non-null.        final int adapterCount = mAdapter.getCount();        mExpectedAdapterCount = adapterCount;        // 0. 是否调用 populate() 重建页面,由最大缓存数和数据总数决定。        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&                mItems.size() < adapterCount;        int newCurrItem = mCurItem;        // 1. 由 getItemPosition() 的返回值决定,是否 needPopulate        boolean isUpdating = false;        for (int i = 0; i < mItems.size(); i++) {            final ItemInfo ii = mItems.get(i);            // 1.0 mAdapter.getItemPosition() 开了一个口,对是否更新数据起到决定性的作用            final int newPos = mAdapter.getItemPosition(ii.object);            // 1.1 这个是默认值: 页面未改变,啥都不做。            // 所以解释了为何 notifyDataSetChanged() 会无效,            // 你需要重写 getItemPosition(), 返回 PagerAdapter.POSITION_NONE。            // 这个时候会回调 instantiateItem(),             // 记得做缓存,而不是每次 new 一个 page 出来。            if (newPos == PagerAdapter.POSITION_UNCHANGED) {                continue;            }            // 1.2 表示页面已销毁,使得 needPopulate = true;            if (newPos == PagerAdapter.POSITION_NONE) {                mItems.remove(i);                i--;                if (!isUpdating) {                    mAdapter.startUpdate(this);                    isUpdating = true;                }                mAdapter.destroyItem(this, ii.position, ii.object);                needPopulate = true;                if (mCurItem == ii.position) {                    // Keep the current item in the valid range                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));                    needPopulate = true;                }                continue;            }            // 1.3 TODO: 尚不明白            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);        // 2. 如果需要 populate(), 调用 requestLayout();        // 会重新走一遍 measure() -> layout() -> draw(),        // 其中 onMeasure() 会间接调用 populate().        if (needPopulate) {            // Reset our known page widths; populate will recompute them.            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);            requestLayout();        }    }


总结


通过源码的分析,我们了解了观察者模式的内部实现。同时,我们看到了notifyDataSetChanged()在使用上的一个坑:
Android viewPage notifyDataSetChanged无刷新
它必须配合getItemPosition()的重写才能正常使用,这样的设计不知是bug还是处于什么特殊目的。另外,在instantiateItem()时,我们得相应得做好缓存。

至此,分析源码还是有益的,可以发现一些设计上的坑。事实上,上面的坑在工作中,我的确踩到过,为此还花了不少时间。

0 0
原创粉丝点击