服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android App中ViewPager与Fragment结合的一些问题解决

Android App中ViewPager与Fragment结合的一些问题解决

2021-07-02 16:14li21 Android

这篇文章主要介绍了Android App中ViewPager与Fragment结合的一些问题解决,重点讲解了如何更新及替换ViewPager中的Fragment,需要的朋友可以参考下

在了解viewpager的工作原理之前,先回顾listview的工作原理:

listview只有在需要显示某些列表项时,它才会去申请可用的视图对象;如果为所有的列表项数据创建视图对象,会浪费内存;
listview找谁去申请视图对象呢? 答案是adapter。adapter是一个控制器对象,负责从模型层获取数据,创建并填充必要的视图对象,将准备好的视图对象返回给listview;
首先,通过调用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.*
继承自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的典型用法
稍后做详细分析:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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管理:

Android App中ViewPager与Fragment结合的一些问题解决

fragmentstatepageadapter的fragment管理:

Android App中ViewPager与Fragment结合的一些问题解决

详细分析 adapter method和fragment lifecycle method 的调用情况
好啦,感受完毕,我们需要探究其详情,梳理adapter创建、销毁fragment的过程,过程中adapter method和fragment lifecycle method哪些被调到,有哪些一样,有哪些不一样。

最开始处于第0页时,adapter不仅为第0页创建fragment实例,还为相邻的第1页创建了fragment实例:

?
1
2
3
4
5
6
7
8
9
10
11
// 刚开始处在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
2
3
4
5
6
7
// 第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说:上上页的实例还保留着,只是销毁了它的视图:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第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)
?
1
2
// 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相应的生命周期函数也就被调到了:

?
1
2
3
4
5
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()

 

?
1
2
3
4
5
6
7
8
9
10
11
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生命周期函数:

?
1
2
3
4
5
6
7
8
9
10
11
12
// 对于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永远不会被销毁。
更新viewpager中的fragment
调用notifydatasetchanged()时,2个adapter的方法的调用情况相同,当前页和相邻的两页的getitemposition都会被调用到。

?
1
2
3
4
// 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方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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().

viewpager中使用fragment+listview,多次切换后造成listview没有数据显示?

viewpager+fragment动态增删缓存问题
产生原因:

我们在开发中会常常用到viewpager+fragment,有时候可能会有这样的需求,需要对viewpager中的内容进行动态的增删管理,但是我们都知道viewpager为了保证滑动的流畅性,viewpager在加载当前页的时候已经将pager页左右页的内容加载进内存里了,所以此时我们不进行任何处理的话,是我发达到我们预期的效果的。
解决方案:

一、将fragmentpageradapter 替换成fragmentstatepageradapter, 因为前者只要加载过,fragment中的视图就一直在内存中,在这个过 程中无论你怎么刷新,清除都是无用的,直至程序退出; 后者可以满足我们的需求。 2.我们可以重写adapter的方法–getitemposition(),让其返回pageradapter.position_none即可。 以下为引用内容:

?
1
2
3
4
5
6
7
@override
 
  public int getitemposition(object object) {
 
  return pageradapter.position_none; 
 
 }

到这一步我们就可以真正的实现随意、彻底删除viewpager中的fragment,随意增删。
二、善用dialog 一些交互简单、或者只是展示功能的页面,如果使用一个activity来显示的话,过于繁琐,开销也很大,使用fragment的话,蛋疼的生命周期也不好处理,此时使用一个全屏的dialog来模拟一个activity就是一个不错的选择。
三、splash页面那点事 :几乎每个页面都会有一个spalsh页,通常我们会用一个activity加载一张全屏的背景图,或者放一个app的logo,展示2秒之后,跳转到登录或者主页面,期间可能会做一些数据初始化,检查更新等操作。相信大多数小伙伴也是这么干的,但是,你不觉得一个activity只显示2秒就杀掉有点浪费?个人觉得这样的开销是非常之不划算的,我们可以借用上面一条,利用一个dialog模拟一个splash页面,2秒之后dismiss掉这个dialog,而检查更新,初始化数据等操作就放到mainactivity中。或者使用fragment替代splashactivity等等方法, 都可以达到splash页的相同效果。
四、善待内部类 在开发中,我们会经常用到内部类,内部类的出现,解决了java只能单继承的局限性,使得开发能更加灵活。但如果内部类用的不好,就会出现android developer的噩梦,oom!。为什么呢?底子稍微好点的同学,应该都知道内部类可以访问外部类的成员变量和方法,因为内部类持有了外部类的引用,当你在一个activity中使用的内部类,当activity销毁时,你的内部类没有释放,就会造成这个activity无法被gc回收,因为内部类中持有了activity的应用。
五、library那些事 library中的switch中不能使用id来case,这个在我的上一篇博文中已经讲过。这里我们再讲一个library的坑,当我们引入一个依赖库时,依赖库中一般都会自带一个support v4的包,这个v4包的版本,和我们创建工程时的版本一般情况下是一致的,但是一旦我们自己工程的v4包的依赖库中的v4包中的版本不一致时,一大推莫名其妙的错误日志就会接踵而来。此时的处理方法也很简单,由于v4包都是向下兼容的,只需要保持依赖库的版本和我们自身项目的版本一致即可。 今天暂时先总结到这里,如果上述言论有错误的地方,希望各位小伙伴们及时指出。

延伸 · 阅读

精彩推荐