ViewPager数据修改使用notifyDataSetChanged无刷新的问题

最近使用viewpager的时候遇到一个问题,viewpager设置过pagerAdapter之后,当需要修改viewpager的数据时,使用pagerAdapter.notifyDataSetChanged方法似乎并没有完全生效。例如,第一次设置viewpager的数据为2页,然后减少为1页时,会出现第2页仍然能翻动却不能停留在第2页的现象。接下来,通过读源码来找寻原因和解决方案。

一、ViewPager和PagerAdapter简介

API中对ViewPager的说明是:一种允许用户左右滑动页面的布局管理器,可以通过实现一个PagerAdapter来产生要展示的页面。

那么首先,来看看API中对PagerAdapter的介绍。PagerAdapter用于将多个页面填充到ViewPager当中,实际中多数情况下,更倾向于使用一个更加具体的实现了PagerAdapter的适配器。实现一个PagerAdapter,必须至少重写以下方法:

  • instantiateItem(ViewGroup, int)
  • destroyItem(ViewGroup, int, Object)
  • getCount()
  • isViewFromObject(View, Object)

PagerAdapter比很多AdapterView的适配器更加通用。ViewPager使用回调来显示一次更新的操作步骤,而不是直接采用视图回收机制。如果需要,PagerAdapter也可以实现视图回收方式,或者使用一种更巧妙的方法来管理页面视图,比如页面使用fragment能够管理自身事务。

ViewPager将每一个页面与一个key关联起来,而不是直接操作页面。这个key用于跟踪和唯一标识一个指定页面,并且独立于adapter。调用PagerAdapter的startUpdate(ViewGroup)方法,表示ViewPager的内容将要发生变化;接着会有一次或多次调用instantiateItem(ViewGroup, int)方法和/或destroyItem(ViewGroup, int, Object)方法;最后调用finishUpdate(ViewGroup),标志着这一次刷新完成。当finishUpdate(ViewGroup)执行完成,与instantiateItem(ViewGroup, int)返回的key相关联的视图,被加入到父ViewGroup当中;而与传递到destroyItem(ViewGroup, int, Object)的key相关联的视图,会被父ViewGroup移除。isViewFromObject(View, Object)方法用于判断一个视图是否与一个指定的key相关联。

一个简单的PagerAdapter可能选择页面视图本身作为key,在创建视图并加入父ViewGroup后通过instantiateItem(ViewGroup, int)返回。相对应的,destroyItem(ViewGroup, int, Object)的实现即将视图从父ViewGroup中移除,isViewFromObject(View, Object)可以实现为return view == object。

PagerAdapter支持数据集的改变。数据集的改变必须发生在主线程,并且必须以调用notifyDataSetChanged()方法结束,这与继承自BaseAdapter的AdapterView的适配器类似。一个数据集的改变,可能涉及页面的增、删及位置改变。Viewpager通过在适配器中实现getItemPosition(Object)方法来保持当前页面处于运行状态。

二、ViewAdapter的notifyDataSetChanged()方法

那么,为什么数据集改变调用notifyDataSetChanged()方法刷新有时候不好用呢。首先从这句话开始。

adapter.notifyDataSetChanged();

该方法调用抽象基类PagerAdapter的notifyDataSetChanged()方法。

    public void notifyDataSetChanged() {
        this.mObservable.notifyChanged();
    }

mObservable是一个DataSetObservable对象,跟进方法,进入到DataSetObservable类中。DataSetObservable对象负责向list当中的所有DataSetObserver对象发送指示。notifyChanged()当中,当数据集变化,调用每个Observer的onChange()方法。

public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

其中,Observable是观察者模式的应用,Observable类是一个泛型抽象类,表示一个观察者对象,提供了观察者注册、取消注册和清空三个方法。DataSetObservable直接继承Observable,使用DataSetObserver实例化了Observable。

DataSetObserver表示一个数据集对象的观察者,接收数据集变化或者失效两种回调。

public abstract class DataSetObserver {
        public void onChanged() {
        // Do nothing
        }
        public void onInvalidated() {
        // Do nothing
        }
    }

在ViewPager当中发现PagerObserver,继承了DataSetObserver抽象类,实现了onChange()和onInvalidated()两个方法。

private class PagerObserver extends DataSetObserver {
        private PagerObserver() {
        }

        public void onChanged() {
            ViewPager.this.dataSetChanged();
        }

        public void onInvalidated() {
            ViewPager.this.dataSetChanged();
        }
    }

接下来,看ViewPager的dataSetChanged()方法。

void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.

        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
                mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

        boolean isUpdating = false;
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            final int newPos = mAdapter.getItemPosition(ii.object);

            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            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;
            }

            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);

        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();
        }
    }

重点看在于mAdapter.getItemPosition(Object)的返回结果。API中对该方法的说明是:该方法在主视图想要判断一个item的位置是否发生变化的时候调用。返回POSITION_UNCHANGED表示给定的item位置没有变化,返回POSITION_NONE表示给定的item已经不存在了。

而API中默认的实现是返回POSITION_UNCHANGED,即假设item的位置永远不会发生变化。这就能解释,为什么在删除或修改数据时,PagerAdapter不能只是通过notifyDataSetChanged实现刷新了。

 public int getItemPosition(Object object) {
        return POSITION_UNCHANGED;
    }

三、解决

比较简单的方法是:在Adapter中重写getItemPosition()方法,强迫viewpager重绘所有item。

 @Override  
    public int getItemPosition(Object object) {  
        return POSITION_NONE;  
    }

当视图比较复杂时,这样写可能会增加开销,那么可以根据自己的需要来实现getItemPosition()方法。例如仅需要对某个特定的view更新,可以通过给该view设置tag,在需要更新时,通过tag定位到这个view进行更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值