Fragment之Fragmentation库(添加转场动画)

0.641字数 3892阅读 5581

部分内容来源于别人的总结,如有冒犯侵权,请告知! 邮箱:simoncqhy@163.com.谢谢!我只想做一个记录,以免自己以后出现不知道怎么解决.

Fragmentation库:非常适合单Activity+多Fragment 或者 多模块Activity+多Fragment的架构

来自一个github上的一个开源库:
https://github.com/YoKeyword/Fragmentation

特性

1、快速开发出各种嵌套设计的Fragment App
2、实时查看Fragment的(包括嵌套Fragment)栈视图的对话框和Log,方便调试
3、增加启动模式、startForResult等类似Activity方法
4、类似Android事件分发机制的Fragment回退方法:onBackPressedSupport(),轻松为每个Fragment实现Back按键事件
5、New!!! 提供onSupportVisible()等生命周期方法,简化嵌套Fragment的开发过程; 提供统一的onLazyInitView()懒加载方法
6、提供靠谱的 Fragment转场动画 的解决方案
7、更强的兼容性, 解决多点触控、重叠等问题
8、支持SwipeBack滑动边缘退出(需要使用Fragmentation_SwipeBack库,
在使用fragment的过程中,我相信大家都遇到很多坑,在开发中,虽然谷歌推出了碎片化处理,但是里面本身还有很多bug
.

在开发中最容易出现的bug如下:

1、getActivity()空指针
2、异常:Can not perform this action after onSaveInstanceState
3、Fragment重叠异常-----正确使用hide、show的姿势
4、Fragment嵌套的那些坑
5、未必靠谱的出栈方法remove()
6、多个Fragment同时出栈的深坑BUG
7、深坑 Fragment转场动画

getActivity()空指针

可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。
大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。
比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。
解决方案:
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些)即:

protected Activity mActivity;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.mActivity = activity;
}

/**
如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替
*/

@Override
public void onAttach(Context context) {
super.onAttach(context);
this.mActivity = (Activity)context;
}

异常:Can not perform this action after onSaveInstanceState
在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你使用commit()提交了Fragment事务,就会抛出该异常!
解决方法2个:
1、(不推荐)该事务使用commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!(在此次离开时恰巧Activity被强杀时)
2、(推荐)在重新回到该Activity的时候(onResumeFragments()或onPostResume()),再执行该事务!

Fragment重叠异常-----正确使用hide、show的姿势和replace(坑相对来说要少些)

原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;
但是因为没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
(如果是replace,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace不会造成重叠现象)

还有一种场景,add和replace都有可能造成重叠: 在onCreate中加载Fragment,并且没有判断saveInstanceState==null,导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)

Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 ;
if(saveInstanceState == null){
// 正常情况下去 加载根Fragment
}
}

这里给出3个解决方案:
1、是大家比较熟悉的 findFragmentByTag:
即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。

下面是个标准恢复写法:

Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
TargetFragment targetFragment;
HideFragment hideFragment;
if (savedInstanceState != null) { // “内存重启”时调用
targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
// 解决重叠问题
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常时
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();
getFragmentManager().beginTransaction()
.add(R.id.container, targetFragment, targetFragment.getClass().getName())
.add(R.id,container,hideFragment,hideFragment.getClass().getName())
.hide(hideFragment)
.commit();
}
}

Fragment嵌套的那些坑
在support 23.2.0以下的支持库中,对于在嵌套子Fragment的startActivityForResult (),会发现无论如何都不能在onActivityResult()中接收到返回值,只有最顶层的父Fragment才能接收到,这是一个support v4库的一个BUG,不过在前两天发布的support 23.2.0库中,已经修复了该问题,嵌套的子Fragment也能正常接收到返回数据了

未必靠谱的出栈方法remove()
如果你想让某一个Fragment出栈,使用remove()在加入回退栈时并不靠谱。
如果你在add的同时将Fragment加入回退栈:addToBackStack(name)的情况下,它并不能真正将Fragment从栈内移除,如果你在2秒后(确保Fragment事务已经完成)打印getSupportFragmentManager().getFragments(),会发现该Fragment依然存在,并且依然可以返回到被remove的Fragment,而且是空白页面。
如果你没有将Fragment加入回退栈,remove方法可以正常出栈。
如果你加入了回退栈,popBackStack()系列方法才能真正出栈,这也就引入下一个深坑,popBackStack(String tag,int flags)等系列方法的BUG。
多个Fragment同时出栈的深坑BUG
在Fragment库中如下4个方法是有BUG的:

