参考资料:

  • 《Android 编程权威指南》

fragment 是一种控制器对象, activity 可委派它完成一些任务。通常这些任务就是管理用户界面。受管的用户界面可以是一整屏或整屏的一部分。

管理用户界面的 fragment 又称为 UI fragment 。它也有自己产生布局文件的视图。fragment 视图包含了用户可以交互的可视化 UI 元素。

fragment 可以托管在 activity 中。如果有多个 fragment 要插入, activity 视图也可提供多个位置。因此,可联合使用 fragment 及 activity 来组装或重新组装用户界面。在整个生命周期过程中,技术上来说 activity 的视图可保持不变。

用 UI fragment 将应用的 UI 分解成构建块,利用一个个构建块,很容易做到构建分页界面、动画侧边栏界面等更多其他定制界面。

fragment 与支持库

Fragment 是在 API 11 之后被引入的。幸运的是,对于 fragment 来说,保证向后兼容比较容易,仅需使用 Android 支持库中的 fragment 相关类即可。

  1. API 11 之前的支持:使用位于 libs/android-support-v4.jar 内的支持库。支持库包含了 Fragment 类(android.support.v4.app.Fragment),该类可以使用在任何 API 4 级及更高版本的设备上。支持库中的类不仅可以在无原生类的旧版本设备上使用,而且可以代替原生类在新版本设备上使用。
  2. API 11 之后的支持:可以直接使用标准库中的原生 fragment 类。

fragment 的生命周期

下图展示了 fragment 的生命周期,与 activity 的生命周期类似。与 activity 生命周期的一个关键区别在于,fragment 的生命周期方法是由托管在 activity 而不是操作系统调用的。操作系统无从知晓 activity 用来管理视图的 fragment 。fragment 的使用是 activity 自己内部的事情。

fragment 的生命周期方法由 activity 负责调用。

基于 Fragment 的应用开发流程

基本的开发步骤:

  1. 定义 fragment 的布局;
  2. 创建 fragment 类;
  3. 修改 activity 及其布局,实现对 fragment 的托管。

1) 定义 fragment 的布局

一个简单的布局文件示例:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText android:id="@+id/crime_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/crime_title_hint"/>
</LinearLayout>

2) 创建 fragment 类

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CrimeActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_crime, parent, false);
mTitleField = (EditText)v.findViewById(R.id.crime_title);
mTitleField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
return v;
}
}

以上代码有几点值得注意的地方:

  1. Fragment.onCreate(Bundle) 方法及其他 Fragment 生命周期方法必须设计为公共方法(方便被托管 fragment 的任何 activity 调用);
  2. 类似于 activity, fragment 同样具有保存及获取状态的 bundle 。我们也可以根据需要覆盖 Fragment.onSaveInstanceState(Bundle) 方法;
  3. Fragment.onCreate(...) 方法中,并 没有 生成 fragment 的视图。创建和配置 fragment 视图是通过另一个 fragment 的生命周期方法来完成的:

1
2
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState)

通过该方法生成 fragment 视图的布局,然后将生成的 View 返回给托管 activity 。LayoutInflater 及 ViewGroup 是用来生成布局的必要参数。Bundle 包含了供该方法在重建视图所使用的数据。在上面代码中,fragment 的视图是直接通过调用 LayoutInflater.inflate(...) 方法并传入布局的资源 ID 生成的。第二个参数是视图的父视图,通常我们需要父视图来正确配置组件。第三个参数告知布局生成器是否将生成的视图添加给父视图。这里,我们传入了 false 参数,因为我们将通过 activity 代码的方式添加视图。

3) 托管 fragment

托管 fragment 有两种方式:

  1. 布局方式托管 fragment;
  2. 代码方式托管 fragment 。

1. 布局方式托管 fragment

使用布局 fragment ,通常是修改 Activity 的布局,在 fragment 元素节点中指定 fragment 的类。示例:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/helloMoonFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.hahack.hellomoon.HelloMoonFragment">
</fragment>

