一、概述
在上一篇文章中,我們通過原始碼的角度瞭解FragmentPagerAdapter
和FragmentStatePagerAdapter
的原理。這其實是為我們分析資料更新問題做一個鋪墊。
在實際的開發當中,我們在ViewPager
中巢狀的Fragment
中並不是固定不變的,需要動態地新增和刪除,下面我們就從幾個大家經常會遇到的問題入手,然後分析問題的原因,最後我們嘗試總結一種比較好的資料更新方式。
二、使用FragmentPagerAdapter
2.1 一段有問題的程式碼
public class DemoActivity extends AppCompatActivity {
private static final int INCREASE = 4;
private FPAdapter mFPAdapter;
private List<String> mTitles;
private int mGroup = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_not_update);
TextView updateTv = (TextView) findViewById(R.id.tv_update);
updateTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateFragments();
}
});
initFPAFragments();
}
private void initFPAFragments() {
mTitles = new ArrayList<>();
for (int i = 0; i < INCREASE; i++) {
mTitles.add("index=" + i + ",group=0");
}
ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
mFPAdapter = new FPAdapter(getSupportFragmentManager(), mTitles);
viewPager.setAdapter(mFPAdapter);
}
private void updateFragments() {
mTitles.clear();
mGroup++;
for (int i = 0; i < INCREASE; i++) {
mTitles.add("index=" + i + ",group=" + mGroup);
}
mFPAdapter.notifyDataSetChanged();
}
private class FPAdapter extends FragmentPagerAdapter {
private List<String> mTitles;
public FPAdapter(FragmentManager fm, List<String> titles) {
super(fm);
mTitles = titles;
}
@Override
public Fragment getItem(int position) {
Log.d("LogcatFragment", "get Item from FPAdapter, position=" + position);
return LogcatFragment.newInstance(mTitles.get(position));
}
@Override
public int getCount() {
return mTitles.size();
}
}
}
複製程式碼
之所以會寫出這樣的程式碼,很大一部分是受到我們平時寫ListView
中BaseAdapter
的影響,因為我們淺意識地認為,呼叫了notifyDataSetChanged()
方法之後,ViewPager
就會去呼叫getItem
方法來獲取新的Fragment
以替換舊的Fragment
,就好像我們使用ListVIew
的時候,它會去回撥BaseAdapter
的getView
方法來獲取新的View
一樣,執行上面的Demo
,會有發現以下幾個問題:
- 第一個問題:呼叫
notifyDataSetChanged()
之後,ViewPager
當前存在的頁面中的Fragment
不會發生變化。 - 第二個問題:對於重新新增的介面,不會回撥
getItem
來獲取新的Fragment
。
2.2 原因分析 - 問題一
我們首先分析問題一:呼叫notifyDataSetChanged()
之後,ViewPager
當前存在的頁面中的Fragment
不會發生變化。
首先,我們確定分析的場景:啟動DemoActivity
,按照之前的分析,現在會給ViewPager
新增兩個頁面,分別是index=0
和index=1
,接著我們呼叫PagerAdapter#notifyDataSetChanged()
,最終會走到ViewPager#dataSetChanged
方法,我們看一下里面做了什麼:
void dataSetChanged() {
//在我們的例子中,返回的是4
final int adapterCount = mAdapter.getCount();
mExpectedAdapterCount = adapterCount;
//此時為true.
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);
//這裡是關鍵,預設都是返回POSITION_UNCHANGED
final int newPos = mAdapter.getItemPosition(ii.object);
//第一種情況:如果返回的是POSITION_UNCHANGED,那麼表示這個介面在ViewPager中的位置沒有變,那麼不需要更新.
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
//第二種情況:如果返回的是POSITION_NONE,就表示這個介面在ViewPager中不存在了,那麼就把它移除.
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;
}
//第三種情況:介面仍然存在,但是其在ViewPager中的位置發生了改變.
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();
}
}
複製程式碼
要理解上面的這段程式碼,首先要明白mItems
是什麼,以及mItems
的ItemInfo
中各個成員變數的含義:
mItems
中的每一個ItemInfo
和存在與ViewPager
中的介面一一關聯。ItemInfo
中的含義:
static class ItemInfo {
Object object; //通過PagerAdapter#instantiateItem所返回的Object.
int position; //這個Item所處的位置,也就是上面我們所說的index.
boolean scrolling;
float widthFactor;
float offset;
}
複製程式碼
此時,也就是我們位於index=0
的頁面,mItems
的內容為:
index=2
的頁面,那麼mItems
的內容變為:
我們注意上面有一句關鍵的話:
final int newPos = mAdapter.getItemPosition(ii.object);
複製程式碼
對於它的返回值,有三種處理方式:
PagerAdapter.POSITION_UNCHANGED
:這個ItemInfo
在整個ViewPager
的位置沒有發生改變。PagerAdapter.POSITION_NONE
:這個ItemInfo
在整個ViewPager
中已經不存在了。ii.position != newPos
,也就是說ItemInfo
在ViewPager
仍然需要存在,但是它的位置發生了改變。
也就是說,notifyDataSetChanged()
只處理ViewPager
當前已經存在的介面,而對於這些介面如何處理,則要依賴於getItemPosition
的返回值,但是**FragmentPagerAdapter
的返回值預設是POSITION_UNCHANGED
**,因此,當前已經存在的介面不會發生任何改變。
2.3 原因分析 - 問題二
問題二:對於重新新增的介面,不會回撥getItem
來獲取新的Fragment
。
這個其實和notifyDataSetChanged()
沒有關係,而是和FragmentPagerAdapter
尋找Fragment
的方式有關,它會優先從FragmentManager
中尋找,找不到了才會回撥getItem
來取新的Fragment
。
但是我們在移除介面的是呼叫的是detach
方法,因此FragmentManager
中仍然儲存了Fragment
的例項,在重新新增的時候就不會再回撥getItem
來取了,這個我們在前一篇文章中已經分析過,就不貼具體的程式碼了。
三、FragmentStatePagerAdapter
我們把上面例子中的FragmentPagerAdapter
替換成為FragmentStatePagerAdapter
,採用一樣的更新方式,此時呼叫notifyDataSetChanged()
之後,ViewPager
當前存在的頁面中的Fragment
依然不會發生變化,不重新整理的原因和FragmentPagerAdapter
是相同的。
與FragmentPagerAdapter
不同的是,對於重新新增的介面,會回撥getItem
來獲取新的Fragment
,這個原因在之前的文章中也分析過了。
四、實現一個高效的動態FragmentPagerAdapter
我們的需求和下面的這個介面類似:
需求包括:- 支援動態地新增和移除介面,介面的個數和頻道的個數相同,並且是可變的。
- 當頻道發生變化時,介面也要根據頻道的順序進行相應的改變,但是,如果上次存在的頻道,在編輯之後仍然存在,那麼應當複用之前的介面。
因此,我們繼承於FramentStatePagerAdapter
:
public abstract class FixedPagerAdapter<T> extends FragmentStatePagerAdapter {
private List<ItemObject> mCurrentItems = new ArrayList<>();
public FixedPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
while (mCurrentItems.size() <= position) {
mCurrentItems.add(null);
}
Fragment fragment = (Fragment) super.instantiateItem(container, position);
ItemObject object = new ItemObject(fragment, getItemData(position));
mCurrentItems.set(position, object);
return object;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
mCurrentItems.set(position, null);
super.destroyItem(container, position, ((ItemObject) object).fragment);
}
@Override
public int getItemPosition(Object object) {
ItemObject itemObject = (ItemObject) object;
if (mCurrentItems.contains(itemObject)) {
T oldData = itemObject.t;
int oldPosition = mCurrentItems.indexOf(itemObject);
T newData = getItemData(oldPosition);
if (equals(oldData, newData)) {
return POSITION_UNCHANGED;
} else {
int newPosition = getDataPosition(oldData);
return newPosition >= 0 ? newPosition : POSITION_NONE;
}
}
return POSITION_UNCHANGED;
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, ((ItemObject) object).fragment);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return super.isViewFromObject(view, ((ItemObject) object).fragment);
}
public abstract T getItemData(int position);
public abstract int getDataPosition(T t);
public abstract boolean equals(T oldD, T newD);
public class ItemObject {
public Fragment fragment;
public T t;
public ItemObject(Fragment fragment, T t) {
this.fragment = fragment;
this.t = t;
}
}
}
複製程式碼
這裡:
- 我們通過一個
mCurrentItems
儲存了當前頁面中對應的Fragment
和其所包含的資料。 - 最重要的是我們重寫了
getItemPosition
方法,根據不同的情況返回位置,這裡需要子類提供三個方面的資訊: - 新資料在某個位置的資料。
- 某個資料在新資料中的位置。
- 判斷兩個資料是否相等的標準。
現在,我們的Adapter
只需要重寫很少的程式碼,就能實現資料的更新:
public class DemoActivity extends AppCompatActivity {
private static final int INCREASE = 4;
private FixedPagerAdapter mFixedPagerAdapter;
private List<String> mTitles;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_not_update);
TextView updateTv = (TextView) findViewById(R.id.tv_update);
updateTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateFragments();
}
});
initFragments();
}
private void initFragments() {
mTitles = new ArrayList<>();
for (int i = 0; i < INCREASE; i++) {
mTitles.add(String.valueOf(i));
}
ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
mFixedPagerAdapter = new MyFixedPagerAdapter(getSupportFragmentManager(), mTitles);
viewPager.setAdapter(mFixedPagerAdapter);
}
private void updateFragments() {
mTitles.clear();
mTitles.add("3");
mTitles.add("2");
mFixedPagerAdapter.notifyDataSetChanged();
}
private class MyFixedPagerAdapter extends FixedPagerAdapter<String> {
private List<String> mTitles;
public MyFixedPagerAdapter(FragmentManager fragmentManager, List<String> titles) {
super(fragmentManager);
mTitles = titles;
}
@Override
public String getItemData(int position) {
return mTitles.size() > position ? mTitles.get(position) : null;
}
@Override
public int getDataPosition(String s) {
return mTitles.indexOf(s);
}
@Override
public boolean equals(String oldD, String newD) {
return TextUtils.equals(oldD, newD);
}
@Override
public Fragment getItem(int position) {
return LogcatFragment.newInstance(mTitles.get(position));
}
@Override
public int getCount() {
return mTitles.size();
}
}
}
複製程式碼