1、popBackStack(String tag,int flags)
2、popBackStack(int id,int flags)
3、popBackStackImmediate(String tag,int flags)
4、popBackStackImmediate(int id,int flags)

深坑 Fragment转场动画

如果你的Fragment没有转场动画,或者使用setCustomAnimations(enter, exit)的话,那么上面的那些坑解决后,你可以愉快的玩耍了。

getFragmentManager().beginTransaction()
.setCustomAnimations(enter, exit)
// 如果你有通过tag/id同时出栈多个Fragment的情况时,
// 请谨慎使用.setCustomAnimations(enter, exit, popEnter, popExit)
// 因为在出栈多Fragment时,伴随出栈动画,会在某些情况下发生异常
// 你需要搭配Fragment的onCreateAnimation()临时取消出栈动画,或者延迟一个动画时间再执行一次上面提到的Hack方法,排序
(注意:如果你想给下一个Fragment设置进栈动画和出栈动画,.setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;
请使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,所以是.setCustomAnimations(进栈动画, exit, popEnter, 出栈动画))

1、一些使用建议
1、对Fragment传递数据,建议使用setArguments(Bundle args)
,而后在onCreate
中使用getArguments()
取出,在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent恢复机制类似。
2、使用newInstance(参数)
创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。
3、如果你需要在Fragment中用到宿主Activity对象,建议在你的基类Fragment定义一个Activity的全局变量,在onAttach
中初始化,这不是最好的解决办法,但这可以有效避免一些意外Crash。详细原因参考第一篇的“getActivity()空指针”部分。

protected Activity mActivity;@Overridepublic void onAttach(Activity activity) { super.onAttach(activity); this.mActivity = activity;}

2、add(), show(), hide(), replace()的那点事
1、区别show()
,hide()
最终是让Fragment的View setVisibility
(true还是false),不会调用生命周期;replace()
的话会销毁视图,即调用onDestoryView、onCreateView等一系列生命周期;
add()
和 replace()
不要在同一个阶级的FragmentManager里混搭使用。
2、使用场景如果你有一个很高的概率会再次使用当前的Fragment,建议使用show()
,hide()
,可以提高性能。
在我使用Fragment过程中,大部分情况下都是用show()
,hide()
,而不是replace()

注意:如果你的app有大量图片,这时更好的方式可能是replace,配合你的图片框架在Fragment视图销毁时,回收其图片所占的内存。
3、onHiddenChanged的回调时机当使用add()
+show(),hide()
跳转新的Fragment时,旧的Fragment回调onHiddenChanged()
,不会回调onStop()
等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged()
,这点要切记。
4、Fragment重叠问题使用show()
,hide()
带来的一个问题就是,如果你不做任何额外处理,在“内存重启”后,Fragment会重叠;(该BUG在support-v4 24.0.0+以上 官方已修复)
有些小伙伴可能就是为了避免Fragment重叠问题,而选择使用replace()
,但是使用show()
,hide()
时,重叠问题很简单解决的:
如果你在用24.0.0+的版本,需要特殊处理,官方已经修复该BUG;
如果你在使用小于24.0.0以下的v4包,可以参考9行代码让你App内的Fragment对重叠说再见
4、使用FragmentPagerAdapter+ViewPager的注意事项

使用FragmentPagerAdapter+ViewPager时,切换回上一个Fragment页面时(已经初始化完毕),不会回调任何生命周期方法以及onHiddenChanged(),只有setUserVisibleHint(boolean isVisibleToUser)会被回调,所以如果你想进行一些懒加载,需要在这里处理。

在给ViewPager绑定FragmentPagerAdapter时,
new FragmentPagerAdapter(fragmentManager)的FragmentManager,一定要保证正确,如果ViewPager是Activity内的控件,则传递getSupportFragmentManager(),如果是Fragment的控件中,则应该传递getChildFragmentManager()。只要记住ViewPager内的Fragments是当前组件的子Fragment这个原则即可。

你不需要考虑在“内存重启”的情况下,去恢复的Fragments的问题,因为FragmentPagerAdapter已经帮我们处理啦。

