如何更新及替換ViewPager中的Fragment

How to update and replace fragment in viewpager?php

ListView的工做原理

在瞭解ViewPager的工做原理以前,先回顧ListView的工做原理:html

  1. ListView只有在須要顯示某些列表項時,它纔會去申請可用的視圖對象;若是爲全部的列表項數據建立視圖對象,會浪費內存;java

  2. ListView找誰去申請視圖對象呢? 答案是adapter。adapter是一個控制器對象,負責從模型層獲取數據,建立並填充必要的視圖對象,將準備好的視圖對象返回給ListViewandroid

  3. 首先,經過調用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

而咱們須要關注的是:編程

ViewPager和它的adapter是如何配合工做的?

聲明:本文內容針對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().

先經過一段代碼瞭解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的管理略有不一樣,在詳細考察兩者區別以前,咱們經過兩種較爲直觀的方式先感覺下:

經過兩張圖片直觀的對比FragmentPagerAdapter和FragmentStatePagerAdapter的區別

說明:這兩張圖片來自於《Android權威編程指南》,原圖有3個Fragment,我增長了1個Fragment,以及被調到的方法。
FragmentPagerAdapter的Fragment管理:
image-11-4-FragmentPagerAdapter的fragment管理-方法調用

FragmentStatePageAdapter的Fragment管理:
image-11-3FragmentStatePagerAdapter的fragment管理-方法調用

詳細分析 adapter method和fragment lifecycle method 的調用狀況

好啦,感覺完畢,咱們須要探究其詳情,梳理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()

Fragment getItem(int position)

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

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。從源碼中能夠看出兩者的區別,一個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()

FragmentPagerAdapter和FragmentStatePagerAdapter對比總結

兩者使用方法基本相同,惟一的區別就在卸載再也不須要的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》)

更新ViewPager中的Fragment

調用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

替換ViewPager中的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().

一個Demo

爲了總結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時,相關的方法調用。經過一個ScrollView和TextView展現出來。
image-update_fragment_in_viewpager_withlog

參考

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.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...

相關文章
相關標籤/搜索