在使用ViewPager常用設(shè)置
1)mViewPager.setOffscreenPageLimit(2);//設(shè)置緩存view 的個(gè)數(shù)(實(shí)際有3個(gè)伴榔,緩存2個(gè)+正在顯示的1個(gè))
2)mViewPager.setPageMargin((int)getResources().getDimensionPixelOffset(R.dimen.ui_5_dip));//設(shè)置viewpager每個(gè)頁卡的間距旭等,與gallery的spacing屬性類似
3)ViewPager更新數(shù)據(jù)問題:
直接使用notifyDataSetChanged是無法更新诱桂,需要同時(shí)重寫getItemPosition返回常量 POSITION_NONE (此常量為viewpager帶的)。
轉(zhuǎn): How to update and replace fragment in viewpager?
How to update and replace fragment in viewpager?
ListView的工作原理
在了解ViewPager的工作原理之前豪诲,先回顧ListView的工作原理:
- ListView只有在需要顯示某些列表項(xiàng)時(shí)榆芦,它才會(huì)去申請(qǐng)可用的視圖對(duì)象捕仔;如果為所有的列表項(xiàng)數(shù)據(jù)創(chuàng)建視圖對(duì)象,會(huì)浪費(fèi)內(nèi)存橡卤;
- ListView找誰去申請(qǐng)視圖對(duì)象呢扮念? 答案是adapter。adapter是一個(gè)控制器對(duì)象碧库,負(fù)責(zé)從模型層獲取數(shù)據(jù)柜与,創(chuàng)建并填充必要的視圖對(duì)象,將準(zhǔn)備好的視圖對(duì)象返回給ListView嵌灰;
- 首先弄匕,通過調(diào)用adapter的getCount()方法,ListView詢問數(shù)組列表中包含多少個(gè)對(duì)象(為避免出現(xiàn)數(shù)組越界的錯(cuò)誤)沽瞭;緊接著ListView就調(diào)用adapter的getView(int, View, ViewGroup)方法迁匠。
ViewPager某種程度上類似于ListView,區(qū)別在于:ListView通過ArrayAdapter.getView(int position, View convertView, ViewGroup parent)
填充視圖;ViewPager通過FragmentPagerAdapter.getItem(int position)
生成指定位置的fragment.
而我們需要關(guān)注的是:
ViewPager和它的adapter是如何配合工作的柒瓣?
聲明:本文內(nèi)容針對(duì)android.support.v4.app.*
ViewPager有兩個(gè)adapter:FragmentPagerAdapter和FragmentStatePagerAdapter:
繼承自android.support.v4.view.PagerAdapter儒搭,每頁都是一個(gè)Fragment,并且所有的Fragment實(shí)例一直保存在Fragment manager中芙贫。所以它適用于少量固定的fragment搂鲫,比如一組用于分頁顯示的標(biāo)簽。除了當(dāng)Fragment不可見時(shí)磺平,它的視圖層(view hierarchy)有可能被銷毀外魂仍,每頁的Fragment都會(huì)被保存在內(nèi)存中。(翻譯自代碼文件的注釋部分)
繼承自android.support.v4.view.PagerAdapter拣挪,每頁都是一個(gè)Fragment擦酌,當(dāng)Fragment不被需要時(shí)(比如不可見),整個(gè)Fragment都會(huì)被銷毀菠劝,除了saved state被保存外(保存下來的bundle用于恢復(fù)Fragment實(shí)例)赊舶。所以它適用于很多頁的情況。(翻譯自代碼文件的注釋部分)
它倆的子類赶诊,需要實(shí)現(xiàn)getItem(int)
和 android.support.v4.view.PagerAdapter.getCount()
.
先通過一段代碼了解ViewPager和FragmentPagerAdapter的典型用法
稍后做詳細(xì)分析:
// 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對(duì)Fragment的管理略有不同笼平,在詳細(xì)考察二者區(qū)別之前,我們通過兩種較為直觀的方式先感受下:
通過兩張圖片直觀的對(duì)比FragmentPagerAdapter和FragmentStatePagerAdapter的區(qū)別
說明:這兩張圖片來自于《Android權(quán)威編程指南》舔痪,原圖有3個(gè)Fragment寓调,我增加了1個(gè)Fragment,以及被調(diào)到的方法锄码。
FragmentPagerAdapter的Fragment管理:
FragmentStatePageAdapter的Fragment管理:
詳細(xì)分析 adapter method和fragment lifecycle method 的調(diào)用情況
好啦夺英,感受完畢,我們需要探究其詳情滋捶,梳理adapter創(chuàng)建痛悯、銷毀Fragment的過程,過程中adapter method和fragment lifecycle method哪些被調(diào)到炬太,有哪些一樣灸蟆,有哪些不一樣。
最開始處于第0頁時(shí)亲族,adapter不僅為第0頁創(chuàng)建Fragment實(shí)例炒考,還為相鄰的第1頁創(chuàng)建了Fragment實(shí)例:
// 剛開始處在page0
D/Adapter (25946): getItem(0)
D/Fragment0(25946): newInstance(2015-09-10) // 注釋:newInstance()調(diào)用了Fragment的構(gòu)造器方法,下同霎迫。
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同樣會(huì)為相鄰的第2頁創(chuàng)建Fragment實(shí)例;
// 第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齊聲說:吶知给,請(qǐng)主公貳放心瓤帚,屬下定會(huì)為您準(zhǔn)備好相鄰的下一頁視圖噠描姚!么么噠!
它倆對(duì)待下一頁的態(tài)度是相同的戈次,但對(duì)于上上頁轩勘,它倆做出了不一樣的事情:
FragmentPagerAdapter說:上上頁的實(shí)例還保留著,只是銷毀了它的視圖:
// 第N次(N不等于1)向右滑動(dòng)選中page2
D/Adapter (25946): onPageSelected(2)
D/Adapter (25946): destroyItem(0) // 銷毀page0的視圖
D/Fragment0(25946): onDestroyView()
D/Fragment3(25946): onCreateView() // page3的Fragment實(shí)例仍保存在FragmentManager中怯邪,所以只需創(chuàng)建它的視圖
FragmentStatePagerAdapter說:上上頁的實(shí)例和視圖都被俺銷毀啦:
// 第N次(N不等于1)向右滑選中page2
D/Adapter (27880): onPageSelected(2)
D/Adapter (27880): destroyItem(0) // 銷毀page0的實(shí)例和視圖
D/Adapter (27880): getItem(3) // 創(chuàng)建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()
Fragment getItem(int position)
// Return the Fragment associated with a specified position.
public abstract Fragment getItem(int position);
當(dāng)adapter需要一個(gè)指定位置的Fragment绊寻,并且這個(gè)Fragment不存在時(shí),getItem就被調(diào)到悬秉,返回一個(gè)Fragment實(shí)例給adapter澄步。
所以,有必要再次強(qiáng)調(diào)和泌,getItem是創(chuàng)建一個(gè)新的Fragment村缸,但是這個(gè)方法名可能會(huì)被誤認(rèn)為是返回一個(gè)已經(jīng)存在的Fragment。
對(duì)于FragmentPagerAdapter武氓,當(dāng)每頁的Fragment被創(chuàng)建后梯皿,這個(gè)函數(shù)就不會(huì)被調(diào)到了。對(duì)于FragmentStatePagerAdapter聋丝,由于Fragment會(huì)被銷毀索烹,所以它仍會(huì)被調(diào)到工碾。
由于我們必須在getItem中實(shí)例化一個(gè)Fragment弱睦,所以當(dāng)getItem()被調(diào)用后,F(xiàn)ragment相應(yīng)的生命周期函數(shù)也就被調(diào)到了:
D/Adapter (25946): getItem(1)
D/Fragment1(25946): newInstance(Hello World, I'm li2.) // newInstance()調(diào)用了Fragment的構(gòu)造器方法渊额;
D/Fragment1(25946): onAttach()
D/Fragment1(25946): onCreate()
D/Fragment1(25946): onCreateView()
void destroyItem(ViewGroup container, int position, Object object)
// 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况木。從源碼中可以看出二者的區(qū)別,一個(gè)detach旬迹,一個(gè)remove火惊,這將調(diào)用到不同的Fragment生命周期函數(shù):
// 對(duì)于FragmentPagerAdapter
D/Adapter (25946): onPageSelected(2)
D/Adapter (25946): destroyItem(0)
D/Fragment0(25946): onDestroyView() // 銷毀視圖
// 對(duì)于FragmentStatePagerAdapter
D/Adapter (27880): onPageSelected(2)
D/Adapter (27880): destroyItem(0)
D/Fragment0(27880): onDestroyView() // 銷毀視圖
D/Fragment0(27880): onDestroy() // 銷毀實(shí)例
D/Fragment0(27880): onDetach()
FragmentPagerAdapter和FragmentStatePagerAdapter對(duì)比總結(jié)
二者使用方法基本相同,唯一的區(qū)別就在卸載不再需要的fragment時(shí)奔垦,采用的處理方式不同:
- 使用FragmentStatePagerAdapter會(huì)銷毀掉不需要的fragment屹耐。事務(wù)提交后,可將fragment從activity的FragmentManager中徹底移除椿猎。類名中的“state”表明:在銷毀fragment時(shí)惶岭,它會(huì)將其onSaveInstanceState(Bundle) 方法中的Bundle信息保存下來。用戶切換回原來的頁面后犯眠,保存的實(shí)例狀態(tài)可用于恢復(fù)生成新的fragment.
- FragmentPagerAdapter的做法大不相同按灶。對(duì)于不再需要的fragment,F(xiàn)ragmentPagerAdapter則選擇調(diào)用事務(wù)的detach(Fragment) 方法筐咧,而非remove(Fragment)方法來處理它鸯旁。也就是說噪矛,F(xiàn)ragmentPagerAdapter只是銷毀了fragment的視圖,但仍將fragment實(shí)例保留在FragmentManager中铺罢。因此艇挨, FragmentPagerAdapter創(chuàng)建的fragment永遠(yuǎn)不會(huì)被銷毀。
(摘抄自《Android權(quán)威編程指南11.1.4》)
更新ViewPager中的Fragment
調(diào)用notifyDataSetChanged()
時(shí)韭赘,2個(gè)adapter的方法的調(diào)用情況相同雷袋,當(dāng)前頁和相鄰的兩頁的getItemPosition都會(huì)被調(diào)用到。
// 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;
}
從網(wǎng)上找到的解決辦法是辞居,覆寫getItemPosition使其返POSITION_NONE楷怒,以觸發(fā)Fragment的銷毀和重建⊥咴睿可是這將導(dǎo)致Fragment頻繁的銷毀和重建鸠删,并不是最佳的方法。
后來我把注意力放在了入口參數(shù)object
上贼陶,"representing an item", 實(shí)際上就是Fragment刃泡,只需要為Fragment提供一個(gè)更新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);
};
// 更新界面時(shí)方法的調(diào)用情況
// 當(dāng)前頁為0時(shí)
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.)
// 當(dāng)前頁為1時(shí)
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)
在最開始調(diào)用notifyDataSetChanged試圖更新Fragment時(shí),我是這樣做的:用arraylist保存所有的Fragment碉怔,當(dāng)需要更新時(shí)烘贴,就從arraylist中取出Fragment,然后調(diào)用該Fragment的update方法撮胧。這種做法非常魚唇桨踪,當(dāng)時(shí)完全不懂得adapter的Fragment manager在替我管理所有的Fragment。而我只需要:
- 覆寫getCount告訴adapter有幾個(gè)Fragment芹啥;
- 覆寫getItem以實(shí)例化一個(gè)指定位置的Fragment返回給adapter锻离;
- 覆寫getItemPosition,把入口參數(shù)強(qiáng)制轉(zhuǎn)型成自定義的Fragment墓怀,然后調(diào)用該Fragment的update方法以完成更新汽纠。
只需要覆寫這幾個(gè)adapter的方法,adapter會(huì)為你完成所有的管理工作傀履,不需要自己保存虱朵、維護(hù)Fragment。
替換ViewPager中的Fragment
應(yīng)用場(chǎng)景可能是這樣钓账,比如有一組按鈕碴犬,Day/Month/Year,有一個(gè)包含幾個(gè)Fragment的ViewPager官扣。點(diǎn)擊不同的按鈕翅敌,需要秀出不同的Fragment。
具體怎么實(shí)現(xiàn)惕蹄,請(qǐng)參考下面的代碼:
github.com/li2/Update_Replace_Fragment_In_ViewPager/ContainerFragment.java
一些誤區(qū)
ViewPager.getChildCount()
返回的是當(dāng)前ViewPager所管理的沒有被銷毀視圖的Fragment蚯涮,并不是所有的Fragment治专。想要獲取所有的Fragment數(shù)量,應(yīng)該調(diào)用ViewPager.getAdapter().getCount()
.
一個(gè)Demo
為了總結(jié)ViewPager的用法遭顶,以及寫這篇筆記张峰,我寫了一個(gè)demo,你可以從這里獲取它的源碼 github.com/li2/
這一張gif圖片棒旗,演示了一個(gè)包含4個(gè)Fragment的ViewPager喘批,通過上面的date+-1 button、EditText铣揉、Checkbox來更新前3個(gè)Fragment的界面饶深;最后一個(gè)Fragment嵌套著2個(gè)Fragment,通過ToggleButton來切換逛拱。
這一張gif演示了切換ViewPager頁以及更新Fragment時(shí)敌厘,相關(guān)的方法調(diào)用。通過一個(gè)ScrollView和TextView展示出來朽合。
參考
為什么調(diào)用 FragmentPagerAdapter.notifyDataSetChanged() 并不能更新其 Fragment俱两? - Dancefire
這篇博文詳細(xì)地解釋了notifyDataSetChanged不能更新Fragment的原因,非常好曹步。android - Update Fragment from ViewPager - Stack Overflow
這個(gè)stackoverflow問答給出了兩種更新Fragment的方法宪彩,非常好。HowTo: ListView, Adapter, getView and different list items’ layouts in one ListView | Android Tales
這篇博文詳細(xì)地解釋了ListView和它的Adapter是如何配合工作的讲婚,以及實(shí)現(xiàn)不同的listitem layout. 非常好尿孔。android - R etrieve a fragment from ViewPager - Stack Overflow
這個(gè)statckoverflow問答討論怎樣從ViewPager中獲取一個(gè)Fragment,但我目前還不知道拿到Fragment要做什么磺樱。android Fragments詳解四:管理fragment - nkmnkm的csdn博客
這是一篇翻譯文章纳猫。這些博文、問答的思路類似竹捉,討論Fragment的tag,通過一個(gè)list<String> 存儲(chǔ)所有Fragment的tag尚骄,然后再adapter里通過fm.findFragmentByTag獲取Fragment块差,然后調(diào)用Fragment的update方法更新;
或者是通過一個(gè)list<Fragment>自己來管理所有的Fragment:
ViewPager Fragment 數(shù)據(jù)更新問題 - shadow066的csnd專欄
關(guān)于ViewPager的數(shù)據(jù)更新問題小結(jié) - leo8573的csdn專欄
Viewpager+fragment數(shù)據(jù)更新問題解析 | 姜糖水
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
其中倔丈,這個(gè)問題的答案解釋的特別好: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里有一個(gè)ViewPager憨闰,ViewPager里有多個(gè)Fragment.這些是Android Fragment相關(guān)的源碼文件:
Android源碼倉庫
android.support.v4.app.Fragment
android.support.v4.view.PagerAdapter
android.support.v4.app.FragmentPagerAdapter
android.support.v4.app.FragmentStatePagerAdapter