有一段时间没有写博客了。作为2017年的第一篇,初衷起始于前段时间一个接触安卓开发还不算太长时间的朋友聊到的一个问题:
“假设,想要对一个Fragment每次在隐藏/显示之间做状态切换时进行监听, 从而在这个时候去完毕一些操作,应该怎么去实现呢?”
相信大家听到这类问题第一反应都会认为是非常easy的。而又经过一番讨论过后,发现他的问题场景相对来说比較特殊一点的是:
其想要监听的Fragment是嵌套在还有一层Fragment内的子Fragment。
这就更有趣了一点,当然了。这个需求场景同样也不会太难实现。
既然这样还写什么博客呢?哈哈。
问题本身尽管不算难解决,但个人发如今对其解决的过程中,事实上能涉及到不少对于Fragment较有用的小细节。
那么,自己也能够刚好借此机会,又一次更加深入的回想、整理和总结一下关于Fragment的一些使用细节和技巧。何乐而不为呢?特此记录。
问题场景还原
如上图所看到的。这一图例基本上包括了如今大多数主流APP经常使用的一种UI设计模式。有底部导航,有ViewPager,有側拉菜单等等。
事实上对于这样的UI模式。有一个非常直观的印象就是“碎片化”。
那么,相应到Android中。用Fragment去实现这样的设计就再合适只是了。
那么,我们也就能够看到:在这里。用户的一系列操作就会涉及到大量的Fragment的隐藏和显示的状态切换工作。
从而提归正传,我们试图在这一图例中去模拟的还原一下之前说到的那个问题。
首先,我们来分解一下这个用例中的UI设计:
- 首先自然是主界面,主界面是一个Activity。Ac中有一个底部导航。分为三页,三页自然分别相应了三个Fragment。
- 第二页和第三页的Fragment界面。我没有去加入额外的设计了,所以十分一目了然,故不加赘述。
- 重点在第一页。能够看到这一页中有一个側滑菜单,側滑菜单里的选项又相应了另外的Fragment界面。
- 那么。非常显然的,側滑菜单里的界面,就是嵌套在底部导航的第一页Fragment里的还有一层Fragment了。
- 最后。我们能够看到嵌套在側滑菜单里的第一个子Fragment,它里面是一个ViewPager,于是又涉及到两个新的子Fragment。
OK,到了这里,有了这一番UI分解。我们有了一个大概的了解。如今我们借助一个实际的经常使用功能更好的还原我们之前说到的那个问题。
假设在底部导航的“第三页”界面中。有一个功能叫做“清除缓存”。那么,使用这一功能就意味着:其他界面当中原先缓存的数据将被清除。
也就意味着,当用户再次切换到另外的界面中时(Fragment由隐藏切换到显示)。就须要清除该界面原本的内容,又一次获取最新的内容显示。
OK。如今已经回到了我们最初说到的话题了。
那么,接着就让我们以这个样例切入。由易到难的看一下:
在常见的各种情况下。应该怎样监听Fragment的显示状态切换。而在这当中。又能够注意哪些关于Fragment比較有用的小细节。
replace与hide/show
在上一节的图例中,我们说到主界面Activity中有一个底部导航栏。分别相应着三个功能界面(即三个Fragment)。
显然,我们肯定有两种方式来控制这样的Fragment的导航切换。即使用replace进行切换,或者通过hide/show来控制切换。
以我们的图例来说,假设我们想要从“第一页”切换到“第二页”,那么我们能够这样做(replace):
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.fragment_container, new SecondFragment());
ft.commit();
当然也能够这样做(hide/show):
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(new FirstFragment())
.show(new SecondFragment())
.commit();
但这里就注意了:假设我们是刚開始接触Fragment,上面的代码看上去似乎没问题,但实际肯定是不能这样去使用hide/show的。
由于就像上述代码表述的一样。我们一定要留意到“new ”,它看上去就像在说:每次隐藏和显示的都是一个全新的Fragment对象。
而事实上也的确如此。所以。这也就意味着:假设这么搞,我们肯定是没办法正确的控制fragment的切换显示的。
那么,我们应该怎么去完毕这样的需求呢?实际能够提供两种方式。第一种就是为Fragment加入“单例”。
以我们图例中显示的来说。当进入主界面后,优先显示的是“第一页的界面”,所以我们能够先让它显示:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.fragment_container, FirstFragment.getInstance());
ft.add(R.id.fragment_container, SecondFragment.getInstance());
ft.add(R.id.fragment_container, ThirdFragment.getInstance());
ft.hide(SecondFragment.getInstance());
ft.hide(ThirdFragment.getInstance());
ft.commit();
于是在此之后。由“第一页”的Fragment切换到“第二页”的工作,则能够通过相似以下的代码来实现:
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(FirstFragment.getInstance())
.show(SecondFragment.getInstance())
.commit();
你肯定注意到,这里我选择先将会使用到的Fragment对象都加入到内存中去,让临时不须要显示的碎片先hide。
须要额外说明的是: 这样的做法本身事实上也是可行的,但在以上的用例里我确实是省方便,而选择了这样的做法。而实际上来说:
个人认为。假设我们事实上并没有让临时不须要显示的Fragment进行“预载入”的需求的话。那么相应来说:
选择在真正须要切换Fragment显示的时候,再将要显示的Fragment对象进行add,然后控制它们hide和show是更好的做法。
而之所以说这样做更好的原因是什么呢?我们略微放一放。在之后不久的分析里我们就能够看到。
上述的“单例”这样的方式能完毕我们的需求吗?当然是能够的。
但让人惬意吗?似乎总认为有些别扭。
的确如此。这样的方式更像是用Java的方式去解决这个问题,而非使用Android的方式来解决这个问题。
所以,我们接着看另外一种方式。即使用FragmentManager自身来管理我们的Fragment对象。
首先。我们要知道,通过FragmentManager开启事务来动态加入Fragment对象的时候。也是能够为Fragment设置标识的。
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
FirstFragment first = new FirstFragment();
SecondFragment second = new SecondFragment();
ThirdFragment third = new ThirdFragment();
ft.add(R.id.fragment_container,first, "Tab0");
ft.add(R.id.fragment_container,second,"Tab1");
ft.add(R.id.fragment_container,third,"Tab2");
ft.hide(second);
ft.hide(third);
ft.commit();
上述代码中的“Tab0”这样的东西就是我们为Fragment对象设置的标识(Tag)。其优点就在于我们之后能够非常方便的控制不同Fragment的切换。
//这里是导航切换的回调
public void onTabSelected(int position) {
if(position != currentFragmentIndex)
{
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
.show(fm.findFragmentByTag("Tab"+position))
.commit();
currentFragmentIndex = position;
}
}
就像这里做的,事实上FragmentManager本身就有一个List来存放我们add的Fragment对象。这意味着:
我们通过设置的Tag。能够直接复用FragmentManager中已经add过的Fragment对象,而无需所谓的“单例”。
在上述代码中:自己定义的currentFragmentIndex用于记录当前所在的导航页索引,position则意味着要切换显示的界面的索引。
那么,再配合上我们对Fragment对象进行add的时候设置的Tag,便能非常方便简洁的实现Fragment的切换显示了。
回到之前说的,我们也能够选择不一次性将三个fragment进行add,而是做的更极致一点。
而原理依旧非常easy:
public void onTabSelected(int position) {
if (position != currentFragmentIndex) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
Fragment targetFragment = fm.findFragmentByTag("Tab" + position);
if (targetFragment == null) {
switch (position) {
case 1:
targetFragment = new SecondFragment();
break;
case 2:
targetFragment = new ThirdFragment();
break;
}
ft.add(R.id.fragment_container,targetFragment,"Tab"+position);
}
ft.hide(fm.findFragmentByTag("Tab"+currentFragmentIndex))
.show(targetFragment)
.commit();
currentFragmentIndex = position;
}
}
与之前不同的就是:如今我们最初仅仅add须要显示的FirstFragment。
然后在切换Fragment显示的时候,首先通过findFragmentByTag查找相应的Fragment对象是否已经被add进了内存,假设没有则新建该对象,并进行add。而假设已经存在,则能够直接进行复用。并控制隐藏与显示的切换了。
好了,到这里我们看到通过这两种方式都能够实现切换Fragment显示的需求。那么,其差别在哪里呢?我们能够简单的到源代码中找下答案。
从源代码看replace与add的异同
首先,我们能够明确一点的是:不管是使用到replace还是使用hide/show。前提都须要确保将相关的Fragment对象放入FragmentManager。
那么。在之前的用例描写叙述中:对于hide/show的使用来说,我们知道首先是通过add的方式来进行的。就像例如以下代码所做的这样:
ft.add(R.id.fragment_container, first, "Tab0");
那么,对于replace来说又是怎样呢?事实上我们能够打开replace方法的源代码看一看说明:
以上截图是源代码中对于replace方法的凝视说明,我们阅读一下发现:简单的来说。它似乎是在告诉我们,调用replace方法,效果基本就等同于:
先调用remove方法删除掉指定containerViewId中当前全部已加入(add)的fragment对象,然后再通过add方法将replace传入的对象加入。
看到这里。我们难免在想,这么说事实上replace和add在本质上来说,是非常相似的。事实上这样说也没错。通过以下代码能够验证上面的结论。
Log.d(TAG,getSupportFragmentManager().getFragments().size()+"");
通过以上代码能够获取当前FragmentManager中存放的有效的fragment对象的数量,那么对于我们上面说到的用例中:
- 当使用replace控制fragment的显示时。会发现获取到的碎片数始终是1。由于每次replace时,都会将之前存在的fragment对象remove掉。
- 当使用hide/show控制时,获取到的碎片数将与我们进行add的数量同样。比方之前我们在首页add了3个fragment,获取到的数量就是3。
那么,fragment内部到底是怎样的机制。才会造成这样的结果呢?回想一下:
事实上我们在管理fragment的时候,始终在和两个东西打交道。那就是:FragmentManager与FragmentTransaction。
FragmentManager事实上是一个抽象类。它的详细实现是FragmentManagerImpl。其内部有这样的东西:
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
//.......
我们前面说到FragmentManager自身就有一个集合来存放fragment对象。事实上就是上面这样的东西。
FragmentTransaction事实上也是一个抽象类。通过FragmentManagerImpl当中的代码,我们能够知道其详细实现:
是的,FragmentTransaction的详细实现事实上是一个叫做BackStackRecord的类。由此为基础,我们就能够看看add和replace到底做了什么样的工作。
能够看到add和replace非常重要的一个的差别在于某种行为标识:“OP_ADD”与“OP_REPLACE”。
但它们二者终于都是来到一个叫做doAddOp的方法。截取这种方法内目的性最强的部分代码例如以下:
能够看到这里做的事实上就是创建一个Op类型的对象,然后运行addOp方法。那么,我们首先看看Op是个什么东西?
好吧,连我数据结构这么渣的。也看出这就是一个链表结构的东东啦。事实上不难理解,由于我们在正式commit事务之前:
事实上能够运行一系列的add。replace,hide,remove等等的操作,所以肯定是须要一个相似链表这样的数据结构来更好的记录这些信息的。
那么。至于addOp这种方法就不难想象了,事实上就是通过改动链表节点信息来记录所做的相似add这样的操作。
节省篇幅,就不贴源代码截图了。
但问题是,到如今我们还没有和之前说到的FragmentManager产生联系。这是没错的,由于真正和FM产生联系。自然是在commit之后。
这里由于能力和篇幅有限,就不会做详细的逐步分析了。总之我们明确一点:BackStackRecord本身实现了Runnable接口。
接着。在commit之后的一系列相关调用之后,终于则会进入到BackStackRecord的run()方法開始运行。
那么,如今我们截取run()方法内我们关心的部分代码来看看:
switch (op.cmd) {
case OP_ADD: {
Fragment f = op.fragment;
f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
} break;
case OP_REPLACE: {
Fragment f = op.fragment;
int containerId = f.mContainerId;
if (mManager.mAdded != null) {
for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
Fragment old = mManager.mAdded.get(i);
if (FragmentManagerImpl.DEBUG) Log.v(TAG,
"OP_REPLACE: adding=" + f + " old=" + old);
if (old.mContainerId == containerId) {
if (old == f) {
op.fragment = f = null;
} else {
if (op.removed == null) {
op.removed = new ArrayList<Fragment>();
}
op.removed.add(old);
old.mNextAnim = exitAnim;
if (mAddToBackStack) {
old.mBackStackNesting += 1;
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
+ old + " to " + old.mBackStackNesting);
}
mManager.removeFragment(old, transition, transitionStyle);
}
}
}
}
if (f != null) {
f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
}
} break;
首先是OP_ADD,能够看到处理的代码非常easy。关键的就是那句:
mManager.addFragment(f, false);
我们回到FragmentManagerImpl来中看看这句代码到底做了什么:
public void addFragment(Fragment fragment, boolean moveToStateNow) {
if (mAdded == null) {
mAdded = new ArrayList<Fragment>();
}
if (DEBUG) Log.v(TAG, "add: " + fragment);
makeActive(fragment);
if (!fragment.mDetached) {
if (mAdded.contains(fragment)) {
throw new IllegalStateException("Fragment already added: " + fragment);
}
mAdded.add(fragment);
fragment.mAdded = true;
fragment.mRemoving = false;
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
if (moveToStateNow) {
moveToState(fragment);
}
}
}
这种方法事实上也不复杂。关键的信息就是将fragment对象加入mAdded集合中,而makeActive则会将其加入到mActive集合。
(之前说到的通过getFragmentManager.getFragments这句代码返回的事实上就是mActive这个集合)
而最后的moveToState就是真正关键的改变fragment对象状态的操作了。这种方法比較复杂。这里不深入分析了。
如今我们回头继续看run方法中OPP_REPLACE运行的操作怎样,事实上有了之前的基础。我们不难发现:
replace的不同之处就在于,会先把mManager中mAdded集合内同样contanierViewID的fragment对象遍历出来删除掉:
mManager.removeFragment(old, transition, transitionStyle);
然后最后就像我们之前知道的那样,事实上依旧是把fragment对象加入到mManager的集合中去:
if (f != null) {
f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
}
如今我们应该明确,replace和add为什么会造成在mManger中存放的数量不同以及源代码中replace方法的凝视说明的原因了。但继续延伸,
回想一下:我们知道FragmentTransaction实际上还有一个功能叫做addToBackStack()。故名思议,就是说将Fragment对象加入返回栈。
事实上这种方法的实际操作,在之前我们分析过的源代码中也能够得知。在BackStackRecord的run方法中,对于OPP_REPLACE操作有例如以下代码:
if(mAddToBackStack)就是指使用了addToBackStack方法的情况。这时运行的一个操作是将fragment对象的mBackStackNesting变量自增。
随后紧接着的操作就是通过mManager去removeFragment,重点就在这里,让我们看看removeFragment方法的源代码:
注意final boolean inactive = !fragment.isInBackStack();这行代码,其详细实现为:
也就是说。由于addToBackStack的存在。会导致inactive的获取结果为false。所以这时根本不会真正去运行if语句块里真正删除fragment对象的操作。
这显然是符合逻辑的,将fragment对象加入返回栈,意味着我之后还可能会使用到该对象,你自然不能像之前一样把它们清除掉。
当然。我们还能够自己在addToBackStack使用之后,再通过fragmentManager.getFragments.size()去验证一下获取到的fragment对象的数量变化。
生命周期的不同
我们如今已经知道当通过add,replace,remove等操作时。终于会通过moveToState去改变相应fragment对象的状态。
那么,hide/show又是怎样呢?于是我们又回到了BackStackRecord的run方法当中寻觅答案:
那我们就以hideFragment作为样例,看看这时mManager到底是做的什么工作呢?
能够看到这时实际上就告别了moveToState,其本质在于通过改变fragment对象的mView的可视性来控制显示,并回调onHiddenChanged。
以上的分析的目的为何呢?事实上就是为了证明。通过replace和hide/show两种方式。最大的差别就在于:二者的生命周期变化相去甚远。
我们还是能够自己去验证这点。最简单的方式是:写一个基类的BaseFragment。然后为全部生命周期回调加入日志打印,就相似于例如以下这样:
public class BaseFragment extends Fragment{
protected String clazzName = this.getClass().getSimpleName() +" ==> ";
@Override
public void onAttach(Context context) {
Log.d(clazzName,"onAttach");
super.onAttach(context);
}
//......
}
那么,当我们使用replace进行切换显示的时候,会发现其生命周期的路线相似于以下这样:
然后我们切换到“hide/show”来观察生命周期的变化,发现其回调例如以下所看到的:
而使用replace时。是否使用addToBackStack的还有一个差别也是生命周期上的不同。
没有使用addToBackStack的时候,被切换的对象和切换进来的对象的生命周期分别为:
- onPause → onStop → onDestroyView → onDestroy → onDeatch
- onAttach → onCreate → onCreateView → onViewCreated → onActivityCreated → onStart → onResume
而当使用了addToBackStack后,被切换的对象的生命周期变化则成了:
- onPause → onStop → onDestroyView
而切换进行的对象,假设是首次进行切换。则与之前无异。反之,假设已经存在于返回栈内,生命周期变化则成了:
- onCreateView → onViewCreated → onActivityCreated → onStart → onResume
如今我们回到之前说到的一个问题。为什么说不须要在最初就把潜在的几个Fragment一股脑进行add,有了之前分析的基础。
我们知道把fragment进行add过后,终于会运行到moveToState方法进行状态设置,那相应到我们之前的样例中来说的话:
就代表着我们最初加入的三个fragment对象,都会经历onAttach → onCreate → …… → onResume这一初始化生命周期。
这意味着加入的三个fragment对象中,“第二页”与“第三页”尽管眼下不用显示。但系统须要耗费时间去完毕它们的初始化周期。
这显然在一定程度上会影响效率。
当然,详细要怎么使用事实上还是看实际的需求哪种更合适。我们仅仅要明确当中的细节就能够了。
如今,我们思考一个问题,对于我们本文中的图例应用来说,到底使用replace还是hide/show更适合呢?事实上有了之前的基础,我们知道:
ft.add(R.id.fragment_container,first, "Tab0");
这行代码的效果,事实上全然能够用这样使用replace来转换:
ft.replace(R.id.fragment_container,first,"Tab0").addToBackStack(null);
可是,我们前面也说到了,最大的差别就在于二者生命周期变化的不同。
再分析一下:
- 首先,假设我们使用的是replace切换fragment的显示,那显然我们须要使用addToBackStack。否则就无需谈什么监听由隐藏到显示了。由于单独replace每次都意味着切换进的是一个全新的fragment对象。
- 假设使用replace+addToBackStack,那么被切换的fragment对象(即隐藏的对象)与切换进的fragment对象(即显示的对象)的生命周期变化路线我们都已经清楚了。这时就有点相似监听Activity了。
- 使用hide/show来控制显示切换,显然是最简单的。监听onHiddenChange回调,实现自己的目的就能够了。
上述的第2、3种方式都能实现目的,但replace最大的缺陷就在于每次切换都会进入onCreateView到onResume这一周期。
这当中总会涉及到我们在fragment这些生命周期中做出的一些比如数据初始化的操作等。显然这样控制起来是非常麻烦的(数据重复载入等)。
所以,综合比較之下,显然hide/show才是最合适的方式。而对于replace来说,最适合的显然就还是那种比較典型的样例:
比方pad上的新闻应用,左边是新闻列表,右边为新闻的详细内容,这时右边的fragment用replace来切换新闻内容显示就是最合适的。
那么回到我们本文之前图例里的演示应用,那么比方在“第三页”清除缓存后,切换到了“第二页”。
这时使用hide/show切换fragment显示,然后通过onHiddenChange完毕监听就非常easy了。
这就是这个图例里。针对于我们最初提出的问题能够演示的第一种情况,也是最简单的一种情况。
当然了,这也是由于图例中,首页底部导航相应的三个fragment都隶属于同一级。即主Activity当中。
getFragmentManager还是getChildFragmentManager()?
如今阶段性总结一下,本文图例中,首页导航的三个fragment都属于同一个FragmentMnanger管理。所以依据之前的源代码分析我们就能够得知:
在我们通过hide/show来切换两个碎片显示时,相相应的,它们的onHiddenChaned方法就会被回调。所以这个时候监听它们的隐藏/显示是非常easy的。
那我们更进一步。比較特殊的是图例中“第一页”的界面。由图能够看到当中有一个側滑菜单,菜单中的三个选项卡对又应另外三个fragment。
OK,那么如今思考一下!这三个fragment还和我们之前说到的首页导航相应的三个fragment位于同一级别吗?我们来分析一下。
首先,我们已经说到在代码中动态的控制fragment。都借助于FragmentManager。而相应“第一页”的FirstFragment本身也是一个fragment。
所以在这个时候,与之前我们在主Activity通过get(Support)FragmentManager有一点不同的是,在Fragment当中我们多了一个选择:
我们发现有趣的是多了一个叫做getChildFragmentManager的方法,它们之间到底差别在哪呢?在主Activity和FirstFragment分别加入下例如以下日志:
// Activity
Log.d(TAG, getSupportFragmentManager()/*或者getFragmentManager()*/+"");
// FirstFragment
Log.d(TAG,getFragmentManager()+"
"+getChildFragmentManager());
然后运行程序发现例如以下的日志打印:
由此我们能够发现,在FirstFragment中获取的FragmentManager和之前在MainActivity中的是同一对象,归属于一个叫做HostCallBacks的东西管理。
与之不同的是:通过getChildFragmentManager获取到的FragmentManager则是还有一个不同的对象,而还有一个不同在于它则属于FirstFragment自身。
(P.S:关于HostCallBack这个东西,有兴趣的朋友能够自己研究源代码或者关于Fragment源代码分析的文章。简单的来说,它是属于FragmentActivity的内部类,getSupportFragmentManager实际就是通过控制HostCallback返回FragmentManagerImpl对象)
好的。那么如今由此事实上不难想象。既然Fragment会多出这么一个特定的方法,肯定是有其存在的意义的。
如今假定我们依旧使用FragmentMananger在FirstFragment中管理側滑菜单的子碎片,那么首先可能会出现例如以下所看到的的问题:
这里能够看到的一个问题就是:当我们在底部导航由第一页转至第三页后。第一页的fragment中间的内容仍然没有消失。
事实上原因不难分析。由于前面说到假设此时使用getFragmentManager,意味着此时获取到的FM对象事实上和MainActivity中使用的FM是同一个对象。
也就是说。如此进行加入,側滑菜单相应的三个Fragment事实上仍然是被add进了与FirstFragment隶属的同样的FragmentManager的集合内。
那么,假定我们把側滑菜单相应的第一个fragment对象命名为Child1Fragment,这事实上也就意味着:
我们视觉上看上去属于第一页(即FirstFragment)的内容事实上本来真正应该是属于Child1Fragment的内容。
但由于我们通过getFragmentManager进行add操作。我们通过例如以下代码完毕前面说到由第一页跳转至第三页的操作则会导致:
ft.hide(first)
.show(third)
.commit;
尽管我们依照逻辑hide了FirstFragment对象,但关键在于:由于它们都属于同一个FM对象。所以事实上Child1Fragment仍然没有被hide。
由此事实上我们不难想象:假设通过FragmentManager在Fragment中嵌套Fragment。将由于逻辑的严重混乱。而造成难以管理。
那么,与之相应的,假设选择使用getChildFragmentManager的优点有哪些呢?我们能够简单的概括一下:
首先,最重要的一点:通过ChildFragmentManager进行管理的子Fragment对象,与其父Fragment对象的生命周期是息息相关的。
举个样例。假设我们用SecondFragment对象来replace掉FirstFragment对象。这时候有了前面的基础。我们都知道:
FristFragment将走入onPause開始的这段生命周期。而使用ChildFragmentManager的优点在于。其内部的子Fragment也会受同样的生命周期管理。
显然,我们能够预见由此带来的最大的优点就是:此时各个Fragment之间的逻辑清楚。层级分布明确,将大大利于我们对其进行管理。
还有一个非常有用的优点就在于:在这样的管理模式下。子Fragment能够非常easy的实现与父Fragment之间进行通信。通过一个样例能更形象的理解。
依旧是本文最初的图例,我在FirstFragment中放置了一个ToolBar。那么假设我切换了选项卡,想要在子Fragment的动态的操作Toolbar,就能这么做:
public void doSomething(){
// do what you want to do
}
是的,首先在FirstFragment中我们提供这一样一个回调方法。然后在子Fragment中我们就能够通过例如以下方式与其发生互动:
FirstFragment parent = (FirstFragment) getParentFragment();
parent.doSomething();
这都是一些非常有用的小技巧,很多其他的延伸有用。我们能够自己在实际中拓展。总之:假设是刚開始接触Fragment,一定记住:
假设是在Fragment中加入Fragment时,请一定记住选择通过getChildFragmentManager来对碎片进行管理。
那么,如今我们言归正传。
通过ChildFragmentManager是不是就能解决我们说到的监听fragment隐藏/显示了呢?
事实上不难猜測出答案。
我们再次回到那个情景,在第三页清楚缓存后,回到第一页,那么非常明显符合我们逻辑的实现就是:
ft.hide(third)
.show(first)
.commit;
如今我相信我们都非常清楚了。这时候通过onHiddenChanged肯定是能够监听到FirstFragment的。但对于嵌套在其内的Child1Fragment就不行了。
可是由于之前的基础,这个问题显然已不难解决。我们能够在FirstFragment中定义一个index来记录此时的側滑菜单的子Fragment索引,随后:
// FirstFragment.java
@Override
public void onHiddenChanged(boolean hidden) {
getChildFragmentManager().getFragments().get(currentIndex).onHiddenChanged(hidden);
}
怎么样。是不是非常easy呢?这就是我们本文中提到的问题的另外一种场景延伸。
所以说细节真的非常能够帮助我们更加灵活的掌握一件事物。
与ViewPager的配合
如今我们进一步深入。正如本文图例所看到的。Child1Fragment中有一个ViewPager,ViewPager中有放置了两个Fragment。
再次衍生我们之前的场景描写叙述:如今在“第三页”清除缓存过后,再次回到第一页,此时第一页显示的正好是ViewPager中的碎片。
那么,这个时候我们应该怎样监听相应的这个位于ViewPager中的Fragment对象呢?相信有了之前的基础,我们非常easy类推出来。
既然上一节中我们已经将显示状态的改变由FirstFragment传递到了Child1Fragment。那么如今仅仅须要继续向ViewPager进行传递即可了。
// Child1Fragment.java
@Override
public void onHiddenChanged(boolean hidden) {
getChildFragmentManager().getFragments().get(mViewPager.getCurrentItem()).onHiddenChanged(hidden);
}
这便是我们本文描写叙述的问题的第三种场景延伸。
可是关于fragment配合viewpager使用时。依旧还有非常多值得留意的小技巧和细节。
ViewPager的缓存机制
通过之前的分析与总结,我们已经清楚通过FragmentManager管理碎片时,Fragment的生命周期变化情况。
那么,当Fragment配合ViewPager时,Fragment的生命周期又是什么情况呢?我们还是能够自己验证一下。
为了能得到更准确的结论,能够把Child1Fragment中ViewPager放置的Fragment数量加入到4个,再通过切换来查看各个碎片的生命周期。
为了节省篇幅,我们选择查看两个最具有代表性的生命周期变化的片段截图,首先是ViewPager的初始显示时的片段截图:
能够看到尽管ViewPager初始时,仅仅须要显示第一个Fragment,可是第二个Fragment对象仍然经过了初始化的生命周期。接着:
假设我们进一步的操作是直接将ViewPager由第一个Fragment滑动至第三个,然后我们再来瞧一瞧相应的生命周期变化:
由此我们能够发现,这个时候不仅切换到的第三个Fragment进行了初始化。与它相邻的第四个碎片同样也进行了初始化。与此同一时候,能够发现:
在这之前显示的第一个Fragment则经过了onPause到onDestoryView的生命周期变化,也就是说这时第一个Fragment的视图会被销毁。
这事实上就是ViewPager自身的一个缓存机制,默认情况下它会帮我们缓存一个Fragment相邻的两个Fragment对象。简单来说,就像上面表现的:
当第一个Fragment须要显示时。其相邻的第二个对象也会进行初始化。第三个Fragment须要显示时,左边第二个对象已经完毕了初始化,于是右边的第四个则会进行初始化。我们不难猜測出设计者如此设计的初衷:显然这是为了用户对ViewPager有更好的体验。设想一下:
- 当用户进入到ViewPager的第一个视图。这时相邻的第二个视图也已经进行了初始化。那么当用户切换到第二页,则能够直接进行浏览了。
- 当用户切换到第三页的时候,之所以选择销毁掉第一页的视图,则是为了降低嵌入的Fragment数量,降低滑动时出现卡顿的可能性。
setOffscreenPageLimit
了解了ViewPager的缓存机制,则有一个比較有用的东西叫做setOffscreenPageLimit,它的作用就是来设置这个缓存的上限。
这个上限的默认值为1,而当该值为1时,其效果就和我们上一节描写叙述的一样。我们能够自己设置该值来改变这个缓存的数量。
但与此同一时候。须要注意的还有一个细节是,要避免做出相似例如以下代码所看到的的这样的想当然的操作:
mViewPager.setOffscreenPageLimit(0);
这行代码是无法完毕你本来想要实现的目的的,究其原因,能够在源代码中找到答案:
从代码中不难看到,当我们传入的limit參数小于DEFAULT_OFFSCREEN_PAGES时。就将直接被设置为等同于这个默认值。
那么DEFAULT_OFFSCREEN_PAGES的值到底是多少呢?事实上从前面的分析就能得出结论,其值为1:
setUserVisibleHint
我们或许已经留意到。在前面的生命周期变化中,有一个叫做setUserVisibleHint的东西重复出现了不少次。事实上有了之前的基础,就easy理解了。
之前通过FragmentManager控制碎片的隐藏和显示,回调的是onHiddenChanged方法。而在ViewPager则没有通过FM来进行控制。
所以不难猜測,这时Fragment对象的显示和隐藏。回调多半就不是onHiddenChanged了。事实正是如此,此时的回调则是setUserVisibleHint。
所以说,假设想要在这样的情况监听Fragment对象的隐藏/显示,那么监听这种方法就能够了。这也理解为我们本文提出的问题的第四种场景延伸。
最后,这里注意一下这样的情景与我们之前描写叙述的第三种场景的差别。
之前说到的对于ViewPager中的碎片的显示/隐藏状态监听的解决方式,是从针对从其他Fragment切换到ViewPager中的某个Fragment显示的情景。
而监听setUserVisibleHint则是针对于都是位于ViewPager内的Fragment对象相互之前的切换显示的情况。
ViewPager的懒载入
事实上写到这里,对于我能想到的关于本文最初说到的那个朋友提出的问题 常见的情景延伸都已经总结到了,本想结束。
可是前面说到ViewPager的缓存机制时,我们提到ViewPager会依据设置的缓存数量上限来控制相应数量的Fragment对象提前初始化。
这就可能涉及到还有一个比較有用的小技巧:配合ViewPager时,Fragment的懒载入。尽管网上该类资料非常多,但还是能够简单总结一下。
所谓的懒载入事实上非常好理解。我们说了正常情况下。ViewPager会对某指定数量的Fragment进行预初始化。
而通常在Fragment初始化的生命周期里:我们都会做一些与该Fragment相关的数据的载入工作等等。那么:
在一些时候。比方某个Fragment在初始化时须要载入的数据量较大;或者说由于数据来源于网络等原因。
此时等待该Fragment完毕初始化。就会从一定程度上影响到应用的效率。这个时候就产生了所谓的“懒载入”的需求。
事实上懒载入的本质非常easy:那就是不要在ViewPager预初始化的时候去载入数据。而是当该Fragment真正显示时才进行载入。
由于我们之前已经知道了setUserVisibleHint这个东西。所以事实上解决方式就不难给出了。这里能够给出一个简单的模板,仅供參考:
public abstract class LazyLoadFragment extends Fragment{
protected boolean isPrepared;
protected boolean isLoadedOnce;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutId(), container, false);
isPrepared = true;
// 数据载入
loadData();
return view;
}
protected abstract int getLayoutId() ;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
loadData();
}
protected void loadData() {
if(!isPrepared || !getUserVisibleHint() || isLoadedOnce)
return;
// 懒载入
lazyLoad();
isLoadedOnce = true;
}
protected abstract void lazyLoad();
@Override
public void onDetach() {
super.onDetach();
isPrepared = isLoadedOnce = false;
}
}
上面的代码应该不难理解,假设须要实现懒载入。则能够让Fragment继承该类,然后再覆写用于载入数据的lazyLoad方法就能够了。