0.说明
本文内容主要来源于Android官方文档,API Guide中Fragment部分的翻译,加入一些自己的理解和实践。不敢说详解,因为大家都知道怎么用。小结是写给自己的。
1.简介
一个Fragment的表现是一个Activity的一种行为或者UI的一部分。你可以把多个Fragment组合在一个activity里创建一个多格UI,也可以在多个activity里复用一个Fragment。你可以把Fragment想象成activity的一个子模块,它有自己的生命周期,接收它自己的输入事件,你也可以在activity运行时添加或者移除它(类似于一个“子activity”你可以在不同activity中复用)。
一个Fragment必须嵌入到一个activity中(不能独立存在)而且它的生命周期直接依赖于宿主activity的生命周期。举例来说,当activity暂停态时,所有内部Fragment也处于暂停态,当activity销毁时,所有内部Fragment也被销毁。但是,当一个activity运行时(处于生命周期的resume态,即完全的前台运行),你可以独立操作每一个Fragment,比如添加或者移除他们。当你执行了一个这样的Fragment事务时(添加或移除或类似的操作),你也可以把他添加到宿主activity管理的回退栈(back stack)–每一个activity回退栈的条目都是一个Fragment事务发生的记录。回退栈允许用户回退一个Fragment事务(向后导航),通过按back键。(手机上的back键是回退、返回的功能,所以回退栈应该是back键对应的一个栈,按back键后可以返回栈内的内容,一般是一系列activity或者Fragment,把Fragment加入回退栈,可以避免打开activity、回退activity的开销,因为Fragment的开销要比activity小)
当你添加一个Fragment作为你的activity布局的一部分时,它会存在于activity视图层次中的一个viewgroup中,而且它有自己的视图布局。你可以在activity的布局文件中添加节点< fragment>来添加一个Fragment,或者在应用的代码中把它添加到一个已有的ViewGroup中。不过,一个Fragment并不是必须作为activity布局的一部分,你也可以把一个Fragment作为没有界面的功能模块。
2.设计原则
Android是在Android3.0(API level11)中加入Fragment,主要是支持越来越多的动态和灵活的大屏设备的UI设计,比如平板电脑。因为一个平板设备的屏幕远大于手持设备,也就拥有更多空间组合和交互UI组件。Fragment支持这种设计而不需要你管理复杂的视图层次切换。通过把一个activity布局分割成若干个Fragment,你可以修改activity的运行时UI并把这些改变提交到activity的回退栈(通过回退栈可以返回前一个运行时UI)。
比如,一个新闻应用可以在左边用一个Fragment展示文章列表,在右边用一个Fragment展示文章内容——两个Fragment同时显示在一个activity中,每个Fragment拥有自己的生命周期回调方法,处理自己的输入事件。因此,作为替代用一个activity选择文章、用另一个activity显示文章,用户可以在一个activity中选择、显示文章,如下图中第一部分:
你应该把每个Fragment设计为模块化和可复用的activity组件。因为每个Fragment定义了自己的布局和自己生命周期回调的行为,你可以在多个activity中包含同一个Fragment,所以你应该设计为可复用并且避免直接用一个Fragment操作另一个Fragment。这一点非常重要,因为一个模块化的Fragment允许你依据不同屏幕分辨率改变Fragment的组合。当设计你的应用同时支持平板和手持设备,你可以在不同屏幕可视空间中,在不同的布局配置中复用你的Fragment,以优化用户的使用体验。比如,在手持设备上,可能有必要分离组合的Fragment而提供单面板UI——当activity无法显示一个以上Fragment的时候。
举例来说——继续使用新闻应用的例子——该应用在平板大小的设备中运行时,在activityA嵌入两个Fragment。但是,在手持设备的屏幕中,没有足够的空间来显示两个Fragment,所以activityA仅包含了一个文章列表的Fragment,当用户选择一篇文章时,启动activityB,它包含了第二个用于显示文章内容的Fragment。因此,这个应用通过在不同组合中复用Fragment,可以同时支持平板和手持设备,如上图所示。
3.创建一个Fragment
Fragment生命周期图:
创建一个Fragment,你必须新建一个Fragment类的子类(或者使用已有的子类)。Fragment类的编码和activity非常相似。它所包含的回调方法也和activity非常像,比如二者都有onCreate(),onStart(),onPause()和onStop()。事实上,你可以把一个已有的Android应用转换成使用Fragment的应用,你只需要简单的把activity回调方法中的代码移动到你的Fragment中。
通常情况下,你至少需要实现如下回调方法:
onCreate():当创建Fragment时由系统调用。在你的实现中,你需要初始化Fragment的必要组件,这些组件会在Fragment暂停(paused)或停止(stopped)时也保存,然后运行(resumed)时重启。
onCreateView():当Fragment第一次需要绘制他的UI时由系统调用。当绘制Fragment的UI时,你必须从这个方法返回一个View,这是你的Fragment布局的根节点。你也可以返回null表示这是一个没有UI的Fragment。
onPause():当用户离开Fragment时由系统调用(这不代表Fragment正在被销毁)(可见不可用时、转为后台不可见时都会pause)。在这里,你通常需要做持久化的工作以保存任何不依赖特定用户的修改(因为用户可能不会再回来)。
大部分应用的Fragment至少应该实现如上三个回调方法,不过,Fragment生命周期的其他回调方法你应该也会用到。
这里有几个子类你可以继承,用来替代Fragment基类:
DialogFragment:显示一个悬浮的窗口。用这个类创建一个对话框是使用activity类中对话框相关方法的非常好的备选方案,因为你可以在activity的Fragment回退栈里放入一个Fragment对话框,允许用户返回到已不见的Fragment。(Fragment可以插入到回退栈中,这是activity做不到的(?),所以用Fragment的对话框可以随时通过回退来显示,而activity需要start、finish等)。
ListFragment:显示通过某个adapter生成的一列item,类似于一个ListActivity。它提供了若干方法管理listview,比如onListItemClick()处理click事件。
PreferenceFragment:显示Preference对象层级列表,类似于PreferenceActivity。当创建一个“setting”Activity时很有用。
4.添加UI界面
Fragment通常用来作为Activity的UI界面的一部分并为Activity提供自己的布局。为Fragment提供布局,你必须实现onCreateView()回调方法,这是当Fragment绘制自己的布局时的系统调用。你实现的该方法必须返回一个View作为你的Fragment布局的根节点。
注意:如果你的Fragment是ListFragment的子类,onCreateView()默认实现是返回一个ListView,所以你不需要再去实现它。
为了从onCreateView()返回一个布局,你可以从一个XML布局资源中载入(inflate)它。为了帮助你这么做,onCreateView()提供了一个LayoutInflate对象的形参。
举例,下面是一个从example_fragment.xml文件装载布局的Fragment的子类:
public static class ExampleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
传入的container参数是你的Fragment插入的父ViewGroup(从当前Activity而来)。savedInstanceState参数是一个Bundle对象,提供了有关上一个Fragment实例的数据,如果当前Fragment是重启的(resumed)。
inflate()方法有三个参数:
①你想要加载的布局的资源ID
②设定被加载布局的父ViewGroup。这里传入container参数是非常重要的,可以使系统把布局参数应用到被加载布局的根视图,参数由它所嵌入的父视图指定。
③一个布尔值,表明在被加载布局的加载过程中是否关联(attach)到ViewGroup(即②)。(在上面的例子中,传入了false是因为系统已经准备好把被加载布局直接插入到container——传入true会在最终布局中再创建一个冗余的ViewGroup)。(在inflate()方法的参数说明中,关于这里说的更清楚,意思是如果false,则root只是用来创建LayoutParams的子类为root视图在xml中,在我的测试工程里面这个参数是null,如果是true,则root就是根视图,如果是false,则它是被加载xml文件的根节点)
5.为Activity添加Fragment
通常,一个Fragment提供了宿主Activity的UI界面的一部分,而且嵌入为Activity的视图层次的一部分。有两种方式你可以添加一个Fragment到一个Activity布局:
(1)在Activity的布局文件中声明Fragment
这种方式下,你可以指定为Fragment指定布局属性就像一个普通视图那样。下面是一个有两个Fragment的Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.example.news.ArticleListFragment"
android:id="@+id/list"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<fragment android:name="com.example.news.ArticleReaderFragment"
android:id="@+id/viewer"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
< fragment>中android:name参数指定了布局关联的Fragment类。
当系统创建这个Activity布局时,它会实例化布局中设定的每个Fragment并且调用每个onCreateView(),以得到每个Fragment的布局。系统会把返回的View直接插入到< fragment>元素所在的位置。
注意:每个fragment需要一个ID,系统可以用它恢复fragment如果Activity重启(restarted)的话(你也可以用来获取fragment并执行事务,比如移除它)。有三种方法可以为一个fragment指定ID:
①为android:id参数赋值(唯一的ID)
②为android:tag参数赋值(唯一的字符串)
③如果以上两种方法你都没提供,系统就使用container视图的ID
(2)在代码中把fragment添加到指定的ViewGroup
有时候在你的Activity运行时,你可以添加fragment到你的Activity布局。你只需要指定一个需要嵌入fragment的ViewGroup。
为了在你的Activity中使用fragment事务(如添加,移除或者更换一个fragment),你必须使用FragmentTransaction中的API。你可以按如下方法在Activity中获取一个FragmentTransaction实例:
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
然后你可以用add()方法添加一个fragment,指定fragment被添加到哪个view中,如:
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
add()方法的第一个参数是一个ViewGroup,代表了fragment要添加的地方,通过ID指定;第二个参数是要添加的fragment的实例。
一旦你用FragmentTransaction做出修改,你都需要调用commit()方法使修改生效。
6.添加一个无UI的fragment
上面的示例演示了如何往Activity中添加有UI的fragment。但是,你也可以用一个fragment为Activity提供后台行为而不需要UI界面。
为了添加一个无UI的fragment,要用add(Fragment,String)方法在Activity中添加一个fragment(提供一个唯一的“tag”而不是一个viewID)。这会添加一个fragment,但是,因为它没有关联Activity布局中任何一个view,它不会收到onCreateView()回调。所以你不需要实现这个方法。
为fragment提供一个字符串tag并不只用在无UI的fragment——你也可以为有UI的fragment提供一个字符串tag——但是如果fragment没有UI,则字符串tag是唯一指定它的方法。如果你想在Activity获取该fragment,你需要使用findFragmentByTag()。
7.管理fragment
你需要用FragmentManager实例管理fragment。你可以在Activity中调用getFragmentManager()获取该实例。
下面是你可以在FragmentManager中做的事情:
①获取当前Activity中存在的fragment,通过findFragmentById()(在Activity布局中提供了fragment的UI)或者通过findFragmentByTag()(不论fragment是否有UI)
②从回退栈弹出fragment,通过popBackStack()(模拟用户发出Back命令)
③为回退栈注册一个监听器,通过addOnBackStackChangedListener()
8.执行fragment事务(Fragment Transactions)
在你的Activity中使用fragment的一个很棒的特性是,你可以添加、移除、替换或者其他操作,响应用户的交互。每一个你提交给Activity的修改设置都称为一个事务(Transactions),然后你可以执行FragmentTransaction类中的API。你也可以把事务保存到Activity的回退栈中,使得用户可以导航回退fragment的改变(类似于导航回退Activity)。
每一个事务都是你想同时执行的一些修改的配置。你可以给一个指定的事务创建所有你想执行的修改,通过使用add(),remove(),或者replace()。然后,让Activity应用这个事务,你需要调用commit()。
在调用commit()之前,你可以还想调用addToBackStack(),为了把事务添加到fragment事务回退栈中。这个回退栈由Activity管理,允许用户返回之前的fragment的状态,通过按下Back按钮。
下面的例子演示了你如何替换一个fragment,并把它的状态保存到回退栈中:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
此例中,newFragment替换了布局容器中ID为 R.id.fragment_container 的所有fragment(如果有的话)。通过调用addToBackStack(),这个替换事务被保存到了回退栈中,所以用户可以回退该事务并且返回之前的fragment(被替换的fragment)通过按Back键。
如果你为事务添加了多个改变(比如调用了别的add()或remove())然后调用了addToBackStack(),这时在你调用commit()前所有的修改已经被允许并被当做单个事务添加到了回退栈中,然后Back键可以把他们一起回退。
你向FragmentTransaction添加修改的顺序无所谓,除了:
①你必须最后调用commit()
②如果你添加多个fragment到同一个容器,那么你添加他们的顺序确定了他们在视图层次中出现的顺序。
如果你没有调用addToBackStack(),当你执行一个移除fragment的事务,当事务提交的时候那个fragment会被销毁,而用户不能再导航回退它。反之,如果你调用了addToBackStack(),当你移除一个fragment的时候,那个fragment只是停止(stopped)而且将来可以被用户导航回退恢复(resumed)。
小贴士:对于每一个fragment事务,你提交事务前调用setTransition()可以执行一系列事务。
调用commit()并不会立即执行事务。换句话说,它只是安排Activity的UI线程(主线程)尽可能快的可以执行事务。如果有必要,你也可以调用executePendingTransactions()在你的UI线程中立即执行commit()提交的事务。通常不需要这么做,除非事务依赖于别的线程中的任务。
警告:你只能在Activity保存他的状态(当用户离开Activity的时候)前用commit()提交事务。如果你试图在那个时间点后提交,将会抛出一个异常。这是因为事务提交后的状态可能因为Activity的恢复(restored)而丢失。如果想在丢失提交也能正常运行,使用commitAllowingStateLoss()。
9.和Activity通信
虽然Fragment类的实现更像一个相对Activity类独立的对象,而且可以在多个activity中使用,一个Fragment实例只会和容纳他的activity绑定。
特别是,这个Fragment实例可以通过getActitivy()方法访问Activity实例,而且可以很容易执行类似于查找一个Activity布局中的组件这样的任务:
View listView = getActivity().findViewById(R.id.list);//Fragment中的代码
同样的,你的Activity可以通过FragmentManager类的findFragmentById()或findFragmentByTag()方法获取Fragment类的引用,然后调用它的方法,比如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
给Activity创建事件回调
有些情况下,你可能需要Fragment和Activity共享事件。一个好的实现方法是,在Fragment中定义一个回调接口并要求宿主Activity实现它。当Activity通过接口接收到一个回调时,可以和布局中其他Fragment共享信息(如果有必要的话)。
比如,上面的新闻应用。Fragment A必须在某个列表项被选择时告诉宿主Activity,以使Fragment B显示对应的文章。本例中,OnArticleSelectedListener接口定义在Fragment A中:
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
如果宿主Activity实现了这个接口并重写(override)了onArticleSelected方法,它可以把FragmentA传来的事件通知给FragmentB。为了确认Activity实现了该接口,FragmentA的onAttach()方法(当添加Fragment到Activity时系统调用)把传入的Activity形参强制类型转换为一个OnArticleSelectedListener实例:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
...
}
如果Activity没有实现该接口,Fragment会抛出一个ClassCastException异常。当成功的时候,mListener成员变量保存了一个实现接口的Activity对象引用,所以Fragment可以通过调用接口中定义的方法向Activity共享事件。举例来说,如果FragmentA是一个ListFragment的子类,每次用户点击list项,系统都会调用FragmentA中的onListItemClick()方法,这时FragmentA调用onArticleSelected()方法和Activity共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// Append the clicked item's row ID with the content provider Uri
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
// Send the event and Uri to the host activity
mListener.onArticleSelected(noteUri);
}
...
}
传入onListItemClick() 的id参数是被点击的item的行ID,Activity(或其他Fragment)用它从应用的ContentProvider中获取文章。
10.处理Fragment的生命周期
管理Fragment的生命周期和管理Activity的生命周期很相似。如同Activity,一个Fragment可以有三个如下必然存在的状态:
①resumed:Fragment在Activity中可见
②paused:另一个Activity在前台显示而且拥有焦点,但是Fragment嵌入的Activity仍然是可见的(前台Activity部分透明或者没有遮挡整个屏幕)
③stopped:当前Fragment不可见。不论宿主Activity被停止或者Fragment被移除但是被添加到回退栈。一个停止态的Fragment仍然是活着的(所有状态和成员信息仍然被系统保存)。但是,它不再对用户可见而且如果Activity被干掉则它也会被干掉。
如果Activity,你也可以用Bundle保存一个Fragment的状态,假如Activity的进程被杀死而你在Activity recreated的时候restore Fragment的状态。你可以在Fragment的onSaveInstanceState()回调过程中保存状态,在onCreate(),onCreateView(),onActivityCreated()过程中恢复。
大部分两者生命周期的不同,在于它在自己的回退栈中是如何保存的。默认情况下,当一个Activity停止态时,系统会管理Activity的回退栈并把它压入栈(所以用户可以通过按Back键回退到它)。但是,一个Fragment要求只有在其实例明确调用addToBackStack()的情况下才会被压入宿主Activity管理的回
退栈,发生在移除Fragment的事务过程中。
其他情况下,管理二者的生命周期是很相似的。所以,管理Activity生命周期的经验也可以应用到Fragment中。当然,你同时需要理解,Activity的生命周期是如何影响Fragment的生命周期的。
警告:如果你需要一个Context对象在你的Fragment类中,你可以调用getActivity()。但是,要注意只有当Fragment关联到一个Activity时才能调用。当Fragment没有关联,或者在生命周期的最后解除了关联,则返回null。
11.和Activity的生命周期联动
Activity的生命周期会直接影响嵌入之前的Fragment的生命周期,所以对每个Activity的生命周期的回调都导致Fragment发送相似的回调。举例来说,当Activity收到onPause(),每个其中的Fragment也会收到onPause()。
Fragment有若干额外的生命周期回调,然而,这些独特的和Activity的交互的处理,都是为了执行如Fragment的UI创建或销毁的动作。这些附加的回调方法有:
onAttach():当Fragment和Activity关联时调用(Activity对象在这里传入)
onCreateView():当创建Fragment的视图层次时调用
onActivityCreated():当宿主Activity的onCreate()方法返回时调用
onDestroyView():当Fragment的视图层次被移除时调用
onDetach():当断开Fragment和Activity关联时调用
Fragment生命周期的流动,就如它的宿主Activity影响的那样,都在生命周期图中说明了。在图中,你可以看到每一个Activity的连续状态如何确定了Fragment可能接收到的回调方法。比如,当Activity接收到onCreate()回调时,Fragment只会接收到onActivityCreated()回调。
一旦Activity到了resumed态(显示态),你可以自由的添加移除Fragment。因此,只有在Activity处于resumed态时,Fragment生命周期的改变是独立的。
但是,只要Activity离开resumed态,Fragment的生命周期会再次被Activity推动。
12.一点想法
fragment和ViewGroup很像,二者都不显示,都在自己内部显示组件。
< fragment>元素其实只是留了一个空位,具体的内容会在编译或运行时被加载的布局替代,它可以为加载的布局生成一个ViewGroup根节点,也可以不生成。
Fragment具有动态切换布局的能力,因此可以让Activity实现部分刷新,而不是全部刷新,节约了很多资源。
Fragment向Activity发送消息,需要通过内部定义的接口,该接口被宿主Activity实现。
前文没有提到action bar,官方文档中说Fragment可以包含一个menu,这个menu可以用在action bar的option menu上。
Activity的打开很销毁资源,关闭也是,其中内容的保存,和其他Activity的交互,方式复杂,代价很大,比如一个ActivityA用于下载,一个ActivityB用于获取列表,二者的切换需要startActivity,每次想回到之前的状态,都很麻烦,而如果换成FragmentA和FragmentB,则一个Fragment隐藏另一个显示时,只需要把切换的事务压入栈,听过入栈和回退可以在两个Fragment之间切换,资源消耗更小,状态保存也比较简单。最终可能只有一个mainActivity,其余全是Fragment。
如果Fragment子类是作为内部类,则必须是static public inner class,因为对于非static内部类实例,Activity需要一直持有对它的引用,导致GC无法回收Activity,造成内存泄露。比如屏幕方向改变时需要销毁当前Activity并新建Activity,而Fragment实例可能仍然存活而且被插入新Activity中,则被销毁的Activity因为持有Fragment实例的引用而未被GC回收。另外,非静态Fragment如果没有空构造函数,第一无法在插入到xml文件中使用,第二FragmentManager有时候会调用空构造函数,导致其无法被复用。