之后只需修改 Activity 类,使其超类为 FragmentActivity 即可:

1
2
3
4
5
6
7
8
public class HelloMoonActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_moon);
}
}

有得必有失,使用如此简单的方法托管 fragment ,同时也失去了只有显式地使用 FragmentManager 才能获得的灵活性和掌控能力。

  • 可覆盖 fragment 的生命周期方法,以响应各种事件。但无法控制调用这些方法的时机。
  • 无法提交移除、替换、分离布局 fragment 的事务。activity 被创建后,即无法做出任何改变。
  • 无法附加 argument 给布局 fragment。

2. 代码方式托管 fragment

以代码的方式托管 fragment ,activity 必须做到:

  • 定义容器视图:在布局中为 fragment 的视图安排位置;
  • 在 Activity 中创建负责管理 fragment 的 FragmentManager;
  • 添加 fragment 到 FragmentManager。

定义容器视图

在 activity 视图层级结构中为 fragment 视图安排位置。比如使用 FrameLayout 来作为 fragment 的容器视图:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

创建 FragmentManager

FragmentManager 类负责管理 fragment 并将它们的视图添加到 activity 的视图层级结构中。FragmentManager 类具体管理的是:

  • fragment 队列
  • fragment 事务的回退栈

FragmentManager 的关系图如下所示:

创建 FragmentManager 的方法示例如下:

1
2
3
4
5
6
7
8
9
10
11
public class CrimeActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
// 获取 FragmentManager
FragmentManager fm = getSupportFragmentManager();
}
}

因为使用了支持库及 FragmentActivity 类,因此这里调用的方法是 getSupportFragmentManager() 。如果不考虑 Honeycomb 以前版本的兼容性问题,可直接继承 Activity 类并调用 getFragmentManager() 方法。

添加 fragment 到 FragmentManager

获取到 FragmentManager 后,以如下示例方式获取一个 fragment 交由 FragmentManager 管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CrimeActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
// 获取 FragmentManager
FragmentManager fm = getSupportFragmentManager();
// 获取 fragment
Fragment fragment = fm.findFragmentById(R.id.fragmentContainer);
// 创建一个新的 fragment 事务,加入一个添加操作,然后提交该事务
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
}
}
}

fragment 事务被用来添加、移除、附加、分离或替换 fragment 队列中的 fragment 。这是使用 fragment 在运行时组装和重新组装用户界面的核心方式。FragmentManager 管理着 fragment 事务的回退栈。

从 fragment 返回托管 Activity

Fragment 的 getActivity() 方法不仅可以返回托管 Activity ,且允许 fragment 处理更多的 activity 相关事务。

示例:

1
2
3
4
5
6
7
8
9
10
11
public class CrimeListFragment extends ListFragment {
private ArrayList<Crime> mCrimes;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActivity().setTitle(R.string.crimes_title);
mCrimes = CrimeLab.get(getActivity()).getCrimes();
}
}

从 fragment 启动另一 activity

从 fragment 中启动 activity 的实现方式,基本等同于从 activity 中启动另一 activity 的实现方式。我们调用 Fragment.startActivity(Intent) 方法,该方法在后台会调用对应的 Activity 方法。

需要注意的是 fragment 和 activity 之间的信息传递方式。

方法1:附加 extra 信息

简单的方式是直接通过 Intent.putExtra(...) 附加 extra 信息。但这么做会使得 fragment 的封装性大打折扣。

示例:

fragment 中:

1
2
3
4
5
6
7
8
public void onListItemClick(ListView l, View v, int position, long id) {
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
// Start CrimeActivity
Intent i = new Intent(getActivity(), CrimeActivity.class);
i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId());
startActivity(i);
}

activity 的响应:

1
2
3
4
5
6
7
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UUID crimeId = (UUID)getActivity().getIntent().getSerializableExtra(EXTRA_CRIME_ID);
mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
}

