ViewPager FragmentPagerAdapter和FragmentStatePagerAdapter使用

在使用ViewPager常用設(shè)置
1)mViewPager.setOffscreenPageLimit(2);//設(shè)置緩存view 的個數(shù)(實際有3個,緩存2個+正在顯示的1個)
2)mViewPager.setPageMargin((int)getResources().getDimensionPixelOffset(R.dimen.ui_5_dip));//設(shè)置viewpager每個頁卡的間距,與gallery的spacing屬性類似
3)ViewPager更新數(shù)據(jù)問題:
直接使用notifyDataSetChanged是無法更新,需要同時重寫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的工作原理:

  1. ListView只有在需要顯示某些列表項時,它才會去申請可用的視圖對象;如果為所有的列表項數(shù)據(jù)創(chuàng)建視圖對象,會浪費內(nèi)存;
  2. ListView找誰去申請視圖對象呢? 答案是adapter。adapter是一個控制器對象,負責(zé)從模型層獲取數(shù)據(jù),創(chuàng)建并填充必要的視圖對象,將準(zhǔn)備好的視圖對象返回給ListView
  3. 首先,通過調(diào)用adapter的getCount()方法,ListView詢問數(shù)組列表中包含多少個對象(為避免出現(xiàn)數(shù)組越界的錯誤);緊接著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)容針對android.support.v4.app.*
ViewPager有兩個adapter:FragmentPagerAdapter和FragmentStatePagerAdapter:

繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,并且所有的Fragment實例一直保存在Fragment manager中。所以它適用于少量固定的fragment,比如一組用于分頁顯示的標(biāo)簽。除了當(dāng)Fragment不可見時,它的視圖層(view hierarchy)有可能被銷毀外,每頁的Fragment都會被保存在內(nèi)存中。(翻譯自代碼文件的注釋部分)

繼承自android.support.v4.view.PagerAdapter,每頁都是一個Fragment,當(dāng)Fragment不被需要時(比如不可見),整個Fragment都會被銷毀,除了saved state被保存外(保存下來的bundle用于恢復(fù)Fragment實例)。所以它適用于很多頁的情況。(翻譯自代碼文件的注釋部分)

它倆的子類,需要實現(xiàn)getItem(int)android.support.v4.view.PagerAdapter.getCount().

先通過一段代碼了解ViewPager和FragmentPagerAdapter的典型用法

稍后做詳細分析:

  // 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的管理略有不同,在詳細考察二者區(qū)別之前,我們通過兩種較為直觀的方式先感受下:

通過兩張圖片直觀的對比FragmentPagerAdapter和FragmentStatePagerAdapter的區(qū)別

說明:這兩張圖片來自于《Android權(quán)威編程指南》,原圖有3個Fragment,我增加了1個Fragment,以及被調(diào)到的方法。
FragmentPagerAdapter的Fragment管理:

image-11-4-FragmentPagerAdapter的fragment管理-方法調(diào)用

FragmentStatePageAdapter的Fragment管理:


image-11-3FragmentStatePagerAdapter的fragment管理-方法調(diào)用

詳細分析 adapter method和fragment lifecycle method 的調(diào)用情況

好啦,感受完畢,我們需要探究其詳情,梳理adapter創(chuàng)建、銷毀Fragment的過程,過程中adapter method和fragment lifecycle method哪些被調(diào)到,有哪些一樣,有哪些不一樣。

最開始處于第0頁時,adapter不僅為第0頁創(chuàng)建Fragment實例,還為相鄰的第1頁創(chuàng)建了Fragment實例:

// 剛開始處在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同樣會為相鄰的第2頁創(chuàng)建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齊聲說:吶,請主公貳放心,屬下定會為您準(zhǔn)備好相鄰的下一頁視圖噠!么么噠!
它倆對待下一頁的態(tài)度是相同的,但對于上上頁,它倆做出了不一樣的事情:
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中,所以只需創(chuàng)建它的視圖

FragmentStatePagerAdapter說:上上頁的實例和視圖都被俺銷毀啦

