是什么
Fragment (碎片)是 Android 中的行为或用户界面部分。
为什么
在 Android 中引入 Fragment 主要是为了给大屏幕 (如平板电脑) 上更加动态和灵活的UI设计提供支持。使用Fragment可以在app适配平板时无需大范围的更改布局。下图是google提供的一个示例图:
如上两幅图中可以看出, 通过使用两个 Fragment 进行不同的排列组合可以很好的适配平板和手机两种不同布局风格, 而减少大量的布局开发作业.
时间点
Android 在 Android 3.0(API 级别 11)中引入了片段, 如果使用v4包可以兼容到 Android 1.6;
如果使用 V4 包的话需要注意一些地方:
1. 果你使用了v4包下的 Fragment , 那么所在的那个Activity就要继承FragmentActivity
.
2. 如果要使用FragmentManager必须使用getSupportFragmentManager()
;
怎么用
1. 生命周期
管理 Fragment 的生命周期与管理 Activity 的生命周期非常相似,片段也以三种状态存在:
1. 继续
片段在运行中的 Activity 中可见。
2. 暂停
另一个 Activity 位于前台并具有焦点,但此片段所在的 Activity 仍然可见(前台 Activity 部分透明,或未覆盖整个屏幕)。
3. 停止
片段不可见。宿主 Activity 已停止,或片段已从 Activity 中移除,但已添加到返回栈。 停止片段仍然处于活动状态(系统会保留所有状态和成员信息)。 不过,它对用户不再可见,如果 Activity 被终止,它也会被终止。
Fragment 与 Activity 的生命周期最显著的区别是它们各自返回栈中的存储方式, 和 Activity 停止时自动放入由系统管理的 Activity 返回栈不同, Fragment 仅在被移除的事务执行期间调用addToBackStack()
显示请求保存实例时, 系统才会将 Fragment 放入由宿主 Activity 管理的返回栈.
Fragment 与 Activity 的生命周期具有协调一致性, 这体现在 Activity 的每次生命周期回调都会引发每个片段的类似回调. 例如, 当 Activity 收到 onPause() 时, Activity 中的每个片段也会收到 onPause(). Fragment中还有一些额外的方法:
onAttach()
在片段已与 Activity 关联时调用(Activity 传递到此方法内)。
onCreateView()
调用它可创建与片段关联的视图层次结构。
onActivityCreated()
在 Activity 的 onCreate() 方法已返回时调用。
onDestroyView()
在移除与片段关联的视图层次结构时调用。
onDetach()
在取消片段与 Activity 的关联时调用。
使用 <fragment>
标签引入 Fragment 的 Activity 的创建到销毁, 生命周期方法调用的顺序:
-Fragment: onCreate
-Fragment: onCreateView
-Fragment: onViewCreated
-Activity: onCreate
-Fragment: onActivityCreated
-Activity: onStart
-Fragment: onStart
-Activity: onResume
-Fragment: onResume
-Fragment: onPause
-Activity: onPause
-Fragment: onStop
-Activity: onStop
-Fragment: onDestroyView
-Fragment: onDestroy
-Fragment: onDetach
-Activity: onDestroy
2. 管理 Fragment
要想管理 Activity 中的 Fragment, 需要使用 FragmentManager. 可以从 Activity 中调用 getFragmentManager() 获取.
可以使用 FragmentManager 执行的操作包括:
- 通过 findFragmentById()(对于在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(对于提供或不提供 UI 的片段)获取 Activity 中存在的片段。
- 通过 popBackStack()(模拟用户发出的返回命令)将片段从返回栈中弹出。
- 通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器。
注意: 在 Fragment 的嵌套情况下在父 Fragment 中使用 getFragmentManager() / getSupportFragmentManager() 获取 FragmentManager; 在子 Fragment 中需要使用getChildFragmentManager 来获取.
2. Fragment的简单使用
- 使用< fragment >标签在布局中添加碎片
- 通过 android:name 属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上。
<fragment
android:id = "@+id/left_fragment"
android:name = "com.example.FragmentTest.fragment.HomeFragment"
android:layout_width = "1dp"
android:layout_height = "match_parent"
android:layout_weight = "1"
tools:layout = "@layout/left_fragment"/>
注:每个片段都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复片段(您也可以使用该标识符来捕获片段以执行某些事务,如将其移除)。 可以通过三种方式为片段提供 ID:
- 为 android:id 属性提供唯一 ID。
- 为 android:tag 属性提供唯一字符串。
- 如果您未给以上两个属性提供值,系统会使用容器视图的 ID。
3. 动态添加Fragment
碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中
- 创建待添加的碎片实例。
- 获取到 FragmentManager,在活动中可以直接调用 getFragmentManager()方法得到。
- 开启一个事务,通过调用 beginTransaction()方法开启。
- 向容器内加入碎片,可以使用 replace() 或 add() 方法实现,需要传入容器的 id 和待添加的碎
片实例。 - 提交事务,调用 commit()方法来完成。
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.fg_main, new MainFragment());
ft.addToBackStack(null);
ft.commit();
注意:
1. 调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程(“主”线程)可以执行该操作时再安排其在线程上运行。不过,如有必要,您也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。
2. 您只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。
4. Fragment 和 Activity 之间进行通讯
- 在 Activity 中获取相应 Fragment 的实例, 可以通过 FragmentManager 使用 findFragmentById() 或 findFragmentByTag().
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);
- 得到当前碎片相关联的活动实例
MainActivity activity = (MainActivity) getActivity();
两个 Fragment 之间如何传递信息
我认为这个其实蛮简单的, 就是通过 FragmentManager 开启一个事务来实现, 在使用 add() / replace() 切换Fragment时通过目标Fragment的实例进行传递数据等各种操作; 关键是如何把事务
抽取出来作为公共的方法.(个人意见, 可能)在 Fragment 中也可以使用
startActivity()
和startActivityForResult()
来启动Activity并进行交互操作.
遇到的坑
对于Fragment中的各种坑推荐大神的Fragment全解析系列, 以及大神所写的Fragmentation库, 以下就只写一些我自己遇到的问题了.
1. getActivity() return null
原因:解决:大部分原因是由于在 Fragment 与 Activity 断开关系 (onDetach() 被调用) 后使用 getActivity() (e.g. 异步网络访问中onDetach() 被调用, 获取访问结果后调用 getActivity() 操作布局)
- 最好的办法就是我们应该避免在已经onDetach这种情况之后再去调用宿主Activity对象,比如取消这些异步任务;
- 还可以在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;
}
2. 异常:Can not perform this action after onSaveInstanceState
原因:解决: 1. (不推荐)该事务使用`commitAllowingStateLoss()`方法提交, 但是有可能导致该次提交无效!(在此次离开时恰巧Activity被强杀时) 2. (推荐)在重新回到该Activity的时候(onResumeFragments()或onPostResume()), 再执行该事务!在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你使用commit()提交了Fragment事务,就会抛出该异常!
注意事项
参数传递
对Fragment传递数据,建议使用`setArguments(Bundle args)`,而后在onCreate中使用`getArguments()`取出,在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent恢复机制类似。创建 Fragment 对象
使用`newInstance(参数) `创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。public static class TestFragment extends Fragment
{
private static final String ARG = "arg";
public static Fragment newInstance(String arg)
{
TestFragment fragment = new TestFragment();
Bundle bundle = new Bundle();
bundle.putString( ARG, arg);
fragment.setArguments(bundle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout. fragment_main, container, false);
TextView tv = (TextView) rootView.findViewById(R.id. tv);
tv.setText(getArguments().getString( ARG));
return rootView;
}
}
add() 和 replace()
add() 和 replace() 的区别
- show(),hide()最终是让Fragment的View setVisibility(true还是false),不会调用生命周期;
- replace()的话会销毁视图,即调用onDestoryView、onCreateView等一系列生命周期;
使用建议
如果你有一个很高的概率会再次使用当前的Fragment,建议使用show(),hide(),可以提高性能。
在我使用Fragment过程中,大部分情况下都是用show(),hide(),而不是replace()。
- onHiddenChanged()
当使用add() + show(),hide()跳转新的Fragment时,旧的Fragment回调onHiddenChanged()
,不会回调onStop()等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged(),这点要切记。
注意:如果你的app有大量图片,这时更好的方式可能是replace,配合你的图片框架在Fragment视图销毁时,回收其图片所占的内存。
使用 ViewPager + Fragment 时使用懒加载
原文链接
- 为什么我们要做懒加载呢?
我们在做应用开发的时候,一个Activity里面可能会以viewpager(或其他容器)与多个Fragment来组合使用,而如果每个fragment都需要去加载数据,或从本地加载,或从网络加载,那么在这个activity刚创建的时候就变成需要初始化大量资源。这样的结果,我们当然不会满意。那么,能不能做到当切换到这个fragment的时候,它才去初始化(不同于View的初始化,这里指的是数据的加载)呢?
答案就在Fragment里的setUserVisibleHint这个方法里具体可查看android API
该方法用于告诉系统,这个Fragment的UI是否是可见的。所以我们只需要继承Fragment并重写该方法,即可实现在fragment可见时才进行数据加载操作,即Fragment的懒加载。
为什么不使用ViewPager的预加载功能呢
现在大体上放置ViewPager预加载的方法有两种:在使用ViewPager嵌套Fragment的时候,由于VIewPager的几个Adapter的设置来说,都会有一定的预加载(默认是左右各一个Frament)。通过设置
setOffscreenPageLimit
(int number) 来设置预加载的熟练,在V4包中,默认的预加载是1,即使你设置为0,也是不起作用的,设置的只能是大于1才会有效果的。我们需要通过更改V4包中的默认属性才可以。限制预加载,会出现滑动过程中卡顿现象。其实Fragment中防止预加载主要是防止数据的预加载,Fragment中的VIew预加载是有好处的,我们可以通过Fragment中的一个方法来达到预加载View 但是不加载数据,在Fragment显示的时候才去加载数据。
public abstract class LazyFragment extends Fragment
{
protected boolean isVisible;
/**
* 在这里实现Fragment数据的缓加载.
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser)
{
super.setUserVisibleHint(isVisibleToUser);
if(getUserVisibleHint())
{
isVisible = true;
onVisible();
} else {
isVisible = false;
onInvisible();
}
}
protected void onVisible()
{
lazyLoad();
}
protected abstract void lazyLoad();
protected void onInvisible(){}
}
在LazyFragment,增加了三个方法,一个是onVisiable,即fragment被设置为可见时调用,一个是onInvisible,即fragment被设置为不可见时调用。另外再写了一个lazyLoad的抽象方法,该方法在onVisible里面调用。你可能会想,为什么不在getUserVisibleHint里面就直接调用呢?
我这么写是为了代码的复用。因为在fragment中,我们还需要创建视图(onCreateView()方法),可能还需要在它不可见时就进行其他小量的初始化操作(比如初始化需要通过AIDL调用的远程服务)等。而setUserVisibleHint是在onCreateView之前调用的,那么在视图未初始化的时候,在lazyLoad当中就使用的话,就会有空指针的异常。而把lazyLoad抽离成一个方法,那么它的子类就可以这样做:
public class OpenResultFragment extends LazyFragment
{
// 标志位,标志已经初始化完成。
private boolean isPrepared;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
Log.d(LOG_TAG, "onCreateView");
View view = inflater.inflate(R.layout.fragment_open_result, container, false);
//XXX初始化view的各控件
isPrepared = true;
//这里再一次调用lazyLoad()方法是因为setUserVisibleHint()是在onCreateView()方法之后调用的
lazyLoad();
return view;
}
@Override
protected void lazyLoad()
{
if(!isPrepared || !isVisible)
{
return;
}
//填充各控件的数据
}
}