方法2:Fragment argument

另一种方式是利用 Fragment argument 。

每个 fragment 实例都可附带一个 Bundle 对象。该 bundle 包含有 key-value 对,我们可以如同附加 extra 到 Activity 的 intent 中那样使用它们。一个 key-value 对即一个 argument 。

1. 创建 fragment argument

要创建 fragment argument ,首先需创建 Bundle 对象。然后,使用 Bundle 限定类型的 “put” 方法(类似于 Intent 的方法),将 argument 添加到 bundle 中。示例:

1
2
3
4
Bundle args = new Bundle();
args.putSerializable(EXTRA_MY_OBJECT, myObject);
args.putInt(EXTRA_MY_INT, myInt);
args.putCharSequence(EXTRA_MY_STRING, myString);

2. 附加 argument 给 fragment

附加 argument bundle 给 fragment ,需调用 Fragment.setArguments(Bundle) 方法。创建和设置 fragment argument 通常是通过添加名为 newInstance() 的静态方法给 Fragment 类完成的。使用该方法,完成 fragment 实例及 bundle 对象的创建,然后将 argument 放入 bundle 中,最后再附加给 fragment 。

托管 activity 需要 fragment 实例时,需调用 newInstance() 方法,而非调用其构造方法。而且,为满足 fragment 创建 argument 的要求, activity 可传入任何需要的参数给 newInstance() 方法。

示例:

在 fragment 中,编写可以接受 UUID 参数的 newInstance(UUID) 方法,通过该方法,完成 arguments bundle 以及 fragment 实例的创建,最后附加 argument 给 fragment 。

1
2
3
4
5
6
7
8
9
public static CrimeFragment newInstance(UUID crimeId) {
Bundle args = new Bundle();
args.putSerializable(EXTRA_CRIME_ID, crimeId);
CrimeFragment fragment = new CrimeFragment();
fragment.setArguments(args);
return fragment;
}

然后修改托管该 fragment 的 activity 的 createFragment() 方法,调用 fragment 的 newInstance(UUID) 方法,并传入从它的 extra 中获取的 UUID 参数值:

1
2
3
4
5
6
7
8
public class CrimeActivity extends SingleFragmentActivity {
@Override
protected Fragment createFragment() {
UUID crimeId = (UUID)getIntent().getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID);
return CrimeFragment.newInstance(crimeId);
}
}

之后修改 fragment 的 onCreate(...) 方法,从 argument 中获取 UUID :

1
2
3
4
5
6
7
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UUID crimeId = (UUID)getArguments().getSerializable(EXTRA_CRIME_ID);
mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
}

fragment 的保留

有时我们希望保留 fragment 以免当设备运行中发生配置变更(如设备旋转)时被释放,导致一些任务的中断。Fragment 具有和 Activity 相同功能的 onSaveInstanceState(Bundle) 方法。然而这种方案不适合用在音乐等任务,因为这仍然无法避免音乐的中断。

幸运的是,为应对设备配置的变化,可使用 fragment 的一个特殊方法 setRetainInstance(true) 来保留 fragment ,示例:

1
2
3
4
5
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}

fragment 的 retainInstance 属性值默认为 false。这表明其不会被保留。因此,设备旋转时 fragment 会随托管 activity 一起销毁并重建。调用 setRetainInstance(true) 方法可保留 fragment。已保留的 fragment 不会随 activity 一起被销毁。相反,它会被一直保留并在需要时原封 不动的传递给新的 activity。

fragment 必须同时满足两个条件才能进入保留状态:

  • 已调用了 fragment 的 setRetainInstance(true) 方法
  • 因设备配置改变(通常为设备旋转),托管 activity 正在被销毁

Fragment 处于保留状态的时间非常短暂,即 fragment 脱离旧 activity 到重新附加给立即创建的新 activity 之间的一段时间。

Comments