在Android开发中,我们应该使用到很高频率的一个控件就是ViewPager。但是在使用ViewPager的过程中,我们会发现有两个问题,一是不能关闭预加载;二是更新ViewPager的Adapter不生效。所以我在这里以FragmentStatePagerAdapter为例,探讨一下为什么更新adapter无法生效,并且提出解决方案。
为什么Adapter更新不生效
更新不生效其实很简单,我们看一下源码的调用过程
PagerAdapter.java
// PagerAdapter.java
/**
* This method should be called by the application if the data backing this adapter has changed
* and associated views should update.
*/
public void notifyDataSetChanged() {
synchronized (this) {
if (mViewPagerObserver != null) {
mViewPagerObserver.onChanged();
}
}
mObservable.notifyChanged();
}
ViewPager.java
// ViewPager.java
private class PagerObserver extends DataSetObserver {
PagerObserver() {
}
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
// 接着调用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);
// 调用adapter的getItemPosition方法,获取新位置
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();
}
}
PageAdapter.getItemPosition(ii.object)
public int getItemPosition(@NonNull Object object) {
return POSITION_UNCHANGED;
}
默认是返回POSITION_UNCHANGED,所以结合上面的逻辑
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
这里跳出循环,所以其实什么事情都没有做,也没办法因为数据集的变更而变更UI。
如何解决
其实ViewPager.dataSetChanged()方法已经为我们预留了更新的逻辑。
......
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;
}
// 这里的newPos就是上面从PagerAdapter.getItemPosition()获取到的位置信息
if (ii.position != newPos) {
if (ii.position == mCurItem) {
// Our current item changed position. Follow it.
newCurrItem = newPos;
}
ii.position = newPos;
needPopulate = true;
}
}
......
所以我们要跳过newPos == PagerAdapter.POSITION_UNCHANGED 和 newPos == PagerAdapter. POSITION_NONE 的逻辑,需要对PagerAdapter.getItemPosition()进行改造。
复制FragmentStatePagerAdapter源码
由于改造getItemPosition()方法需要对源码进行操作,所以我们首先需要复制一份源码,暂时叫DynamicFragmentStatePagerAdapter
改造getItemPosition
@Override
public int getItemPosition(@NonNull Object object) {
int index = indexOfFragments(object);
return index != -1 ? index : super.getItemPosition(object);
}
private int indexOfFragments(Object object) {
if (object instanceof Fragment) {
return mFragments.indexOf(object);
}
return -1;
}
getItemPosition方法的注释我们了解一下,
/**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
*
The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
简单来说就是就是根据这个方法,来判断当前item的位置是否发生了改变,返回值包括POSITION_UNCHANGED、POSITION_NONE、以及[0, {@link #getCount()}。所以当我们数据集发生变化的时候,其实fragment的位置,也要相应的发生变化否则就会在错误的位置获取到错误的页面,会发生页面显示错乱。
mFragments对象是用来缓存当前adapter维护的在内存中的fragment缓存对象。是一个数组结构,数组会随着数据集的增大而增大,数组的内容是fragment对象,会根据当前viewpager位置,保存的ViewPager.setOffscreenPageLimit(size)中size的大小的实例,ViewPager进行滑动时,超出page limit的页面会被执行destroyItem方法,并且预加载的fragment会执行instantiateItem方法,确保adapter中只保留相应数量的fragment实例。这里不展开说,可以详细看下FragmentStatePagerAdapter.instantiateItem(@NonNull ViewGroup container, int position)的方法源码。
mSavedState对象用来缓存adapter中所有fragment执行onSaveInstanceState()之后的数据,在destroyItem方法中调用被销毁的fragment的onSaveInstanceState方法,并把数据放在mSavedState对应的位置。随后在执行instantiateItem的时候,首先从mFragments找相应位置的fragment,并且找到mSavedState相应位置的state数据,进行页面的恢复。
有了上面的这两个知识点,我们就知道接下来要怎么做,首先我们在给fragment确定位置的时候,也就是在getItemPosition方法中,我们根据当前的fragment对象在mFragments确定新的位置。如果位置发生变化,则会重新刷新。代码如下:
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 (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);
// 这里会做刷新操作,设置新的current item位置
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();
}
}
所以一切的根源,就是在数据集发生变化时,需要通知对应位置的** mFragments和mSavedState**对象,插入null对象。
举例
如果我们要在数据集的头部插入数据,我们可以这么做,在copy源码的DynamicFragmentStatePagerAdapter类中加入我们的自定义方法,例如:
public void insertEmptyHeaderFragment() {
mFragments.add(0, null);
mSavedState.add(0, null);
}
在数据集插入单个数据的同时,也在该方法中插入对应的null,这样位置和值才能对的上,以至于恢复页面的时候,不会找错save state而在对的位置产生错误的页面。举例
fun insertData(data: XXX) {
dataSet?.add(0, data)
insertEmptyHeaderFragment()
}
插入其他位置,我相信对同学们应该也不难了。记得数据集更新后要调用adapter.notifyDataSetChanged()方法。这样就会去刷新数据集了。
总结
重点是改变getItemPosition()的位置计算,并且在更新数据集的时候,更新mFragments和mSavedState的位置。本文是根据使用ViewPager+FragmentStatePagerAdapter来举例,如果是其他adapter类,相信你经过上面的介绍之后,应该不是什么难事。最后就是,遇到困难不用怕,分析,跟踪源码,不能通过继承实现的,就copy源码来改,只要思想不滑坡,方法总比困难多。欢迎有不清楚的同学,可以线下持续交流。