最近使用viewpager的时候遇到一个问题,viewpager设置过pagerAdapter之后,当需要修改viewpager的数据时,使用pagerAdapter.notifyDataSetChanged方法似乎并没有完全生效。例如,第一次设置viewpager的数据为2页,然后减少为1页时,会出现第2页仍然能翻动却不能停留在第2页的现象。接下来,通过读源码来找寻原因和解决方案。
API中对ViewPager的说明是:一种允许用户左右滑动页面的布局管理器,可以通过实现一个PagerAdapter来产生要展示的页面。
那么首先,来看看API中对PagerAdapter的介绍。PagerAdapter用于将多个页面填充到ViewPager当中,实际中多数情况下,更倾向于使用一个更加具体的实现了PagerAdapter的适配器。实现一个PagerAdapter,必须至少重写以下方法:
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)方法来保持当前页面处于运行状态。
那么,为什么数据集改变调用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进行更新。