轉:http://www.apkbus.com/android-90417-1-1.html
在一個 Android 應用中,我使用 FragmentPagerAdapter 來處理多 Fragment 頁面的橫向滑動。不過我碰到了一個問題,即當 Fragment 對應的數據集發生改變時,我希望能夠通過調用 mAdapter.notifyDataSetChanged() 來觸發 Fragment 頁面使用新的數據調整或重新生成其內容,可是當我調用 notifyDataSetChanged() 后,發現什么都沒發生。 搜索之后發現不止我一個人碰到這個問題,大家給出的解決辦法五花八門,有些確實解決了問題,但是我總感覺問題沒搞清楚。於是我決定搞明白這個問題到底是怎么回事,以及正確的用法到底如何。要搞明白這個問題,僅僅閱讀文檔並不足夠,還需要閱讀相關幾個類的相關方法的實現,搞懂其設計意圖。下面就是通過閱讀源代碼搞明白的內容。 【ViewPager】 ViewPager 如其名所述,是負責翻頁的一個 View。准確說是一個 ViewGroup,包含多個 View 頁,在手指橫向滑動屏幕時,其負責對 View 進行切換。為了生成這些 View 頁,需要提供一個 PagerAdapter 來進行和數據綁定以及生成最終的 View 頁。
【PagerAdapter】
PageAdapter 是 ViewPager 的支持者,ViewPager 將調用它來取得所需顯示的頁,而 PageAdapter 也會在數據變化時,通知 ViewPager。這個類也是
FragmentPagerAdapter 以及
FragmentStatePagerAdapter 的基類。如果繼承自該類,至少需要實現 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
【FragmentPagerAdapter】 FragmentPagerAdapter 繼承自 PagerAdapter。相比通用的 PagerAdapter,該類更專注於每一頁均為 Fragment 的情況。如文檔所述,該類內的每一個生成的 Fragment 都將保存在內存之中,因此適用於那些相對靜態的頁,數量也比較少的那種;如果需要處理有很多頁,並且數據動態性較大、占用內存較多的情況,應該使用FragmentStatePagerAdapter。FragmentPagerAdapter 重載實現了幾個必須的函數,因此來自 PagerAdapter 的函數,我們只需要實現 getCount(),即可。且,由於 FragmentPagerAdapter.instantiateItem() 的實現中,調用了一個新增的虛函數 getItem(),因此,我們還至少需要實現一個 getItem()。因此,總體上來說,相對於繼承自 PagerAdapter,更方便一些。
【FragmentStatePagerAdapter】 FragmentStatePagerAdapter 和前面的 FragmentPagerAdapter 一樣,是繼承子 PagerAdapter。但是,和 FragmentPagerAdapter 不一樣的是,正如其類名中的 'State' 所表明的含義一樣,該 PagerAdapter 的實現將只保留當前頁面,當頁面離開視線后,就會被消除,釋放其資源;而在頁面需要顯示時,生成新的頁面(就像 ListView 的實現一樣)。這么實現的好處就是當擁有大量的頁面時,不必在內存中占用大量的內存。
討論 之前看到一些解決辦法,有的認為這是一個 bug,應該被修復;有的建議不用 FragmentPagerAdapter,而改用 FragmentStatePagerAdapter,並且重載 getItemPosition() 並返回 POSITION_NONE,以觸發銷毀對象以及重建對象。從上面的分析中看,后者給出的建議確實可以達到調用 notifyDataSetChanged() 后,Fragment 被以新的參數重新建立的效果。 但是問題在於,如果我們只能這么解決這個問題,豈不是 FragmentPagerAdapter 就用不上了?最關鍵的是,二者對應的情況不同。對於頁面相對較少的情況,我仍舊希望能夠將生成的 Fragment 保存在內存中,在需要顯示的時候直接調用,而不要產生生成、銷毀對象的額外的開銷,這樣效率更高。這種情況下,選擇 FragmentPagerAdapter 是更適合,不加考慮的選擇 FragmentStatePagerAdapter 是不合適的。我們不能夠因噎廢食。 因此,對於 FragmentPagerAdapter 的解決方案就是,分別重載 getItem() 以及 instantiateItem() 對象。getItem() 只用於生成新的與數據無關的 Fragment;而 instantiateItem() 函數則先調用父類中的 instantiateItem() 取得所對應的 Fragment 對象,然后,根據對應的數據,調用該對象對應的方法進行數據設置。 當然,不要忘記重載 getItemPosition() 函數,返回 POSITION_NONE,這個兩個類的解決方案都需要的。二者不同之處在於,FragmentStatePagerAdapter 在會在因 POSITION_NONE 觸發調用的 destroyItem() 中真正的釋放資源,重新建立一個新的 Fragment;而 FragmentPagerAdapter 僅僅會在 destroyItem() 中 detach 這個 Fragment,在 instantiateItem() 時會使用舊的 Fragment,並觸發 attach,因此沒有釋放資源及重建的過程。 這樣,當 notifyDataSetChanged() 被調用后,會最終觸發 instantiateItem(),而不管 getItem() 是否被調用,我們都在重載的 instantiateItem() 函數中已經將所需要的數據傳遞給了相應的 Fragment。在 Fragment 接下來的 onCreateView(), onStart() 以及 onResume() 的事件中,它可以正確的讀取新的數據,Fragment 被成功復用了。 這里需要注意一個問題,在 Fragment 沒有被添加到 FragmentManager 之前,我們可以通過 Fragment.setArguments() 來設置參數,並在 Fragment 中,使用 getArguments() 來取得參數。這是常用的參數傳遞方式。但是這種方式對於我們說的情況不適用。因為這種數據傳遞方式只可能用一次,在 Fragment 被添加到 FragmentManager 后,一旦被使用,我們再次調用 setArguments() 將會導致 java.lang.IllegalStateException: Fragment already active 異常。因此,我們這里的參數傳遞方式選擇是,在繼承的 Fragment 子類中,新增幾個 setter,然后通過這些 setter 將數據傳遞過去。反向也是類似。相關信息可以參考 [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; }[/mw_shl_code] |