// 第N次(N不等于1)向右滑選中page2
D/Adapter (27880): onPageSelected(2)
D/Adapter (27880): destroyItem(0)  // 銷毀page0的實例和視圖
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需要一個指定位置的Fragment,并且這個Fragment不存在時,getItem就被調(diào)到,返回一個Fragment實例給adapter。
所以,有必要再次強調(diào),getItem是創(chuàng)建一個新的Fragment,但是這個方法名可能會被誤認為是返回一個已經(jīng)存在的Fragment
對于FragmentPagerAdapter,當(dāng)每頁的Fragment被創(chuàng)建后,這個函數(shù)就不會被調(diào)到了。對于FragmentStatePagerAdapter,由于Fragment會被銷毀,所以它仍會被調(diào)到。
由于我們必須在getItem中實例化一個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ū)別,一個detach,一個remove,這將調(diào)用到不同的Fragment生命周期函數(shù):

// 對于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()

FragmentPagerAdapter和FragmentStatePagerAdapter對比總結(jié)

二者使用方法基本相同,唯一的區(qū)別就在卸載不再需要的fragment時,采用的處理方式不同:

  • 使用FragmentStatePagerAdapter會銷毀掉不需要的fragment。事務(wù)提交后,可將fragment從activity的FragmentManager中徹底移除。類名中的“state”表明:在銷毀fragment時,它會將其onSaveInstanceState(Bundle) 方法中的Bundle信息保存下來。用戶切換回原來的頁面后,保存的實例狀態(tài)可用于恢復(fù)生成新的fragment.
  • FragmentPagerAdapter的做法大不相同。對于不再需要的fragment,F(xiàn)ragmentPagerAdapter則選擇調(diào)用事務(wù)的detach(Fragment) 方法,而非remove(Fragment)方法來處理它。也就是說,F(xiàn)ragmentPagerAdapter只是銷毀了fragment的視圖,但仍將fragment實例保留在FragmentManager中。因此, FragmentPagerAdapter創(chuàng)建的fragment永遠不會被銷毀。

(摘抄自《Android權(quán)威編程指南11.1.4》)

更新ViewPager中的Fragment

調(diào)用notifyDataSetChanged()時,2個adapter的方法的調(diào)用情況相同,當(dāng)前頁和相鄰的兩頁的getItemPosition都會被調(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", 實際上就是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);
};

// 更新界面時方法的調(diào)用情況
// 當(dāng)前頁為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.)

// 當(dāng)前頁為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)

在最開始調(diào)用notifyDataSetChanged試圖更新Fragment時,我是這樣做的:用arraylist保存所有的Fragment,當(dāng)需要更新時,就從arraylist中取出Fragment,然后調(diào)用該Fragment的update方法。這種做法非常魚唇,當(dāng)時完全不懂得adapter的Fragment manager在替我管理所有的Fragment。而我只需要:

  • 覆寫getCount告訴adapter有幾個Fragment;
  • 覆寫getItem以實例化一個指定位置的Fragment返回給adapter;
  • 覆寫getItemPosition,把入口參數(shù)強制轉(zhuǎn)型成自定義的Fragment,然后調(diào)用該Fragment的update方法以完成更新。

只需要覆寫這幾個adapter的方法,adapter會為你完成所有的管理工作,不需要自己保存、維護Fragment

替換ViewPager中的Fragment

應(yīng)用場景可能是這樣,比如有一組按鈕,Day/Month/Year,有一個包含幾個Fragment的ViewPager。點擊不同的按鈕,需要秀出不同的Fragment。
具體怎么實現(xiàn),請參考下面的代碼:
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().

一個Demo

為了總結(jié)ViewPager的用法,以及寫這篇筆記,我寫了一個demo,你可以從這里獲取它的源碼 github.com/li2/

這一張gif圖片,演示了一個包含4個Fragment的ViewPager,通過上面的date+-1 button、EditText、Checkbox來更新前3個Fragment的界面;最后一個Fragment嵌套著2個Fragment,通過ToggleButton來切換。

image-update_fragment_in_viewpager_demo

這一張gif演示了切換ViewPager頁以及更新Fragment時,相關(guān)的方法調(diào)用。通過一個ScrollView和TextView展示出來。

image-update_fragment_in_viewpager_withlog

參考

關(guān)于作者

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內(nèi)容