在上一篇《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的滑动与事件分发相关。