一、概述
在平時的開發當中,用到ViewPager
的場景主要是以下兩種:
- 對於主頁中的每個子頁面,用
Fragment
包裹起來,然後通過ViewPager
來實現頁面之間的切換。 - 廣告輪播圖。
其中對於第一種情況,我們常常會使用到兩個PagerAdapter
的實現類,也就是FragmentStatePagerAdapter
和FragmentPagerAdapter
,今天,我們就來學習一下它們的使用方法,並進行對比。
二、FragmentPagerAdapter
2.1 使用
在我們的例子中,我們定義了一個Acitivity
,它的佈局中包含有一個ViewPager
。初始時候我們會給mFragments
列表中新建4
個Fragment
例項,然後把它傳給繼承於FragmentPagerAdapter
的介面卡,LogcatFragment
就是用來列印Fragment
的生命週期:
private void initFPAFragments() {
mFragments = new ArrayList<>();
for (int i = 0; i < INCREASE; i++) {
//初始時刻有4個Fragment,每個Fragment和一條資料相關聯.
mFragments.add(LogcatFragment.newInstance("index=" + i));
}
ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
mFPAdapter = new FPAdapter(getSupportFragmentManager(), mFragments);
viewPager.setAdapter(mFPAdapter);
}
private class FPAdapter extends FragmentPagerAdapter {
private List<Fragment> mFragments;
public FPAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
mFragments = fragments;
}
@Override
public Fragment getItem(int position) {
Log.d("LogcatFragment", "get Item from FPAdapter, position=" + position);
return mFragments.get(position);
}
@Override
public int getCount() {
return mFragments.size();
}
}
複製程式碼
2.2 現象
使用過ViewPager
的同學都知道,ViewPager
有一個setOffscreenPageLimit
,它表示對於當ViewPager
處於IDLE
狀態時,它的左右兩端最多會保留多少個頁面,對於超出這個範圍的頁面有可能會需要從PageAdapter
中進行重建,這裡我們設定的是1
,下面我們進行一系列的操作,並觀察此時各個頁面及其內部的Fragment
的變化情況:
- 第一步:當我們第一次啟動
Activity
的時候,預設會新增它的左右兩個介面,由於我們位於第一個(index=0
),因此會新增它及其右邊的介面(index=0
),此時這兩個頁面當中內部的Fragment
的生命週期如下圖所示: 從我們經常看到的Fragment
生命週期的圖來看,就是下面紅色的部分: - 第二步:下面,我們滑動到
index=1
的介面,此時index=2
的頁面會被新增,它內部的Fragment
所走的生命週期和上面完全相同,由於index=1
左右兩邊的介面個數都為1
,因此不會有頁面被移除。 - 第三步:繼續往右滑動到
index=2
的介面,此時會新增index=3
的頁面,並移除index=0
的頁面,其內部包含的Fragment
的生命週期列印為: 可以看到對於新增的index=3
的頁面而言,它內部的Fragment
所走的生命週期和index=0/1/2
相同,而被移除的index=0
的頁面內部的Fragment
所走的生命週期為: - 第四步:向右滑動到
index=1
的介面,此時index=0
的介面需要被重新新增,而index=3
的介面則需要被移除,此時的列印為: 這時候,對於重新新增的頁面index=0
,它和第一次新增的時候有兩點不同: - 沒有再去自定義的
FragmentPagerAdapter
中取Fragment
- 其內部的
Fragment
所走的生命週期不同,此時為:
最後,我們總結一下,對於三種情況的頁面內部的Fragment
所走生命週期的區別如下圖所示:
- 第一次新增的頁面
- 重新新增的頁面
- 移除的頁面
2.3 原始碼解析
現在,我們就開始解釋一下,為什麼第一次新增和重新新增的頁面內部對應的Fragment
會有所不同,我們只需要關注FragmentPagerAdapter
內的兩個函式:
public Object instantiateItem(ViewGroup container, int position)
,新增頁面時回撥。public void destroyItem(ViewGroup container, int position, Object object)
,移除頁面時回撥。
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
//這裡的itemId返回的是對應position的頁面的唯一識別符號.
final long itemId = getItemId(position);
//1.先是通過FragmentManager來找.
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
//2.如果找到了,那麼呼叫attach方法.
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
//3.如果沒找到,那麼通過子類實現的getItem方法來獲取.
fragment = getItem(position);
//這裡呼叫的是add方法.
mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
}
//根據需要,回撥下面這兩個方法.
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
//返回給ViewPager.
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
//4.移除介面時,呼叫的是detach方法.
mCurTransaction.detach((Fragment)object);
}
複製程式碼
注意看上面的註釋,就能解釋上面我們看到的現象了:
- 第一次新增介面的時候,由於
FragmentManager
中沒有這個Fragment
,因此需要通過自定義的FragmentPagerAdapter
獲取,然後呼叫add
方法,也就是上面程式碼中的第**(3)**步,所走的生命週期為onAttach() -> onResume()
。 - 移除介面時,使用的是
detach
方法,也就是上面程式碼中的第**(4)**步,接觸過Fragment
的人都知道,這時候僅僅是Fragment
的介面被從View
樹上移除了而已,它的例項仍然被儲存在FragmentManager
當中,所走的生命週期為onPause() -> onDestroyView()
。 - 重新新增介面時,由於此時去
FragmentManager
中能找到那個Fragment
,所以呼叫的是attach
方法,也就是上面程式碼中的第**(2)**步,所走的生命週期為onCreateView() -> onResume()
,並且不需要再從自定義的FragmentPagerAdapter
中獲取Fragment
。
整個邏輯如下圖所示:
三、FragmentStatePagerAdapter
3.1 現象
我們的程式碼基本不用改動,只需要把原來繼承於FragmentPagerAdapter
的子類替換為繼承FragmentStatePagerAdapter
就可以了。在第二章當中,我們分析得很詳細,相信大家對於整個分析的套路已經理解,因此,為了減少篇幅,我們直接說結論,當進行和上面相同的操作之後,把頁面分為三種型別:
- 第一次新增
- 重新新增
- 移除
此時,它們內部的Fragment
所走的生命週期為:
對於重新新增和移除的介面,其內部的Fragment
所走的生命週期都和FragmentPagerAdapter
不同,下面,我們就從原始碼的角度,來看一下導致這些區別的原因。
3.2 原始碼解析
和前面類似,我們只關注新增和移除時呼叫的那兩個方法:
@Override
public Object instantiateItem(ViewGroup container, int position) {
//如果mFragments中存在對應位置的fragment,那麼直接返回.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);
//多了恢復狀態的操作.
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
//保證mFragments的大小和ViewPager往右滑動的最遠的index相同.
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
//設定對應位置.
mFragments.set(position, fragment);
//這裡很關鍵,呼叫的add方法.
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
while (mSavedState.size() <= position) {
mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);
//呼叫的是remove方法.
mCurTransaction.remove(fragment);
}
複製程式碼
從上面的原始碼當中,總結出以下幾點:
FragmentStatePagerAdapter
在移除頁面的時候,呼叫的是remove
方法,也就是說,FragmentManager
中不再有這個Fragment
的例項,所走的生命週期為onPause() -> onDetach()
。- 無論是新增頁面還是重新新增頁面,它是通過
add
方法,並且每次都會通過自定義的FragmentStatePagerAdapter
子類的getItem
方法來獲取Fragment
,所以它們內部的Fragment
所走生命週期相同,都是從onAttach() -> onResume()
。 - 對於
ViewPager
當前介面中所對應的Fragment
,是通過一個mFragments
列表來管理的,由於此時沒有FragmentManager
來幫我們實現Fragment
集合的狀態的儲存和恢復,所以就需要我們自己實現onSave/onRestore
方法來進行狀態的儲存和恢復。
整個流程如下圖所示:
四、總結
FragmentPagerAdapter
和FragmentStatePagerAdapter
最大的區別就在於前者會把所有Fragment
的示例都快取在記憶體當中,而後者僅僅儲存了ViewPager
當前存在的頁面所對應的Fragment
,當頁面被移除之後,這個Fragment
的示例它也就不再儲存了。
當然,在我們前面的例子中,雖然使用了FragmentStatePagerAdapter
,但是由於我們在DemoActivity
中用一個列表儲存了所有的Fragment
例項,因此它沒有被回收,如果希望讓頁面被移除的時候,其對應的Fragment
例項也被回收,那麼我們的FragmentStatePagerAdapter
的子類應該寫成這樣:
private void initFSPAFragments() {
ViewPager viewPager = (ViewPager) findViewById(R.id.vp_content);
mFSPAdapter = new FSPAdapter(getSupportFragmentManager());
viewPager.setAdapter(mFSPAdapter);
}
private class FSPAdapter extends FragmentStatePagerAdapter {
public FSPAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Log.d("LogcatFragment", "get Item from FSPAdapter, position=" + position);
return LogcatFragment.newInstance("index=" + position);
}
@Override
public int getCount() {
return INCREASE;
}
}
複製程式碼