ViewPager FragmentPagerAdapter和FragmentStatePagerAdapter使用

在使用ViewPager常用设置
1)mViewPager.setOffscreenPageLimit(2);//设置缓存view 的个数(实际有3个,缓存2个+正在显示的1个)
2)mViewPager.setPageMargin((int)getResources().getDimensionPixelOffset(R.dimen.ui_5_dip));//设置viewpager每个页卡的间距,与gallery的spacing属性类似
3)ViewPager更新数据问题:
直接使用notifyDataSetChanged是无法更新,需要同时重写getItemPosition返回常量 POSITION_NONE (此常量为viewpager带的)。

转: How to update and replace fragment in viewpager?

How to update and replace fragment in viewpager?

ListView的工作原理

在了解ViewPager的工作原理之前,先回顾ListView的工作原理:

  1. ListView只有在需要显示某些列表项时,它才会去申请可用的视图对象;如果为所有的列表项数据创建视图对象,会浪费内存;
  2. ListView找谁去申请视图对象呢? 答案是adapter。adapter是一个控制器对象,负责从模型层获取数据,创建并填充必要的视图对象,将准备好的视图对象返回给ListView
  3. 首先,通过调用adapter的getCount()方法,ListView询问数组列表中包含多少个对象(为避免出现数组越界的错误);紧接着ListView就调用adapter的getView(int, View, ViewGroup)方法。

ViewPager某种程度上类似于ListView,区别在于:ListView通过ArrayAdapter.getView(int position, View convertView, ViewGroup parent)填充视图;ViewPager通过FragmentPagerAdapter.getItem(int position)生成指定位置的fragment.

而我们需要关注的是:

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

声明:本文内容针对android.support.v4.app.*
ViewPager有两个adapter:FragmentPagerAdapter和FragmentStatePagerAdapter:

继承自android.support.v4.view.PagerAdapter,每页都是一个Fragment,并且所有的Fragment实例一直保存在Fragment manager中。所以它适用于少量固定的fragment,比如一组用于分页显示的标签。除了当Fragment不可见时,它的视图层(view hierarchy)有可能被销毁外,每页的Fragment都会被保存在内存中。(翻译自代码文件的注释部分)

继承自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

参考

关于作者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容