在Android開發(fā)中,我們應(yīng)該使用到很高頻率的一個控件就是ViewPager乌逐。但是在使用ViewPager的過程中擎椰,我們會發(fā)現(xiàn)有兩個問題,一是不能關(guān)閉預(yù)加載钟哥;二是更新ViewPager的Adapter不生效迎献。所以我在這里以FragmentStatePagerAdapter為例,探討一下為什么更新adapter無法生效腻贰,并且提出解決方案吁恍。
為什么Adapter更新不生效
更新不生效其實很簡單,我們看一下源碼的調(diào)用過程
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();
}
}
// 接著調(diào)用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);
// 調(diào)用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;
}
默認(rèn)是返回POSITION_UNCHANGED并徘,所以結(jié)合上面的邏輯
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
這里跳出循環(huán),所以其實什么事情都沒有做盛杰,也沒辦法因為數(shù)據(jù)集的變更而變更UI禽捆。
如何解決
其實ViewPager.dataSetChanged()
方法已經(jīng)為我們預(yù)留了更新的邏輯。
......
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()
進行改造肄程。
復(fù)制FragmentStatePagerAdapter源碼
由于改造getItemPosition()
方法需要對源碼進行操作锣吼,所以我們首先需要復(fù)制一份源碼,暫時叫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.
*
* <p>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.
*/
簡單來說就是就是根據(jù)這個方法玄叠,來判斷當(dāng)前item的位置是否發(fā)生了改變,返回值包括POSITION_UNCHANGED拓提、POSITION_NONE读恃、以及[0, {@link #getCount()}。所以當(dāng)我們數(shù)據(jù)集發(fā)生變化的時候代态,其實fragment的位置寺惫,也要相應(yīng)的發(fā)生變化否則就會在錯誤的位置獲取到錯誤的頁面,會發(fā)生頁面顯示錯亂蹦疑。
mFragments對象是用來緩存當(dāng)前adapter維護的在內(nèi)存中的fragment緩存對象西雀。是一個數(shù)組結(jié)構(gòu),數(shù)組會隨著數(shù)據(jù)集的增大而增大歉摧,數(shù)組的內(nèi)容是fragment對象艇肴,會根據(jù)當(dāng)前viewpager位置,保存的ViewPager.setOffscreenPageLimit(size)
中size的大小的實例叁温,ViewPager進行滑動時再悼,超出page limit的頁面會被執(zhí)行destroyItem
方法,并且預(yù)加載的fragment會執(zhí)行instantiateItem
方法膝但,確保adapter中只保留相應(yīng)數(shù)量的fragment實例冲九。這里不展開說,可以詳細(xì)看下FragmentStatePagerAdapter.instantiateItem(@NonNull ViewGroup container, int position)
的方法源碼跟束。
mSavedState對象用來緩存adapter中所有fragment執(zhí)行onSaveInstanceState()
之后的數(shù)據(jù)莺奸,在destroyItem
方法中調(diào)用被銷毀的fragment的onSaveInstanceState方法,并把數(shù)據(jù)放在mSavedState對應(yīng)的位置泳炉。隨后在執(zhí)行instantiateItem
的時候憾筏,首先從mFragments找相應(yīng)位置的fragment,并且找到mSavedState相應(yīng)位置的state數(shù)據(jù)花鹅,進行頁面的恢復(fù)氧腰。
有了上面的這兩個知識點,我們就知道接下來要怎么做刨肃,首先我們在給fragment確定位置的時候古拴,也就是在getItemPosition
方法中,我們根據(jù)當(dāng)前的fragment對象在mFragments確定新的位置真友。如果位置發(fā)生變化黄痪,則會重新刷新。代碼如下:
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);
......
......
// 這里發(fā)現(xiàn)位置和原來的位置不一樣盔然,說明發(fā)生了變化桅打,
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);
// 這里會做刷新操作是嗜,設(shè)置新的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();
}
}
所以一切的根源,就是在數(shù)據(jù)集發(fā)生變化時挺尾,需要通知對應(yīng)位置的** mFragments和mSavedState**對象鹅搪,插入null對象。
舉例
如果我們要在數(shù)據(jù)集的頭部插入數(shù)據(jù)遭铺,我們可以這么做丽柿,在copy源碼的DynamicFragmentStatePagerAdapter
類中加入我們的自定義方法,例如:
public void insertEmptyHeaderFragment() {
mFragments.add(0, null);
mSavedState.add(0, null);
}
在數(shù)據(jù)集插入單個數(shù)據(jù)的同時魂挂,也在該方法中插入對應(yīng)的null甫题,這樣位置和值才能對的上,以至于恢復(fù)頁面的時候涂召,不會找錯save state而在對的位置產(chǎn)生錯誤的頁面坠非。舉例
fun insertData(data: XXX) {
dataSet?.add(0, data)
insertEmptyHeaderFragment()
}
插入其他位置,我相信對同學(xué)們應(yīng)該也不難了芹扭。記得數(shù)據(jù)集更新后要調(diào)用adapter.notifyDataSetChanged()
方法麻顶。這樣就會去刷新數(shù)據(jù)集了。
總結(jié)
重點是改變getItemPosition()的位置計算舱卡,并且在更新數(shù)據(jù)集的時候,更新mFragments和mSavedState的位置队萤。本文是根據(jù)使用ViewPager+FragmentStatePagerAdapter來舉例轮锥,如果是其他adapter類,相信你經(jīng)過上面的介紹之后要尔,應(yīng)該不是什么難事舍杜。最后就是,遇到困難不用怕赵辕,分析既绩,跟蹤源碼,不能通過繼承實現(xiàn)的还惠,就copy源碼來改饲握,只要思想不滑坡,方法總比困難多蚕键。歡迎有不清楚的同學(xué)救欧,可以線下持續(xù)交流。