5、是使用单Activity+多Fragment的架构,还是多模块Activity+多Fragment的架构?

单Activity+多Fragment:

一个app仅有一个Activity,界面皆是Frament,Activity作为app容器使用。
优点:性能高,速度最快。参考:新版知乎 、google系app
缺点:逻辑比较复杂,尤其当Fragment之间联动较多或者嵌套较深时,比较复杂。
多模块Activity+多Fragment:
一个模块用一个Activity,比如
1、登录注册流程:
LoginActivity + 登录Fragment + 注册Fragment + 填写信息Fragment + 忘记密码Fragment
2、或者常见的数据展示流程:
DataActivity + 数据列表Fragment + 数据详情Fragment + ...
优点:速度快,相比较单Activity+多Fragment,更易维护。
2017年5月17日
更新内容
Fragment 想要实现切换页面的时候进行实时更新,我们都知道Activity中想要达到这种效果,我们只需要了解其生命周期就可以,只要activity没有被销毁,当我们进行页面切换的时候,刷新网络数据,只需要在onResume()方法中执行代码就行,但是这种缺点就是,数据量太大会出现加载缓慢,甚至加载不出来,超时,对于这种只是适量于先少量数据.
那如果在fragment中怎么实现呢?当然这也是有办法的,Fragment既然是Activity的一个碎片自然也是能够实现的.方法就不是一样的了.官方文档中给出了两个方法:
方法一:

Paste_Image.png

这个方法官方文档的解释如下:
/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* <p>An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.</p>
*
* <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
Frament里面有一个setUserVisibleHint方法,setUserVisibleHint每次fragment显示与隐藏都会调用
由于setUserVisibleHint优于onCreate调用,所以当onCreate调用完毕setUserVisibleHint就不会触发,使用这个方法可能会有一些问题,因为这个方法是在onCreate方法之后运行的,一进来有时候可能看不到第一页的数据,这个时候
你可以在onCreate 或者是onCreateView里面进行判断
if (getUserVisibleHint()) {
//加载数据相当于Fragment的onPause
}
这样就能看到第一页的数据了!
方法二:
public void onHiddenChanged(boolean hidden) {
}

官方解释:
/**
* Called when the hidden state (as returned by {@link #isHidden()} of
* the fragment has changed. Fragments start out not hidden; this will
* be called whenever the fragment changes state from that.
* @param hidden True if the fragment is now hidden, false otherwise.
*/
意思就是说: 如果该Fragment对象已经被隐藏,那么它返回true。默认情况下,Fragment是被显示的。能够用onHiddenChanged(boolean)回调方法获取该Fragment对象状态的改变,要注意的是隐藏状态与其他状态是正交的---也就是说,要把该Fragment对象显示给用户,Fragment对象必须是被启动并不被隐藏。
方法区别:
当fragment结合viewpager使用时 setUserVisibleHint方法会调用,而onHiddenChanged方法不会调用.
如果没有使用到viewpager setUserVisibleHint方法不会调用,而onHiddenChanged方法会调用.

重点部分———添加转场动画

   Fragment的转场动画一般有两种,Fragment的设置需要在transaction.add 或transaction.remove之前。一种android提供了默认方法,一种自定义动画
      //淡入淡出的默认动
       transaction = getSupportFragmentManager().beginTransaction();  transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

//设置自定义过场动画
transaction.setCustomAnimations(
R.anim.push_left_in,
R.anim.push_left_out,
R.anim.push_left_in,
R.anim.push_left_out);

动画文件放置位置: res/anim: 这是兼容API-11以下的,只能有四种补间动画方式
//push_left_in_no_alpha,acitivity转场的时候用alpha会不好看
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="200"
android:fromXDelta="100%p"
android:toXDelta="0" />
</set>

//push_left_out_no_alpha
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="200"
android:fromXDelta="0"
android:toXDelta="-100%p" />
</set>

//push_right_in_no_alpha
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="200"
android:fromXDelta="-100%p"
android:toXDelta="0" />
</set>

//push_right_out_no_alpha
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="200"
android:fromXDelta="0"
android:toXDelta="100%p" />
</set>
用set标签的意思是可以集合多个动画一起执行,也可以自行选择单个动画,如alpha等。

推荐阅读更多精彩内容