How to update and replace fragment in viewpager?php
在瞭解ViewPager的工做原理以前,先回顧ListView的工做原理:html
ListView只有在須要顯示某些列表項時,它纔會去申請可用的視圖對象;若是爲全部的列表項數據建立視圖對象,會浪費內存;java
ListView找誰去申請視圖對象呢? 答案是adapter。adapter是一個控制器對象,負責從模型層獲取數據,建立並填充必要的視圖對象,將準備好的視圖對象返回給ListView;android
首先,經過調用adapter的getCount()方法,ListView詢問數組列表中包含多少個對象(爲避免出現數組越界的錯誤);緊接着ListView就調用adapter的getView(int, View, ViewGroup)方法。git
ViewPager某種程度上相似於ListView,區別在於:ListView經過ArrayAdapter.getView(int position, View convertView, ViewGroup parent)
填充視圖;ViewPager經過FragmentPagerAdapter.getItem(int position)
生成指定位置的fragment.github
而咱們須要關注的是:編程
聲明:本文內容針對android.support.v4.app.*
ViewPager有兩個adapter:FragmentPagerAdapter和FragmentStatePagerAdapter:數組
繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,而且全部的Fragment實例一直保存在Fragment manager中。因此它適用於少許固定的fragment,好比一組用於分頁顯示的標籤。除了當Fragment不可見時,它的視圖層(view hierarchy)有可能被銷燬外,每頁的Fragment都會被保存在內存中。(翻譯自代碼文件的註釋部分)ide
繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,當Fragment不被須要時(好比不可見),整個Fragment都會被銷燬,除了saved state被保存外(保存下來的bundle用於恢復Fragment實例)。因此它適用於不少頁的狀況。(翻譯自代碼文件的註釋部分)
它倆的子類,須要實現getItem(int)
和 android.support.v4.view.PagerAdapter.getCount()
.
稍後作詳細分析:
// Set a PagerAdapter to supply views for this pager. ViewPager viewPager = (ViewPager) findViewById(R.id.my_viewpager_id); viewPager.setAdapter(mMyFragmentPagerAdapter); private FragmentPagerAdapter mMyFragmentPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public int getCount() { return 2; // Return the number of views available. } @Override public Fragment getItem(int position) { return new MyFragment(); // Return the Fragment associated with a specified position. } // Called when the host view is attempting to determine if an item's position has changed. @Override public int getItemPosition(Object object) { if (object instanceof MyFragment) { ((MyFragment)object).updateView(); } return super.getItemPosition(object); } }; private class MyFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // do something such as init data } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_my, container, false); // init view in the fragment return view; } public void updateView() { // do something to update the fragment } }
FragmentPagerAdapter和FragmentStatePagerAdapter對Fragment的管理略有不一樣,在詳細考察兩者區別以前,咱們經過兩種較爲直觀的方式先感覺下:
說明:這兩張圖片來自於《Android權威編程指南》,原圖有3個Fragment,我增長了1個Fragment,以及被調到的方法。
FragmentPagerAdapter的Fragment管理:
FragmentStatePageAdapter的Fragment管理:
好啦,感覺完畢,咱們須要探究其詳情,梳理adapter建立、銷燬Fragment的過程,過程當中adapter method和fragment lifecycle method哪些被調到,有哪些同樣,有哪些不同。
最開始處於第0頁時,adapter不只爲第0頁建立Fragment實例,還爲相鄰的第1頁建立了Fragment實例:
// 剛開始處在page0 D/Adapter (25946): getItem(0) D/Fragment0(25946): newInstance(2015-09-10) // 註釋:newInstance()調用了Fragment的構造器方法,下同。 D/Adapter (25946): getItem(1) D/Fragment1(25946): newInstance(Hello World, I'm li2.) D/Fragment0(25946): onAttach() D/Fragment0(25946): onCreate() D/Fragment0(25946): onCreateView() D/Fragment1(25946): onAttach() D/Fragment1(25946): onCreate() D/Fragment1(25946): onCreateView()
第1次從第0頁滑到第1頁,adapter一樣會爲相鄰的第2頁建立Fragment實例;
// 第1次滑到page1 D/Adapter (25946): onPageSelected(1) D/Adapter (25946): getItem(2) D/Fragment2(25946): newInstance(true) D/Fragment2(25946): onAttach() D/Fragment2(25946): onCreate() D/Fragment2(25946): onCreateView()
FragmentPagerAdapter和FragmentStatePagerAdapter齊聲說:吶,請主公貳放心,屬下定會爲您準備好相鄰的下一頁視圖噠!麼麼噠!
它倆對待下一頁的態度是相同的,但對於上上頁,它倆作出了不同的事情:
FragmentPagerAdapter說:上上頁的實例還保留着,只是銷燬了它的視圖:
// 第N次(N不等於1)向右滑動選中page2 D/Adapter (25946): onPageSelected(2) D/Adapter (25946): destroyItem(0) // 銷燬page0的視圖 D/Fragment0(25946): onDestroyView() D/Fragment3(25946): onCreateView() // page3的Fragment實例仍保存在FragmentManager中,因此只需建立它的視圖
FragmentStatePagerAdapter說:上上頁的實例和視圖都被俺銷燬啦:
// 第N次(N不等於1)向右滑選中page2 D/Adapter (27880): onPageSelected(2) D/Adapter (27880): destroyItem(0) // 銷燬page0的實例和視圖 D/Adapter (27880): getItem(3) // 建立page3的Fragment D/Fragment3(27880): newInstance() D/Fragment0(27880): onDestroyView() D/Fragment0(27880): onDestroy() D/Fragment0(27880): onDetach() D/Fragment3(27880): onAttach() D/Fragment3(27880): onCreate() D/Fragment3(27880): onCreateView()
// Return the Fragment associated with a specified position. public abstract Fragment getItem(int position);
當adapter須要一個指定位置的Fragment,而且這個Fragment不存在時,getItem就被調到,返回一個Fragment實例給adapter。
因此,有必要再次強調,getItem是建立一個新的Fragment,可是這個方法名可能會被誤認爲是返回一個已經存在的Fragment。
對於FragmentPagerAdapter,當每頁的Fragment被建立後,這個函數就不會被調到了。對於FragmentStatePagerAdapter,因爲Fragment會被銷燬,因此它仍會被調到。
因爲咱們必須在getItem中實例化一個Fragment,因此當getItem()被調用後,Fragment相應的生命週期函數也就被調到了:
D/Adapter (25946): getItem(1) D/Fragment1(25946): newInstance(Hello World, I'm li2.) // newInstance()調用了Fragment的構造器方法; D/Fragment1(25946): onAttach() D/Fragment1(25946): onCreate() D/Fragment1(25946): onCreateView()
// Remove a page for the given position. public void FragmentPagerAdapter.destroyItem(ViewGroup container, int position, Object object) { mCurTransaction.detach((Fragment)object); } public void FragmentStatePagerAdapter.destroyItem(ViewGroup container, int position, Object object) { mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); mFragments.set(position, null); mCurTransaction.remove(fragment); }
銷燬指定位置的Fragment。從源碼中能夠看出兩者的區別,一個detach,一個remove,這將調用到不一樣的Fragment生命週期函數:
// 對於FragmentPagerAdapter D/Adapter (25946): onPageSelected(2) D/Adapter (25946): destroyItem(0) D/Fragment0(25946): onDestroyView() // 銷燬視圖 // 對於FragmentStatePagerAdapter D/Adapter (27880): onPageSelected(2) D/Adapter (27880): destroyItem(0) D/Fragment0(27880): onDestroyView() // 銷燬視圖 D/Fragment0(27880): onDestroy() // 銷燬實例 D/Fragment0(27880): onDetach()
兩者使用方法基本相同,惟一的區別就在卸載再也不須要的fragment時,採用的處理方式不一樣:
使用FragmentStatePagerAdapter會銷燬掉不須要的fragment。事務提交後,可將fragment從activity的FragmentManager中完全移除。類名中的「state」代表:在銷燬fragment時,它會將其onSaveInstanceState(Bundle) 方法中的Bundle信息保存下來。用戶切換回原來的頁面後,保存的實例狀態可用於恢復生成新的fragment.
FragmentPagerAdapter的作法大不相同。對於再也不須要的fragment,FragmentPagerAdapter則選擇調用事務的detach(Fragment) 方法,而非remove(Fragment)方法來處理它。也就是說,FragmentPagerAdapter只是銷燬了fragment的視圖,但仍將fragment實例保留在FragmentManager中。所以, FragmentPagerAdapter建立的fragment永遠不會被銷燬。
(摘抄自《Android權威編程指南11.1.4》)
調用notifyDataSetChanged()
時,2個adapter的方法的調用狀況相同,當前頁和相鄰的兩頁的getItemPosition都會被調用到。
// Called when the host view is attempting to determine if an item's position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter. public int getItemPosition(Object object) { return POSITION_UNCHANGED; }
從網上找到的解決辦法是,覆寫getItemPosition使其返POSITION_NONE,以觸發Fragment的銷燬和重建。但是這將致使Fragment頻繁的銷燬和重建,並非最佳的方法。
後來我把注意力放在了入口參數object
上,"representing an item", 實際上就是Fragment,只須要爲Fragment提供一個更新view的public方法:
@Override // To update fragment in ViewPager, we should override getItemPosition() method, // in this method, we call the fragment's public updating method. public int getItemPosition(Object object) { Log.d(TAG, "getItemPosition(" + object.getClass().getSimpleName() + ")"); if (object instanceof Page0Fragment) { ((Page0Fragment) object).updateDate(mDate); } else if (object instanceof Page1Fragment) { ((Page1Fragment) object).updateContent(mContent); } else if (object instanceof Page2Fragment) { ((Page2Fragment) object).updateCheckedStatus(mChecked); } else if (...) { } return super.getItemPosition(object); }; // 更新界面時方法的調用狀況 // 當前頁爲0時 D/Adapter (21517): notifyDataSetChanged(+0) D/Adapter (21517): getItemPosition(Page0Fragment) D/Fragment0(21517): updateDate(2015-09-12) D/Adapter (21517): getItemPosition(Page1Fragment) D/Fragment1(21517): updateContent(Hello World, I am li2.) // 當前頁爲1時 D/Adapter (21517): notifyDataSetChanged(+1) D/Adapter (21517): getItemPosition(Page0Fragment) D/Fragment0(21517): updateDate(2015-09-13) D/Adapter (21517): getItemPosition(Page1Fragment) D/Fragment1(21517): updateContent(Hello World, I am li2.) D/Adapter (21517): getItemPosition(Page2Fragment) D/Fragment2(21517): updateCheckedStatus(true)
在最開始調用notifyDataSetChanged試圖更新Fragment時,我是這樣作的:用arraylist保存全部的Fragment,當須要更新時,就從arraylist中取出Fragment,而後調用該Fragment的update方法。這種作法很是魚脣,當時徹底不懂得adapter的Fragment manager在替我管理全部的Fragment。而我只須要:
覆寫getCount告訴adapter有幾個Fragment;
覆寫getItem以實例化一個指定位置的Fragment返回給adapter;
覆寫getItemPosition,把入口參數強制轉型成自定義的Fragment,而後調用該Fragment的update方法以完成更新。
只須要覆寫這幾個adapter的方法,adapter會爲你完成全部的管理工做,不須要本身保存、維護Fragment。
應用場景多是這樣,好比有一組按鈕,Day/Month/Year,有一個包含幾個Fragment的ViewPager。點擊不一樣的按鈕,須要秀出不一樣的Fragment。
具體怎麼實現,請參考下面的代碼:
github.com/li2/Update_Replace_Fragment_In_ViewPager/ContainerFragment.java
ViewPager.getChildCount()
返回的是當前ViewPager所管理的沒有被銷燬視圖的Fragment,並非全部的Fragment。想要獲取全部的Fragment數量,應該調用ViewPager.getAdapter().getCount()
.
爲了總結ViewPager的用法,以及寫這篇筆記,我寫了一個demo,你能夠從這裏獲取它的源碼 github.com/li2/
這一張gif圖片,演示了一個包含4個Fragment的ViewPager,經過上面的date+-1 button、EditText、Checkbox來更新前3個Fragment的界面;最後一個Fragment嵌套着2個Fragment,經過ToggleButton來切換。
這一張gif演示了切換ViewPager頁以及更新Fragment時,相關的方法調用。經過一個ScrollView和TextView展現出來。
爲何調用 FragmentPagerAdapter.notifyDataSetChanged() 並不能更新其 Fragment? - Dancefire
這篇博文詳細地解釋了notifyDataSetChanged不能更新Fragment的緣由,很是好。
android - Update Fragment from ViewPager - Stack Overflow
這個stackoverflow問答給出了兩種更新Fragment的方法,很是好。
HowTo: ListView, Adapter, getView and different list items’ layouts in one ListView | Android Tales
這篇博文詳細地解釋了ListView和它的Adapter是如何配合工做的,以及實現不一樣的listitem layout. 很是好。
android - R etrieve a fragment from ViewPager - Stack Overflow
這個statckoverflow問答討論怎樣從ViewPager中獲取一個Fragment,但我目前還不知道拿到Fragment要作什麼。
這些博文、問答的思路相似,討論Fragment的tag,經過一個list<String> 存儲全部Fragment的tag,而後再adapter裏經過fm.findFragmentByTag獲取Fragment,而後調用Fragment的update方法更新;
或者是經過一個list<Fragment>本身來管理全部的Fragment:
ViewPager Fragment 數據更新問題 - shadow066的csnd專欄
關於ViewPager的數據更新問題小結 - leo8573的csdn專欄
Viewpager+fragment數據更新問題解析 | 薑糖水
android - How to get existing fragments when using FragmentPagerAdapter - Stack Overflow
android - Retrieve a Fragment from a ViewPager - Stack Overflow
[android - support FragmentPagerAdapter holds reference to old fragments - Stack Overflow](
http://stackoverflow.com/questions/97271...
其中,這個問題的答案解釋的特別好:However, the ones that are added to the fragment manager now are NOT the ones you have in your fragments list in your Activity. 企圖這樣更新界面是行不通的:pagerAdapter.getItem(1)).update(id, name)
android - Display fragment viewpager within a fragment - Stack Overflow
Fragment裏有一個ViewPager,ViewPager裏有多個Fragment.
這些是Android Fragment相關的源碼文件:
Android源碼倉庫
android.support.v4.app.Fragment
android.support.v4.view.PagerAdapter
android.support.v4.app.FragmentPagerAdapter
android.support.v4.app.FragmentStatePagerAdapter
版權聲明:《如何更新及替換ViewPager中的Fragment?》由 WeiYi.Li 在 2015年09月13日寫做。著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
文章連接:http://li2.me/2015/09/how-to-update-repl...