android viewpager 删除 fragment,Android App中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的典型用法

稍后做详细分析:

// 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管理:

3ef8d5c18cf3f13eeab9eac9289a86b6.png

FragmentStatePageAdapter的Fragment管理:

cdfc134d6ebf6a5dd72f3d5ac993c321.png

详细分析 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永远不会被销毁。

更新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().

ViewPager中使用Fragment+ListView,多次切换后造成ListView没有数据显示?

ViewPager+Fragment动态增删缓存问题产生原因:

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

解决方案:

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

@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包都是向下兼容的,只需要保持依赖库的版本和我们自身项目的版本一致即可。 今天暂时先总结到这里,如果上述言论有错误的地方,希望各位小伙伴们及时指出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值