fragment 是一种控制器对象, activity 可委派它完成一些任务。通常这些任务就是管理用户界面。受管的用户界面可以是一整屏或整屏的一部分。
管理用户界面的 fragment 又称为 UI fragment 。它也有自己产生布局文件的视图。fragment 视图包含了用户可以交互的可视化 UI 元素。
fragment 可以托管在 activity 中。如果有多个 fragment 要插入, activity 视图也可提供多个位置。因此,可联合使用 fragment 及 activity 来组装或重新组装用户界面。在整个生命周期过程中,技术上来说 activity 的视图可保持不变。
用 UI fragment 将应用的 UI 分解成构建块,利用一个个构建块,很容易做到构建分页界面、动画侧边栏界面等更多其他定制界面。
fragment 与支持库
Fragment 是在 API 11 之后被引入的。幸运的是,对于 fragment 来说,保证向后兼容比较容易,仅需使用 Android 支持库中的 fragment 相关类即可。
API 11 之前的支持:使用位于 libs/android-support-v4.jar 内的支持库。支持库包含了 Fragment 类(android.support.v4.app.Fragment),该类可以使用在任何 API 4 级及更高版本的设备上。支持库中的类不仅可以在无原生类的旧版本设备上使用,而且可以代替原生类在新版本设备上使用。
API 11 之后的支持:可以直接使用标准库中的原生 fragment 类。
fragment 的生命周期
下图展示了 fragment 的生命周期,与 activity 的生命周期类似。与 activity 生命周期的一个关键区别在于,fragment 的生命周期方法是由托管在 activity 而不是操作系统调用的。操作系统无从知晓 activity 用来管理视图的 fragment 。fragment 的使用是 activity 自己内部的事情。
fragment 的生命周期方法由 activity 负责调用。
基于 Fragment 的应用开发流程
基本的开发步骤:
定义 fragment 的布局;
创建 fragment 类;
修改 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; } }
以上代码有几点值得注意的地方:
Fragment.onCreate(Bundle)
方法及其他 Fragment 生命周期方法必须设计为公共方法(方便被托管 fragment 的任何 activity 调用);
类似于 activity, fragment 同样具有保存及获取状态的 bundle 。我们也可以根据需要覆盖 Fragment.onSaveInstanceState(Bundle)
方法;
在 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 有两种方式:
布局方式托管 fragment;
代码方式托管 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 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 fm = getSupportFragmentManager(); Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); 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 之间的信息传递方式。
简单的方式是直接通过 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); 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 之间的一段时间。