一个Fragment代表一个Activity的一种行为或者用户界面的一部分。你可以组合多个fragment到一个activity来创建一个多面板的UI,或者复用一个fragment到多个activity中。你可以认为fragment就是activity的一个模块,它有自己的生命周期,接收直接的输入事件,在activity运行中可以随意添加和删除。
fragment在Android3.0中被加入,只要是为了支持平板等大屏幕设备,让你可以在activity运行中动态和灵活的操作界面的元素,而且可以把这些操作记录到后退堆栈中,方便管理。
为了创建fragment,你需要继承Fragment类,Fragment类的代码和Activity很相似,也有onCreate(), onStart(), onPause(), onStop()函数。
要管理你的fragment需要使用FragmentManager,在activity中调用getFragmentManager()获得。
fragment能在activity中被添加,删除,替换等,这些操作都叫做事务,这些事务通过FragmentTransaction来处理。你也可以保存每个事务到activity管理的后退堆栈中,让用户可以返回以前的状态。
fragment可以通过getActivity()来取得Activity布局中的视图:
下面是一个包含两个不同fragment的实例,一个显示莎士比亚剧集标题,一个显示对应剧集介绍。
一个fragment通常需要嵌入到一个activity中,这个activity的生命周期也直接影响fragment的生命周期。例如,activity暂停时,包含在其中的fragment也暂停,activity被销毁时,fragment也被销毁。不过,当activity在运行中时,你可以单独的操作任何一个fragment,比如添加和删除。当你执行这些fragment修改时,你也可以添加这些fragment到一个activity管理的后退堆栈中。这个堆栈记录了fragment的改变,让用户可以恢复到改变前的状态。
当你添加fragment到activity的布局中时,fragment被放在ViewGroup中,fragment也有自己的视图布局。你可以再布局文件中使用<fragment>元素来添加一个fragment,也可以在代码中添加到一个已存在的ViewGroup中。一个fragment可以是界面的一部分,也可以隐藏着为activity工作。
设计原理
fragment在Android3.0中被加入,只要是为了支持平板等大屏幕设备,让你可以在activity运行中动态和灵活的操作界面的元素,而且可以把这些操作记录到后退堆栈中,方便管理。
例如,你可以使用一个fragment在屏幕左边显示一个文章列表,用另外一个fragment在右边显示选中状态下的文章内容,这些可以在一个activity中完成。
你应该定义每个fragment为一个模块化和可重用的activity组件。因为每个fragment都定义有自己的布局,行为,生命周期,你可以包含一个fragment到多个activity中,所以你应该避免使用一个fragment直接去操作另外一个fragment。另外一个应该注意的是,模块化的fragment可以让你在不同的尺寸在改变fragment的组合。你可以基于你的屏幕大小来优化用户体验,例如,在手机上,你可能需要一次只显示一个fragment。
根据上图显示,在大屏幕下,一个Activity可以包含两个fragment,而在手机上没有足够的空间,一个Activity只能包含一个fragment,当用户选择第一个fragment时才启动包含另外一个fragment的activity。
创建一个Fragment
为了创建fragment,你需要继承Fragment类,Fragment类的代码和Activity很相似,也有onCreate(), onStart(), onPause(), onStop()函数。
通常情况下,你至少需要实现下面这些声明回调函数:
onCreate()
onCreateView()创建fragment时调用,你应该在这个函数中初始化一些基本组件,这些组件是fragment暂停,或者停止时你还希望能保持的。
onPause()在fragment第一次绘制的时候调用,为了绘制fragment的界面,你需要在这个方法中返回一个view,如果不提供界面的话就可以返回null。
用户离开fragment时调用,离开不代表被销毁。通常在这个方法中保存那些需要持久保存的数据,因为用户可以不会再回到这个界面来了。
大多数程序都需要实现上面三个方法,不过fragment还有一些其他的生命周期方法(见下图),我们在下面的章节介绍。
下面有一些fragment的子类你可能想要扩展:
DialogFragment
ListFragment显示一个浮动对话框。
PreferenceFragment显示一个列表,列表的元素可以被一个适配器管理,类似一个ListActivity。
添加一个用户界面像list一样显示一个Preference对象层。类似PreferenceActivity,通常用来为程序创建一个“设置”功能的activity。
一个fragment通常是activity界面的一部分,不过它也有自己的布局。
你需要实现onCreateView()函数来提供一个fragment的布局,然后返回一个fragment根布局。
提示:如果你的fragment是一个ListFragment,onCreateView()默认返回一个ListView,那么你就不需要实现它。
为了从onCreateView()中返回一个布局,你可以从布局资源文件中取得布局,而且onCreateView()还提供了一个LayoutInflater对象来帮助你。
下面的例子是从example_fragment.xml文件中取得一个布局并返回:
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参数传递了一个父类ViewGroup,让你的fragment布局添加到里面。savedInstanceState参数是这个fragment先前的实例留下的数据,可以让你恢复它以前的状态。
inflate()提供了三个参数:
- 布局资源ID
- 包含这个fragment的父类容器,一个ViewGroup。
- 一个布尔值,指定了布局是否附加到第二个参数的ViewGroup中,上面的例子设置为false,因为系统已经把布局插入到了容器中,如果为true的话,会创建一个view group来包含fragment布局。
添加一个fragment到activity
有两种添加fragment的方法:
1. 在activity布局文件中声明fragment。
你可以在XML文件直接定义fragment:
<?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都要有一个唯一的标示符,让系统在重启activity时恢复fragment。下面有三个方法为fragment提供一个ID:
- 提供android:id属性
- 提供android:tag属性
- 每个都没提供,那么系统会使用容器View的ID
2. 编程的方式添加fragment到一个已存在的ViewGroup。
在activity运作中的任何时候,你都可以添加一个fragment到activity布局中,你只需简单的指定一个用来保护fragment的ViewGroup。
为了在activity中改变fragment,你需要使用FragmentTranslation类:
FragmentManager fragmentManager =getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
;
下面执行一个添加操作:
ExampleFragment fragment = new ExampleFragment(); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit();
add()的第一个参数用来填充fragment的ViewGroup,第二个参数就是fragment了。
要让修改完成你必须调用commit()方法。
添加一个没有UI的fragment
你可以使用add(Fragment, string)来添加一个没有UI的fragment到activity中(第二个参数string为fragment提供一个唯一的tag,而不是ID)。由于这个fragment和布局无关,所以不用实现onCrateView()方法。
tag字符串在可视的fragment也可以使用,不过没有UI的fragment一定要有tag,activity需要使用findFragmentByTag()来使用fragment。
管理Fragments
要管理你的fragment需要使用FragmentManager,在activity中调用getFragmentManager()获得。
你可以通过FragmentManager做下面这个事:
- 使用findFragmentById()或者findFragmentByTag()取得已经存在的fragment。
- 使用popBackStack()把fragment从堆栈中取出。
- 使用addOnBackStackChangedListener()注册堆栈的改变的监听。
执行Fragment事务
fragment能在activity中被添加,删除,替换等,这些操作都叫做事务,这些事务通过FragmentTransaction来处理。你也可以保存每个事务到activity管理的后退堆栈中,让用户可以返回以前的状态。
你可以从FragmentManager获得一个FragmentTransaction实例:
FragmentManager fragmentManager =getFragmentManager()
; FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
;
每个事务是一些你想同时改变的集合,意思就是你可以使用add(), remove(), replace()等方法组成一组操作集,然后通过commit()方法同时提交并执行。
在调用commit()之前,你可以调用addToBackStack()来添加事务到后退堆栈中,用户在按back键的时候能够恢复先前的fragment状态。
下面的例子是替换一个fragment,然后保存先前的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();
如果你添加有多个改变的事务到后退堆栈中,那么恢复时候也是恢复所有的操作。
添加到FragmentTransaction的操作顺序并不重要,重要的是:
- 你必须在最后调用commit()。
- 如果你添加多个fragment到相同的容器中,那么添加的顺序指定了显示的顺序。
如果删除fragment时不调用addToBackStack(),那么提交后这个fragment会被销毁,如果调用了addToBackStack(),那么这个fragment只是被停止。
提示:每个fragment事务都可以添加一个事务动画,在提交前调用setTransition()实现。
调用commit()后不会立即执行事务,会在activity的UI线性允许的时候执行,如果必要的话,你也可以在UI线程中调用executePendingTransactions()来立即执行。
警告:commit()只能在activity保存状态前执行,不然会抛出异常。如果在保存状态后执行,在activity恢复状态时,提交的事务将会丢失。如果你不建议丢失事务,你可以使用commitAllowingStateLoss()代替commit()。
与Activity通信
fragment可以通过getActivity()来取得Activity布局中的视图:
View listView =getActivity()
.findViewById
(R.id.list);
而activity可以通过FragmentManager的方法findFragmentById()或者findFragmentByTag()来取得fragment:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
给activity创建事件回调
在一些情况下,你可能需要fragment分享事件给activity。实现这个的好办法就是在fragment中定义一个回调接口,然后在activity中实现这个接口。
例如,如果一个程序中有两个fragment,一个用来显示文章列表(fragment A),一个用来显示文件内容(fragment B),那么A在用户点击列表时需要告诉Activity那个文章被选中,然后B显示相应的文章内容。我们现在在A中声明一个OnArticleSelectedListener接口:
public static class FragmentA extends ListFragment { ... // Container Activity must implement this interface public interface OnArticleSelectedListener { public void onArticleSelected(Uri articleUri); } ... }
为了保证activity必须实现这个接口,fragment的onAttach()(当activity添加一个fragment时会调用这个方法)回调函数需要实例化一个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没有实现接口,那么就会抛出ClassCastException的异常。如果成功实现,那么mListener成员对象就持有一个实现了OnArticleSelectedListener的activity的引用。那么fragment A就可以使用mListener来调用activity中实现的onArticleSelected()方法:
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);
}
...
}
id参数就是选项的行ID,activity使用这个id从ContentProvider中查询文章内容。
添加菜单项到Action Bar
你的fragment可以分配菜单项到activity的Options Menu(现在使用Action Bar),通过onCreateOptionsMenu()实现。
你也可以在fragment布局中注册一个view来提供context menu。
提示:activity会先接受到menu item选择的回调,如果activity不处理,才会传递到fragment中。
处理fragment生命周期
fragment和activity一样存在3个状态:
Resumed
Paused在运行中的activity中可见。
Stopped其他activity在前台,不过包含fragment的activity依然可见。
和activity一样,你也可以使用Bundle保存fragment的状态,可以在onSaveInstanceState()中保存状态,在onCreate(), onCreateView(), 或者onActivityCreated()中恢复状态。fragment不可见。activity被停止时,或者fragment被移除但是被添加到了后退堆栈时。这个状态下,会随着activity被杀掉而被杀掉。
和activity不同的是activity的后退堆栈由系统管理,而fragment的堆栈由宿主activity管理。而且需要明确调用addToBackStack()才能记录事务。
警告:如果你需要一个Context对象,那么调用getActivity()取得,前提是fragment已经附加到activity中,不然返回null。
和activity生命周期协调工作
activity的生命周期直接影响fragment的生命周期,例如,当activity的onPause()被调用时,fragment对应的onPause()也被调用。
fragment有一些额外的生命周期回调:
onAttach()
onCreateView()fragment已经和activity有关联时调用。
onActivityCreated()调用它创建和fragment有关的view层。
onDestroyView()activity的onCreate()调用返回时执行。
onDetach()和view层关联的fragment被移除时调用。
fragment与activity失去关联时调用。
一旦activity处于resumed状态,你就可以随意添加和删除fragment了。当activity离开resumed状态时,你就可以随着上图周期对应的方法执行下去。
实例
下面是一个包含两个不同fragment的实例,一个显示莎士比亚剧集标题,一个显示对应剧集介绍。
在主activity应用一个布局:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_layout); }
布局文件是:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment" android:id="@+id/titles" android:layout_weight="1" android:layout_width="0px" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/details" android:layout_weight="1" android:layout_width="0px" android:layout_height="match_parent" android:background="?android:attr/detailsElementBackground" /> </LinearLayout>
使用这个布局,系统实例化TitlesFragment在activity加载布局时,刚开始的FrameLayout是空的,直到列表被选择。
不是所有的屏幕都能显示两个fragment,只有在横屏的时候才使用这个布局,所以保持这个文件在res/layout-land/fragment_layout.xml中。
当屏幕是竖屏时应用保存在下面文件中的布局:res/layout/fragment_layout.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment" android:id="@+id/titles" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
这个布局只包含TitlesFragment。意思就是说,竖屏时只显示剧集列表。当用户点击列表时,程序要启动一个新的activity来显示剧集介绍。
下面我先来看TitlesFragment是怎么实现的,这个fragment继承ListFragment来处理列表的工作。
当用户点击列表时,依据是否有两个布局来判断是创建一个新的fragment,还是开启一个新的activity。
public static class TitlesFragment extends ListFragment { boolean mDualPane; int mCurCheckPosition = 0; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Populate list with our static array of titles. setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES)); // Check to see if we have a frame in which to embed the details // fragment directly in the containing UI. View detailsFrame = getActivity().findViewById(R.id.details); mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE; if (savedInstanceState != null) { // Restore last state for checked position. mCurCheckPosition = savedInstanceState.getInt("curChoice", 0); } if (mDualPane) { // In dual-pane mode, the list view highlights the selected item. getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); // Make sure our UI is in the correct state. showDetails(mCurCheckPosition); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("curChoice", mCurCheckPosition); } @Override public void onListItemClick(ListView l, View v, int position, long id) { showDetails(position); } /** * Helper function to show the details of a selected item, either by * displaying a fragment in-place in the current UI, or starting a * whole new activity in which it is displayed. */ void showDetails(int index) { mCurCheckPosition = index; if (mDualPane) { // We can display everything in-place with fragments, so update // the list to highlight the selected item and show the data. getListView().setItemChecked(index, true); // Check what fragment is currently shown, replace if needed. DetailsFragment details = (DetailsFragment) getFragmentManager().findFragmentById(R.id.details); if (details == null || details.getShownIndex() != index) { // Make new fragment to show this selection. details = DetailsFragment.newInstance(index); // Execute a transaction, replacing any existing fragment // with this one inside the frame. FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.replace(R.id.details, details); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } } else { // Otherwise we need to launch a new activity to display // the dialog fragment with selected text. Intent intent = new Intent(); intent.setClass(getActivity(), DetailsActivity.class); intent.putExtra("index", index); startActivity(intent); } } }
第二个fragment,DetailsFragment显示TitlesFragment中选择的列表项对应的剧集介绍:
public static class DetailsFragment extends Fragment { /** * Create a new instance of DetailsFragment, initialized to * show the text at 'index'. */ public static DetailsFragment newInstance(int index) { DetailsFragment f = new DetailsFragment(); // Supply index input as an argument. Bundle args = new Bundle(); args.putInt("index", index); f.setArguments(args); return f; } public int getShownIndex() { return getArguments().getInt("index", 0); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (container == null) { // We have different layouts, and in one of them this // fragment's containing frame doesn't exist. The fragment // may still be created from its saved state, but there is // no reason to try to create its view hierarchy because it // won't be displayed. Note this is not needed -- we could // just run the code below, where we would create and return // the view hierarchy; it would just never be used. return null; } ScrollView scroller = new ScrollView(getActivity()); TextView text = new TextView(getActivity()); int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getActivity().getResources().getDisplayMetrics()); text.setPadding(padding, padding, padding, padding); scroller.addView(text); text.setText(Shakespeare.DIALOGUE[getShownIndex()]); return scroller; } }
下面是DetailsActivity的实现,竖屏的时候会使用到:
public static class DetailsActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { // If the screen is now in landscape mode, we can show the // dialog in-line with the list so we don't need this activity. finish(); return; } if (savedInstanceState == null) { // During initial setup, plug in the details fragment. DetailsFragment details = new DetailsFragment(); details.setArguments(getIntent().getExtras()); getFragmentManager().beginTransaction().add(android.R.id.content, details).commit(); } } }
如果是竖屏的话,结束这个activity,以便横屏时可以显示DetailsFragment到TitlesFragment旁边。