AndroidApp中ViewPager与Fragment结合的⼀些问题解决
在了解ViewPager的⼯作原理之前,先回顾ListView的⼯作原理:
ListView只有在需要显⽰某些列表项时,它才会去申请可⽤的视图对象;如果为所有的列表项数据创建视图对象,会浪费内存;
ListView谁去申请视图对象呢?答案是adapter。adapter是⼀个控制器对象,负责从模型层获取数据,创建并填充必要的视图对象,将准备好的视图对象返回给ListView;
⾸先,通过调⽤adapter的getCount()⽅法,ListView询问数组列表中包含多少个对象(为避免出现数组越界的错误);紧接着ListView就调⽤adapter的getView(int, View, ViewGroup)⽅法。ViewPager某种程度上类似于ListView,区别在于:ListView通过View(int position, View convertView, ViewGroup parent)填充视图;ViewPager通过Item(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.Count().
先通过⼀段代码了解ViewPager和FragmentPagerAdapter的典型⽤法
稍后做详细分析:
// Set a PagerAdapter to supply views for this pager.
ViewPager viewPager = (ViewPager) findViewById(_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();
}
ItemPosition(object);
}
};
private class MyFragment extends Fragment {
@Override
public void onCreate(Bundle 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管理:
FragmentStatePageAdapter的Fragment管理:
详细分析 adapter method和fragment lifecycle method 的调⽤情况
好啦,感受完毕,我们需要探究其详情,梳理adapter创建、销毁Fragment的过程,过程中adapter m
ethod和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中,所以只需创建它的视图android权威编程指南
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);
}
销毁指定位置的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(" + Class().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 (...) {
}
ItemPosition(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/li2/Update_Replace_Fragment_In_ViewPager/ContainerFragment.java
⼀些误区
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包都是向下兼容的,只需要保持依赖库的版本和我们⾃⾝项⽬的版本⼀致即可。今天暂时先总结到这⾥,如果上述⾔论有错误的地⽅,希望各位⼩伙伴们及时指出。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论