版權(quán)聲明:本文為作者原創(chuàng),轉(zhuǎn)載必須注明出處。
轉(zhuǎn)載請注明出處:http://www.lxweimin.com/p/b1cacb5605b4
1)FragmentStatePagerAdapter和FragmentPagerAdapter的異同:
相同點(diǎn):FragmentPagerAdapter、FragmentStatePagerAdapter均出自v4包,均繼承自 PagerAdapter,緩存策略也相同,都是加載當(dāng)前頁面及其左右兩邊臨近的頁面。
不同點(diǎn):
FragmentPagerAdapterdestroyItem() 中是 detach(),銷毀fragment:只走destroyView ,不走destroy。Fragment 還在 FragmentManager 管理中, ?Fragment 所占用的資源不會被釋放,只會銷毀視圖。FragmentPagerAdapter會對我們?yōu)g覽過Fragment進(jìn)行緩存,保存這些界面的臨時狀態(tài),這樣當(dāng)我們左右滑動的時候,界面切換更加的流暢。但是,這樣也會增加程序占用的內(nèi)存。
FragmentStatePagerAdapterdestroyItem() 中是remove(),銷毀fragment:既走destroyView ,又走destroy。Fragment 不在 FragmentManager 管理中,將 Fragment 移除,并釋放其資源。(就像 ListView 的實(shí)現(xiàn)一樣)。這么實(shí)現(xiàn)的好處就是當(dāng)擁有大量的頁面時,不必占用大量的內(nèi)存。
2)notifyDataSetChanged
在一個 Android 應(yīng)用中,我使用FragmentPagerAdapter來處理多 Fragment 頁面的橫向滑動。不過我碰到了一個問題,即當(dāng) Fragment 對應(yīng)的數(shù)據(jù)集發(fā)生改變時,我希望能夠通過調(diào)用 mAdapter.notifyDataSetChanged() 來觸發(fā) Fragment 頁面使用新的數(shù)據(jù)調(diào)整或重新生成其內(nèi)容,可是當(dāng)我調(diào)用 notifyDataSetChanged() 后,發(fā)現(xiàn)什么都沒發(fā)生。
搜索之后發(fā)現(xiàn)不止我一個人碰到這個問題,大家給出的解決辦法五花八門,有些確實(shí)解決了問題,但是我總感覺問題沒搞清楚。于是我決定搞明白這個問題到底是怎么回事,以及正確的用法到底如何。要搞明白這個問題,僅僅閱讀文檔并不足夠,還需要閱讀相關(guān)幾個類的相關(guān)方法的實(shí)現(xiàn),搞懂其設(shè)計(jì)意圖。下面就是通過閱讀源代碼搞明白的內(nèi)容。
【ViewPager】
ViewPager如其名所述,是負(fù)責(zé)翻頁的一個 View。準(zhǔn)確說是一個ViewGroup,包含多個 View 頁,在手指橫向滑動屏幕時,其負(fù)責(zé)對 View 進(jìn)行切換。為了生成這些 View 頁,需要提供一個PagerAdapter來進(jìn)行和數(shù)據(jù)綁定以及生成最終的 View 頁。
ViewPager 通過 setAdapter() 來建立與 PagerAdapter 的聯(lián)系。這個聯(lián)系是雙向的,一方面,ViewPager 會擁有PagerAdapter 對象,從而可以在需要時調(diào)用 PagerAdapter 的方法;另一方面,ViewPager 會在 setAdapter() 中調(diào)用 PagerAdapter 的 registerDataSetObserver() 方法,注冊一個自己生成的 PagerObserver 對象,從而在 PagerAdapter 有所需要時(如notifyDataSetChanged()或 notifyDataSetInvalidated() 時),可以調(diào)用 Observer 的 onChanged() 或 onInvalidated() 方法,從而實(shí)現(xiàn) PagerAdapter 向 ViewPager 方向發(fā)送信息。
在PagerObserver.onChanged(),以及 PagerObserver.onInvalide() 中被調(diào)用。因此當(dāng) PagerAdapter.notifyDataSetChanged() 被觸發(fā)時,ViewPager.dataSetChanged() 也可以被觸發(fā)。該函數(shù)將使用 getItemPosition() 的返回值來進(jìn)行判斷,如果為 POSITION_UNCHANGED,則什么都不做;如果為 POSITION_NONE,則調(diào)用 PagerAdapter.destroyItem() 來去掉該對象,并設(shè)置為需要刷新 (needPopulate = true) 以便觸發(fā)PagerAdapter.instantiateItem() 來生成新的對象。
【PagerAdapter】
PageAdapter是 ViewPager 的支持者,ViewPager 將調(diào)用它來取得所需顯示的頁,而 PageAdapter 也會在數(shù)據(jù)變化時,通知 ViewPager。這個類也是FragmentPagerAdapter以及FragmentStatePagerAdapter的基類。如果繼承自該類,至少需要實(shí)現(xiàn) instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
該函數(shù)用以返回給定對象的位置,給定對象是由 instantiateItem() 的返回值。
在 ViewPager.dataSetChanged() 中將對該函數(shù)的返回值進(jìn)行判斷,以決定是否最終觸發(fā) PagerAdapter.instantiateItem() 函數(shù)。
在 PagerAdapter 中的實(shí)現(xiàn)是直接傳回POSITION_UNCHANGED。如果該函數(shù)不被重載,則會一直返回 POSITION_UNCHANGED,從而導(dǎo)致 ViewPager.dataSetChanged() 被調(diào)用時,認(rèn)為不必觸發(fā) PagerAdapter.instantiateItem()。很多人因?yàn)闆]有重載該函數(shù),而導(dǎo)致調(diào)用
PagerAdapter.notifyDataSetChanged() 后,什么都沒有發(fā)生。
在每次 ViewPager 需要一個用以顯示的 Object 的時候,該函數(shù)都會被 ViewPager.addNewItem()調(diào)用。
在數(shù)據(jù)集發(fā)生變化的時候,一般 Activity 會調(diào)用 PagerAdapter.notifyDataSetChanged(),以通知 PagerAdapter,而 PagerAdapter 則會通知在自己這里注冊過的所有DataSetObserver。其中之一就是在 ViewPager.setAdapter() 中注冊過的 PageObserver。PageObserver 則進(jìn)而調(diào)用 ViewPager.dataSetChanged(),從而導(dǎo)致 ViewPager 開始觸發(fā)更新其內(nèi)含 View 的操作。
【FragmentPagerAdapter】
FragmentPagerAdapter繼承自 PagerAdapter。相比通用的 PagerAdapter,該類更專注于每一頁均為 Fragment 的情況。如文檔所述,該類內(nèi)的每一個生成的 Fragment 都將保存在內(nèi)存之中,因此適用于那些相對靜態(tài)的頁,數(shù)量也比較少的那種;如果需要處理有很多頁,并且數(shù)據(jù)動態(tài)性較大、占用內(nèi)存較多的情況,應(yīng)該使用FragmentStatePagerAdapter。FragmentPagerAdapter 重載實(shí)現(xiàn)了幾個必須的函數(shù),因此來自 PagerAdapter 的函數(shù),我們只需要實(shí)現(xiàn) getCount(),即可。且,由于 FragmentPagerAdapter.instantiateItem() 的實(shí)現(xiàn)中,調(diào)用了一個新增的虛函數(shù) getItem(),因此,我們還至少需要實(shí)現(xiàn)一個 getItem()。因此,總體上來說,相對于繼承自 PagerAdapter,更方便一些。
該類中新增的一個虛函數(shù)。函數(shù)的目的為生成新的Fragment 對象。重載該函數(shù)時需要注意這一點(diǎn)。在需要時,該函數(shù)將被 instantiateItem() 所調(diào)用。
如果需要向 Fragment 對象傳遞相對靜態(tài)的數(shù)據(jù)時,我們一般通過 Fragment.setArguments() 來進(jìn)行,這部分代碼應(yīng)當(dāng)放到 getItem()。它們只會在新生成 Fragment 對象時執(zhí)行一遍。
如果需要在生成 Fragment 對象后,將數(shù)據(jù)集里面一些動態(tài)的數(shù)據(jù)傳遞給該 Fragment,那么,這部分代碼不適合放到 getItem() 中。因?yàn)楫?dāng)數(shù)據(jù)集發(fā)生變化時,往往對應(yīng)的 Fragment 已經(jīng)生成,如果傳遞數(shù)據(jù)部分代碼放到了 getItem() 中,這部分代碼將不會被調(diào)用。這也是為什么很多人發(fā)現(xiàn)調(diào)用 PagerAdapter.notifyDataSetChanged() 后,getItem() 沒有被調(diào)用的一個原因。
函數(shù)中判斷一下要生成的 Fragment 是否已經(jīng)生成過了,如果生成過了,就使用舊的,舊的將被 Fragment.attach();如果沒有,就調(diào)用 getItem() 生成一個新的,新的對象將被 FragmentTransation.add()。
FragmentPagerAdapter 會將所有生成的 Fragment 對象通過 FragmentManager 保存起來備用,以后需要該 Fragment 時,都會從 FragmentManager 讀取,而不會再次調(diào)用 getItem() 方法。
如果需要在生成 Fragment 對象后,將數(shù)據(jù)集中的一些數(shù)據(jù)傳遞給該 Fragment,這部分代碼應(yīng)該放到這個函數(shù)的重載里。在我們繼承的子類中,重載該函數(shù),并調(diào)用 FragmentPagerAdapter.instantiateItem() 取得該函數(shù)返回 Fragment 對象,然后,我們該 Fragment 對象中對應(yīng)的方法,將數(shù)據(jù)傳遞過去,然后返回該對象。
否則,如果將這部分傳遞數(shù)據(jù)的代碼放到 getItem()中,在 PagerAdapter.notifyDataSetChanged() 后,這部分?jǐn)?shù)據(jù)設(shè)置代碼將不會被調(diào)用。
該函數(shù)被調(diào)用后,會對 Fragment 進(jìn)行FragmentTransaction.detach()。這里不是 remove(),只是 detach(),因此 Fragment 還在 FragmentManager 管理中,F(xiàn)ragment 所占用的資源不會被釋放。
【FragmentStatePagerAdapter】
FragmentStatePagerAdapter和前面的 FragmentPagerAdapter 一樣,是繼承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一樣的是,正如其類名中的 'State' 所表明的含義一樣,該 PagerAdapter 的實(shí)現(xiàn)將只保留當(dāng)前頁面,當(dāng)頁面離開視線后,就會被消除,釋放其資源;而在頁面需要顯示時,生成新的頁面(就像 ListView 的實(shí)現(xiàn)一樣)。這么實(shí)現(xiàn)的好處就是當(dāng)擁有大量的頁面時,不必在內(nèi)存中占用大量的內(nèi)存。
一個該類中新增的虛函數(shù)。
函數(shù)的目的為生成新的Fragment 對象。
Fragment.setArguments() 這種只會在新建 Fragment 時執(zhí)行一次的參數(shù)傳遞代碼,可以放在這里。
由于 FragmentStatePagerAdapter.instantiateItem() 在大多數(shù)情況下,都將調(diào)用 getItem() 來生成新的對象,因此如果在該函數(shù)中放置與數(shù)據(jù)集相關(guān)的 setter 代碼,基本上都可以在 instantiateItem() 被調(diào)用時執(zhí)行,但這和設(shè)計(jì)意圖不符。畢竟還有部分可能是不會調(diào)用 getItem() 的。因此這部分代碼應(yīng)該放到 instantiateItem() 中。
除非碰到 FragmentManager 剛好從 SavedState 中恢復(fù)了對應(yīng)的 Fragment 的情況外,該函數(shù)將會調(diào)用 getItem() 函數(shù),生成新的 Fragment 對象。新的對象將被 FragmentTransaction.add()。
FragmentStatePagerAdapter 就是通過這種方式,每次都創(chuàng)建一個新的 Fragment,而在不用后就立刻釋放其資源,來達(dá)到節(jié)省內(nèi)存占用的目的的。
將 Fragment 移除,即調(diào)用FragmentTransaction.remove(),并釋放其資源。
討論
之前看到一些解決辦法,有的認(rèn)為這是一個bug,應(yīng)該被修復(fù);有的建議不用 FragmentPagerAdapter,而改用 FragmentStatePagerAdapter,并且重載 getItemPosition() 并返回 POSITION_NONE,以觸發(fā)銷毀對象以及重建對象。從上面的分析中看,后者給出的建議確實(shí)可以達(dá)到調(diào)用 notifyDataSetChanged() 后,F(xiàn)ragment 被以新的參數(shù)重新建立的效果。
但是問題在于,如果我們只能這么解決這個問題,豈不是 FragmentPagerAdapter 就用不上了?最關(guān)鍵的是,二者對應(yīng)的情況不同。對于頁面相對較少的情況,我仍舊希望能夠?qū)⑸傻?Fragment 保存在內(nèi)存中,在需要顯示的時候直接調(diào)用,而不要產(chǎn)生生成、銷毀對象的額外的開銷,這樣效率更高。這種情況下,選擇 FragmentPagerAdapter 是更適合,不加考慮的選擇 FragmentStatePagerAdapter 是不合適的。我們不能夠因噎廢食。
因此,對于 FragmentPagerAdapter 的解決方案就是,分別重載 getItem() 以及 instantiateItem() 對象。getItem() 只用于生成新的與數(shù)據(jù)無關(guān)的 Fragment;而 instantiateItem() 函數(shù)則先調(diào)用父類中的 instantiateItem() 取得所對應(yīng)的 Fragment 對象,然后,根據(jù)對應(yīng)的數(shù)據(jù),調(diào)用該對象對應(yīng)的方法進(jìn)行數(shù)據(jù)設(shè)置。
當(dāng)然,不要忘記重載 getItemPosition() 函數(shù),返回 POSITION_NONE,這個兩個類的解決方案都需要的。二者不同之處在于,F(xiàn)ragmentStatePagerAdapter 在會在因 POSITION_NONE 觸發(fā)調(diào)用的 destroyItem() 中真正的釋放資源,重新建立一個新的 Fragment;而 FragmentPagerAdapter 僅僅會在 destroyItem() 中 detach 這個 Fragment,在 instantiateItem() 時會使用舊的 Fragment,并觸發(fā) attach,因此沒有釋放資源及重建的過程。
這樣,當(dāng) notifyDataSetChanged() 被調(diào)用后,會最終觸發(fā) instantiateItem(),而不管 getItem() 是否被調(diào)用,我們都在重載的 instantiateItem() 函數(shù)中已經(jīng)將所需要的數(shù)據(jù)傳遞給了相應(yīng)的 Fragment。在 Fragment 接下來的 onCreateView(), onStart() 以及 onResume() 的事件中,它可以正確的讀取新的數(shù)據(jù),F(xiàn)ragment 被成功復(fù)用了。
這里需要注意一個問題,在 Fragment 沒有被添加到 FragmentManager 之前,我們可以通過 Fragment.setArguments() 來設(shè)置參數(shù),并在 Fragment 中,使用 getArguments() 來取得參數(shù)。這是常用的參數(shù)傳遞方式。但是這種方式對于我們說的情況不適用。因?yàn)檫@種數(shù)據(jù)傳遞方式只可能用一次,在 Fragment 被添加到 FragmentManager 后,一旦被使用,我們再次調(diào)用 setArguments() 將會導(dǎo)致java.lang.IllegalStateException: Fragment already active異常。因此,我們這里的參數(shù)傳遞方式選擇是,在繼承的 Fragment 子類中,新增幾個 setter,然后通過這些 setter 將數(shù)據(jù)傳遞過去。反向也是類似。相關(guān)信息可以參考 [5]。哦,這些 setter 中要注意不要操作那些 View,這些 View 只有在 onCreateView() 事件后才可以操作。
針對 FragmentPagerAdapter 的解決辦法如下列代碼所示:
[mw_shl_code=java,true]
@Override
public Fragment getItem(int position) {
MyFragment f = new MyFragment();
return f;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
MyFragment f = (MyFragment) super.instantiateItem(container, position);
String title = mList.get(position);
f.setTitle(title);
return f;
}
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
3)ViewPager.setOffscreenPageLimit:預(yù)加載的頁面數(shù),
4)getFragmentManager()和getChildFragmentManager()的區(qū)別:
需要管理相互獨(dú)立的并且隸屬于Activity的Fragment使用FragmentManager(),而在Fragment中動態(tài)的添加Fragment要使用getChildFragmetManager()來管理。