Android Fragment 详解(未完...)
版权声明:本文为博主原创文章,未经博主允许不得转载。
微博:厉圣杰
源码:AndroidDemo/Fragment
文中如有纰漏,欢迎大家留言指出。
之前写过一篇关于 Fragment 生命周期的文章,针对 Fragment 各种情况下的生命周期进行了讨论,这段时间对 Fragment 的使用次数逐渐增加,对 Fragment 也有一定的了解,写此文以做总结。本文中使用 support library v4 包中的 Fragment。
本文大致会从以下几个方面对 Fragment 进行总结,如有不当之处,欢迎指出~
Fragment 简介
Fragment 是在 Android 3.0 中引入,主要用于在大屏设备(如平板电脑)上支持更多动态和灵活的 UI 设计。 因为平板的屏幕比手机屏幕大得多,所以有更多的空间放更多的 UI 控件,也会产生更多的交互。Fragment 可以将 Activity 中的 UI 划分到自身,以实现在多个 Activity 中重用。
下图来自 Android 官方文档,很好的说明了 Fragment 的设计理念:
当在平板电脑上运行时,App 可以在 Activity A 中嵌入 2 个 Fragment。当用户在 Fragment A 中选中某个 Item 时就可以直接在 Fragment B 中展示内容。而在手机等小屏幕设备上,由于没有足够的空间,Activity A 只能展示 Fragment A,并且只能通过启动 Activity B 才能展示选中 Item 的内容。
Fragment 有自己的布局,有自己的生命周期,有自己的响应事件。但 Fragment 又依赖于 Activity 的存在,你可以把多个 Fragment 嵌入到一个 Activity 中或者多个 Activity 重用一个 Fragment。Activity 的生命周期直接影响 Fragment 的生命周期。在 Activity 运行过程中,可以添加、移除或者替换Fragment。
Fragment 使用比较多的直接子类有:
DialogFragment:一种特殊的 Fragment ,可以创建类似 Dialog 的对话框。
因为是 Fragment ,所以可以管理其生命周期。官方推荐使用。
ListFragment:类似于 ListActivity
PreferenceFragment:类似于 PreferenceActivity
关于 Fragment 子类的使用方式本文不在赘述,有兴趣的可以自行上网搜索~
Fragment 的创建
Fragment 可以通过 XML 静态添加,也可以通过代码动态添加,而动态添加又可以分为通过 new 一个Fragment 和通过 newInstance() 生成一个 Fragment 实例。
静态添加Fragment
首先,我们创建一个布局文件 fragment_test.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="This is a Fragment"
android:textSize="20sp" />
</LinearLayout>
然后创建 TestFragment
,该类继承作者自己封装的 BaseFragment
,与系统自带的 Fragment 有些许差异,但不影响理解
public class TestFragment extends BaseFragment {
@BindView(R.id.tv_content)
TextView mTvContent;
private String mTitle;
private int mBgColor;
public static TestFragment newInstance(String title, int bgColor) {
Bundle args = new Bundle();
TestFragment fragment = new TestFragment();
args.putString(Constant.EXTRA_TITLE, title);
args.putInt(Constant.EXTRA_COLOR, bgColor);
fragment.setArguments(args);
return fragment;
}
@Override
protected int getPageLayoutID() {
return R.layout.fragment_test;
}
@Override
protected void initData() {
if (getArguments() == null) {
return;
}
mTitle = getArguments().getString(Constant.EXTRA_TITLE);
mBgColor = getArguments().getInt(Constant.EXTRA_COLOR);
}
@Override
protected void initView(View view, Bundle savedInstanceState) {
if (!TextUtils.isEmpty(mTitle)) {
mTvContent.setText(mTitle);
}
mTvContent.setBackgroundColor(mBgColor);
}
@Override
protected void initViewListener() {}
@Override
protected void process(Bundle savedInstanceState) {}
}
接下去,在 Activity 的布局文件中添加 TestFragment
即可,使用方法与普通控件相似。
activity_static_create.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_static_create"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.littlejie.demo.modules.base.fragment.create.StaticCreateActivity">
<!-- name 属性用于指定使用哪个 Fragment -->
<fragment
android:name="com.littlejie.demo.modules.base.fragment.create.TestFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Activity 布局相对简单,这里就不再展示代码。运行效果如下:
动态添加 Fragment
现在我们已经学会了如何在 XML 中静态添加 Fragment,但这只是最基础的使用。Fragment 的强大之处在于其可以动态的添加到 Activity 当中。
这里我们依旧使用上一节中 TestFragment
的代码,这也是 Fragment 复用性的体现。
主界面由两个 Button 和一个 FrameLayout 组成。
activity_dynamic_create.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_dynamic_create"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.littlejie.demo.modules.base.fragment.create.DynamicCreateActivity">
<Button
android:id="@+id/btn_show_fragment1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="显示Fragment1" />
<Button
android:id="@+id/btn_show_fragment2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="显示Fragment2" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
DynamicCreateActivity
继承自作者封装的 BaseActivity
,代码如下:
public class DynamicCreateActivity extends BaseActivity {
@Override
protected int getPageLayoutID() {
return R.layout.activity_dynamic_create;
}
@Override
protected void initData() {}
@Override
protected void initView() {}
@Override
protected void initViewListener() {}
@Override
protected void process() {}
@OnClick(R.id.btn_show_fragment1)
public void showFragment1() {
showFragment(TestFragment.newInstance("This is Fragment1", Color.YELLOW));
}
@OnClick(R.id.btn_show_fragment2)
public void showFragment2() {
showFragment(TestFragment.newInstance("This is Fragment2", Color.CYAN));
}
private void showFragment(Fragment fragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
}
程序运行效果如图:
![2017-01-23 14_49_51-w300](http://odsdowehg.bkt.clouddn.com/2017-01-23 14_49_51.gif)
动态添加 Fragment 的主要逻辑集中在 showFragment()
中,该过程可以分解为以下几步:
1. 获取 FragmentManager ,在 V4 包中通过 `getSupportFragmentManager()`,在系统中原生的 Fragment 是通过 `getFragmentManager()` 获得的。
2. 调用 `beginTransaction()` 开启事务
3. 向 FrameLatout 添加 Fragment,一般使用 add() 或者replace() 方法实现,需要传入容器的 id 和 Fragment 的实例
4. 调用 `commit()` 方法提交事务
通过上述例子,你是不是对 Fragment 的使用有了初步的了解呢?
补充:new Fragment 与 newInstance() 的区别
当系统内存不足时,Fragment 可能会被销毁,当其再次实例化时,系统只会调用 Fragment 的无参构造函数,一些需要外部传入的参数的初始化过程就无法完成。而通过 newInstance() 方式就可以将外部传入的在 Fragment 内部,在 Fragment 调用 onCreate()
时将其取出初始化。
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
try {
Class<?> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;
}
return f;
} catch (ClassNotFoundException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
}
}
Fragment 中的重要方法
上一节我们学习了 Fragment 的创建方式,算是对 Fragment 的使用有了初步了解,也使用了 Fragment 中的一些方法,但仅仅这样是不够的。下面,我们来系统的学习下 Fragment 使用中常见的方法。
FragmentManager
FragmentManager 用于管理 Fragment,可以通过 getSupportFragmentManager()
和 getChildFragmentManager()
获取,但两者之间是有差异的。
/**
* 管理 Activity 中的 Fragment,对于 support library v4 包下的 Fragment。
* 类似的有 getFragmentManager() ,不过该方法只支持 Android 3.0 以上。
*/
void getSupportFragmentManager()
/**
* 管理 Fragment 内嵌的 Fragment,要与前者区别开,否则会造成很郁闷的问题。如:add()、replace()、getBackStackEntryCount() 操作的结果会与预期不一致。
*/
void getChildFragmentManager();
FragmentManager 使用频率比较高的有以下方法:
/**
* 开启事务,用于 Fragment 的处理
*/
FragmentTransaction beginTransaction();
/**
* 将 Fragment 从回退栈中弹出,需要与 addToBackStack() 配合使用
* 若回退栈不为空,按返回键时默认弹出回退栈顶端的 Fragment
*/
void popBackStack();
/**
* 针对第一个参数,如果 name 为 null,那么只有顶部的状态被弹出;
* 如果 name 不为 null,并且找到了这个 name 所指向的 Fragment 对象;
* 根据 flags 的值,如果是 flag = 0 ,那么将会弹出该状态以上的所有状态;
* 如果 flag = POP_BACK_STACK_INCLUSIVE ,那么将会弹出该状态(包括该状态)以上的所有状态。
*/
void popBackStack(String name, int flags);
/**
* 针对第一个参数,如果该 id 找不到,那么什么都不做;
* 否则根据 flags 的值,如果是 flag = 0 ,那么将会弹出该状态以上的所有状态;
* 如果 flag = POP_BACK_STACK_INCLUSIVE ,那么将会弹出该状态(包括该状态)以上的所有状态。
*/
void popBackStack(int id, int flags);
/**
* 和 popBackStack(int id, int flags) 类似,不同的是这个事立马弹出,和 executePendingTransactions() 方法之后的效果一样。
* 如果有东西弹出,返回为 true ;否则就是 false 。
*/
boolean popBackStackImmediate();
/**
* 获取回退栈中 Fragment 的个数
*/
int getBackStackEntryCount();
/**
* 根据序号返回后台堆栈中的BackStackEntry对象,最底的序号为0。
*/
FragmentManager.BackStackEntry getBackStackEntryAt(int index);
/**
* 通过 tag 查找 Fragment,这个 tag 可以是 XML 中的,也可以是通过事务动态添加的
*/
Fragment findFragmentByTag(String tag);
/**
* 通过 id 查找 Fragment,这个 id 可以是 XML 中的,也可以是通过事务动态添加的
*/
Fragment findFragmentById(int id);
/**
* 抽象方法,为 Fragment 的回退栈添加一个监听器,用于监听堆栈的改变情况。
*/
public abstract void addOnBackStackChangedListener(OnBackStackChangedListener listener);
/**
* 抽象方法,移除回退栈的监听器
*/
public abstract void removeOnBackStackChangedListener(OnBackStackChangedListener listener);
FragmentTransaction
boolean add():将一个 Fragment 实例添加到最顶端
/**
* 将 Fragment 添加到回退栈,
*/
addToBackStack():
replace():
show():
hide():
commit():
Fragment
以下方法主要用于判断 Fragment 的状态,如可见性等,后续会详细讲解。
/**
* 获取当前 Fragment 的父 Fragment。
* 如果该 Fragment 是直接添加在 Activity 上,那么返回 null
*/
Fragment getParentFragment();
/**
* 如 TestFragment 中,该方法主要为 Fragment 提供一些初始化数据
* 数据以 Bundle 形式封装传递。
*/
void setArguments(Bundle args);
/**
* 获取传递给 Fragment 的参数
*/
Bundle getArguments();
/**
* 判断 Fragment 是否已经添加到 Activity 上
*/
boolean isAdded();
/**
*
*/
boolean isHidden();
void onHiddenChanged();
boolean isVisible();
void setVisibleToUser();
boolean getUserVisibleHint();
Fragment 的生命周期
现在,我们对 Fragment 的使用已经有了详尽的认识。但是,有句古话说的好:工欲善其事,必先利其器。要对 Fragment 有透彻的认识,还得掌握它的生命周期。
阎王叫你五更死,你活不到三更
简单来说,Fragment 的生命周期可以用下图来表示:
是不是觉得这图看着很眼熟,没错,前面讲过 Fragment 是依赖于 Activity 存在的,所以 Fragment 的生命周期跟 Activity 的生命周期很相似。
下图很好的描述了 Fragment 与 Activity 生命周期的关系,请看图~
Fragment 的生命周期这样就讲完了?理论上来说,是的。但是,我还有话要讲~
上面只是展示了 Fragment 与 Activity 生命周期最基本的关系,如果通过 addToBackStack()
将 Fragment 放入回退栈,然后通过 popBackStack()
出栈,Fragment 的生命周期会如何变化呢?如果 Fragment 与 ViewPager 结合使用,Fragment 的生命周期又是如何?如果通过 hide()
和 show()
方法来展示隐藏,这时 Fragment 的生命周期又会如何?
![屏幕快照 2017-01-04 下午8.30.54-w400](http://odsdowehg.bkt.clouddn.com/屏幕快照 2017-01-04 下午8.30.54.png)
不急,先看思维导图中的问题,然后咱们就来研究一下上诉问题~
既然是 Fragment 的生命周期,那自然是少不了对 Fragment 生命周期的监测,怎么办?打 Log。
项目结构如下
![屏幕快照 2017-01-04 下午8.43.37-w400](http://odsdowehg.bkt.clouddn.com/屏幕快照 2017-01-04 下午8.43.37.png)
本文涉及到的类主要集中在 life
包中。代码已上传 AndroidDemo/Fragment ,这里贴出 LifeFragment
的主要代码。
LifeFragment.java
/**
* 测试 Fragment 生命周期,setUserVisibleHint 初始进来时只有默认 Tab
* Created by littlejie on 2016/12/30.
*/
public class LifeFragment extends BaseFragment {
private final String TAG = LifeFragment.class.getSimpleName();
//截取 Fragment.toString() 方法中的标识数字
private final String ID = this.toString().substring(this.toString().indexOf("{") + 1, this.toString().length() - 1);
private TextView mTvContent;
//默认 Title 值
private String mTitle = "Tab";
public static LifeFragment newInstance(String title) {
Bundle args = new Bundle();
LifeFragment fragment = new LifeFragment();
args.putString(Constant.PARAM_TITLE, title);
fragment.setArguments(args);
return fragment;
}
/**
* 当 Fragment 调用 hide() 、 show() 时回调
* @param hidden
*/
@Override
public void onHiddenChanged(boolean hidden) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onHiddenChanged.hidden = " + hidden);
super.onHiddenChanged(hidden);
}
/**
* 当 Fragment 与 ViewPager 结合使用时,切换 Pager 时回调
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is setUserVisibleHint.isVisibleToUser = " + isVisibleToUser);
super.setUserVisibleHint(isVisibleToUser);
}
/**
* Fragment 关联到 Activity 时回调
* 此时 Activity 已经与 Fragment 关联,通过 Context 向下转型,就可以与 Activity 通信
* 当然也可以使用 getActivity(),前提是这个 fragment 已经和宿主的 activity 关联,并且没有脱离
* onAttach 只调用一次。
*
* @param context
*/
@Override
public void onAttach(Context context) {
//由于 onCreate 是在 onAttach 后执行,故此时 mTitle 为空
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onAttach.");
super.onAttach(context);
}
/**
* 系统创建 Fragment 的时候回调,介于 onAttach() 和 onCreateView() 之间
* 一般用于初始化一些数据
* 值得注意的是,此时 Activity 还在创建中,因此不能在执行一些跟 Activity UI 相关的操作
* 否则,会出现一些难以预料的问题,比如:NullPointException
* 如果要对 Activity 上的 UI 进行操作,建议在 onActivityCreated() 中操作
*
* @param savedInstanceState
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onCreate.");
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
if (getArguments() != null) {
mTitle = getArguments().getString(Constant.PARAM_TITLE);
}
//测试 onCreate() 是 Activity 的 UI 是否初始化完成
//if (getContext() instanceof LifeCircleActivity) {
//((LifeActivity) getContext()).setActivityCreated("Fragment 进行 onCreate() 时 Activity UI 尚未初始化完成。"
// + "\n你看不到我,因为我已经变成异常抛出了。");
//}
}
/**
* 创建 Fragment 需要显示的 View,默认返回 null。
* 当返回的 View 不为 null 时,View 被销毁时会调用 onDestroyView()
* 此处应该只进行布局的初始化,而不应该执行耗时操作,如网络请求、数据库读取
*
* @param inflater
* @param container
* @param savedInstanceState
* @return
*/
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onCreateView.");
return inflater.inflate(R.layout.fragment_life_circle, container, false);
}
/**
* 该方法在 onCreateView() 之后会被立即执行
* 此时可以对 View 对象进行赋值,当然在 onCreateView() 中也可以执行
*
* @param view
* @param savedInstanceState
*/
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onViewCreated.");
super.onViewCreated(view, savedInstanceState);
mTvContent = (TextView) view.findViewById(R.id.tv_content);
mTvContent.setText(mTitle);
}
/**
* 当 Activity 执行完 onCreate() 方法后调用
* 此时可以执行与 Activity 相关的 UI 操作
*
* @param savedInstanceState
*/
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onActivityCreated.");
super.onActivityCreated(savedInstanceState);
//测试 onCreate() 是 Activity 的 UI 是否初始化完成
if (getContext() instanceof SimpleLifeCircleActivity) {
((SimpleLifeCircleActivity) getContext()).setActivityCreated("Fragment 进行 onActivityCreated() 时 Activity UI 已初始化完成。"
+ "\n你能看到我。");
}
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onViewStateRestored.");
super.onViewStateRestored(savedInstanceState);
}
/*-----------跟 Activity 中对应方法类似-------------*/
@Override
public void onStart() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onStart.");
super.onStart();
}
@Override
public void onResume() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onResume.");
super.onResume();
}
@Override
public void onPause() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onPause.");
super.onPause();
}
@Override
public void onStop() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onStop.");
super.onStop();
}
/*-----------跟 Activity 中对应方法类似-------------*/
/**
* 表示销毁 Fragment 相关联的 UI 布局,清除所有跟视图相关的资源。
* 不一定在 Activity 的 onDestroy() 方法中调用
* 如:Fragment 与 ViewPager 结合使用时
*
* @see com.littlejie.fragment.lifecircle.LifeCircleWithViewPagerActivity
*/
@Override
public void onDestroyView() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onDestroyView.");
super.onDestroyView();
}
/**
* 销毁 Fragment 对象的时候调用,一般是调用 Activity 的 onDestroy() 的时候执行
*/
@Override
public void onDestroy() {
//当调用 Activity 的 onDestroy() 时调用
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onDestroy.");
super.onDestroy();
}
/**
* 解除 Fragment 与 Activity 的关联
*/
@Override
public void onDetach() {
Log.i(TAG, "Fragment id = " + ID + "," + mTitle + " is onDetach.");
super.onDetach();
}
}
由于日志过多,这里不再展示,有兴趣的可自行下载 Demo 测试。这里简要总结下关于 Fragment 生命周期。
- 第一点很重要,最开始的两张图展示的 Fragment 与 Activity 的生命周期关系没毛病。
onAttach()
和onCreate()
只在 Fragment 与 Activity 第一次关联时调用。onDestroy()
和onDetach()
只在 Fragment 的宿主 Activity 销毁时才会被调用。- 根据前 3 点,将 Fragment 通过
addToBackStack()
只涉及onCreateView()
和onDestroyView()
这之间的生命周期。add()
和replace()
不会对 Fragment 的生命周期产生影响,但add()
方法会造成 Fragment 叠加显示。 - Fragment 与 ViewPager 结合使用时的生命周期与第 4 点相似。
- 通过
hide()
、show()
来隐藏、显示Fragment,此时 Fragment 只改变了可见性,并不涉及生命周期的改变 - 第 7 点与Fragment 和 Activity 的生命周期有关,即:不要在 Fragment 的 onCreate() 方法中操作宿主Activity 的 UI。因为你无法保证此时 宿主Activity 的 UI 已经完全初始化。PS:某些情况下是可以确保 宿主Activity 已经初始化完成的。