一个fragment呈现activity中的一个行为或者一部分用户界面,你可以在一个activity中嵌套多个fragment,也可以在多个activity中复用一个fragment,你可以把fragment想成是activity的一个组合模块,它有自己的生命周期,接受自己的输入事件,并且你可以在activity运行时进行fragment的添加、删除操作。
fragment必须内嵌在一个activity中,并且它的生命周期被宿主activity的生命周期直接影响。比如,当这个activity被paused,那么它里面所有的fragment都会被paused,当这个activity被destroyed,那么它里面所有的fragment也都会被destroyed。但是,当activity处于running状态时,你可以自主地操作fragment,比如添加、删除fragment。当你执行一个fragment事务,你可以把fragment添加到一个宿主activity的backstack(返回栈)中——每个返回栈记录了被触发的fragment事务。返回栈允许用户通过点击back按键倒退fragment事务。
当你添加一个fragment作为你的activity中的一部分时,它存活于activity的视图层级关系中的viewgroup里面并且可以定义自己的view布局。你可以通过在activity的layout布局文件中声明一个fragment(作为<fragment>元素)来插入一个fragment,也可以通过代码在程序中把它添加到已存在的一个viewgroup中。然而,一个fragment并不被要求一定要作为一个activity的layout文件中的一部分,你也可以使用一个fragment作为activity一个不可见的部分。
本文档主要描述如何使用fragment来创建你的application,包括当一个fragment被添加到activity的返回栈中怎么可以维持他们的状态、与activity和activity中的其他fragment共享事件、为activity中action bar做出贡献,还有一些其他的内容。
Android在version3.0里第一次介绍了fragment,主要是为了更多的支持大屏幕的动态灵活的UI设计,比如说在平板上。因为平板的屏幕比一般手机的屏幕大,拥有更多的空间来组合交换UI组件。fragment允许你不需要去管理复杂的view层级变化。通过把activity布局分成多个fragment,你可以在runningtime修改activity的外观并且在activity管理的返回栈里保存变化。
例如,一个程序可以在左边显示一个商品表而在右边显示这个商品的详情——两个fragment显示在一个activity中,每个fragment都有自己的生命周期回调方法并处理自己的输入事件。如此,用户在一个activity里选择一个商品并查看商品详情,而不是在一个activity选择商品却在另外一个activity中查看商品详情。
在你的application中fragment应该是一个标准且可重用的组件。那是因为fragment使用生命周期回调方法定义自己的布局和行为,你可以在多个activity中包含一个fragment。fragment可以提高不同屏幕尺寸的用户体验效果这点很重要。例如,当屏幕够大时你可以在一个activity中包含多个fragment,否则你可以通过fragment分裂成多个activity区显示。
例如--继续以上面那个图的应用为例--这个应用能嵌入两个fragment在Activity A,当运行在一个特大屏幕时(如在tablet 平板),然而,当运行在一个普通大小的屏幕时,没有足够的空间放下两个fragment,所以Activity A只包含商品列表的fragment,当用户选择一个商品时,它start 显示商品详情的Activity B,因而,应用支持两种设计模式。
要创建一个fragment,你必须创建一个继承Fragment的子类。这个fragment类在代码上很像activity。包含一些和activity很像的回调方法,比如onCreate()、onStart()、onPause()和onStop()。实际上,假如你正在使用fragment来重写一个以前的application,你可能仅仅需要把activity回调方法中的代码复制到fragment的回调方法中去。
通常你至少需要实现下面这几个生命周期回调方法:
onCreate():当创建一个fragment时,系统回调这个方法。在这个方法中,你应该初始化这个fragment的基础组件——当这个fragment暂停或者停止时你想保留的东西(初始化上次离开这个fragment时所保存的一些状态)。
onCreateView():当这个fragment第一次绘制它的UI时,系统回调这个方法。为了给你的fragment绘制一个UI,你必须从这个方法里返回一个View作为你的fragment布局的rootView。假如这个fragment不提供一个UI的话就可以返回null。
onPause():当用户离开这个fragment时,系统回调这个方法,作为用户正在离开这个fragment的第一个 提示(尽管这并不意味着这个fragment正在被销毁)。在这里你通常需要保存一下状态的变化(因为用户可能不会再回来了)。
大多数application至少需要为每个fragment实现上面三个回调方法,但是有几个其他的回调方法,你可能也要实现用来处理fragment生命周期的不同阶段。其它全部的生命周期回调方法在稍后讨论。
还有几个Fragment类的子类你可能想要继承,来代替继承Fragment类:
DialogFragment
显示一个悬浮的对话框。相对于使用Activity类中dialog帮助方法,使用这个类来创建一个对话框是一个更好的选择。因为这样你可以把一个对话框收编到这个activity管理的fragment返回栈中去,允许用户返回一个消失了的fragment。
ListFragment
显示一个被adapter(比如SimpleCursorAdapter)管理的列表,类似于ListActivity。它提供了一些方法来管理一个列表视图。比如onListItemClick()回调方法用来处理点击事件。
PreferenceFragment
显示一个Preference对象层次结构作为列表。类似于PreferenceActivity。当你要为你的application创建一个设置界面时是很有用的。
fragment通常被当做activity的用户界面的一部分来使用,并且可以导入自己的布局到activity中。
为了给一个fragment提供一个布局,你必须实现onCreateView()回调方法。
注意:假如你的fragment是ListFragment的一个子类,onCreateView()默认是返回一个ListView,所以你可以不实现这个方法。
为了从onCreateView()方法返回一个布局,你可以inflate一个使用XML定义的layout资源文件。为了帮助你这样做,onCreateView()方法中提供了一个LayoutInflater对象。
例如,这是一个Fragment的子类从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); } }这个通过onCreateView()方法传递进来的参数 container是父ViewGroup(你的fragment将被内嵌在这里面)。参数savedInstanceState是一个bundle,假如这个fragment是被恢复的,它提供前面那个fragment实例的数据。
这个inflate()方法有三个参数:
现在,你已经知道如何通过提供layout文件来创建一个fragment了。下一步,你需要把fragment添加到activity中。
通常,fragment作为宿主activity的UI界面的一部分,作为activity整个视图层级结构的一部分内嵌进去。你可以通过两种方式来给activity的布局添加fragment:
在这种情况下,你可以就像给一个View指定属性一样给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布局时,它将会实例化layout中指定的每个fragment和为每个fragment调用onCreateView()方法。系统将会用onCreateView()方法返回的View来替代<fragment>元素。
注意:每个fragment必须要求有唯一的标识符以至于当一个activity是restarted时系统可以用来恢复这个fragment(和你可以用来捕获fragment执行的事务,比如移除操作)。有三种方式可以为一个fragment提供一个ID:
当你的activity处于running的任何时候,你都为你的activity layout添加fragment。你只需要简单指定一下这个fragment放置于哪个ViewGroup中就行了。
为了在你的activity中处理fragment事务(比如添加、移除、替换),你必须使用FragmentTransaction。在你的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()方法来使得改变生效。
上面的示例展示了通过添加一个fragment来给你的activity提供一个UI界面。但是,你也可以通过使用一个没有额外的UI界面的fragment来为activity提供一个一个后台行为。
为了添加一个没有UI界面的fragment,通过从activity调用add(Fragment, String)来添加一个fragment (为这个fragment提供一个独一无二的tag标签,而不是一个view ID)。但是,这种方式添加一个fragment,没有与activity layout中的一个View关联起来,它不会接收到一个onCreateView()调用。所以你不需要实现这个方法。
通过提供一个字符串标签tag来添加一个没有UI界面的fragment是不严格的——你也通过提供一个字符串标签tag来添加一个有UI界面的fragment——但如果fragment没有UI界面,然后这个字符串tag就是唯一的方式来定义fragment了。在之后假如你想从activity中获取到这个fragment,你想要使用findFragmentByTag()。
为了管理在你的activity中的fragment,你想要使用FragmentManager。为了得到它,你需要在activity中调用getFragmentManager()。
你可以通过FragmentManager类做到的一些事情:
关于在activity中使用fragment的一个很好功能是添加、移除、替换和执行一些其他的操作,响应用户交互方面的能力。
每次你提交给activity的一系列改变叫做一个事务,你可以使用API FragmentTransaction来执行一个事务。你也可以保存每一个事务到由activity管理的返回栈中,这样你就可以向后导航至fragment之前的变化。
你可以像下面这样通过FragmentManager获取到一个FragmentTransaction:
每一个事务是你想要同时执行的一系列变化。你可以使用比如add()、remove()和replace()这些方法为某个指定的事务设置所有你想执行的改变,然后,为了让这个事务应用到activity中去,你必须调用commit()方法。在你调用commit()方法之前,你可能会想调用addToBackStack()方法来把这个事务添加到事务返回栈中去,这个事务栈由activity管理它允许用户通过按下返回键来回退至一个fragment状态。
例如,下面这个例子就会告诉你怎么使用另外一个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();
在上面这个例子当中,新的fragment替代了容器R.id.fragment_container中的fragment。并且通过调用addToBackStack,把这个替换事务保存至了返回栈中,以至于用户可以通过按下返回键来反向执行这个事务并回退至上一个fragment。
假如添加了多个变化到你的事务中(如add()和remove())并且调用了addToBackStack()方法,在你调用commit()方法前的所有操作是作为一个事务的,当你按下回退键,这些所有的操作都将被反转。
你添加至事务的所有变化的顺序是无关紧要的,除了:
当你执行一个移除fragment的事务时,如果你没有调用addToBackStack()方法,那么当事务被提交,那个fragment就会被销毁,并且用户不能返回到那个fragment。
小点:对于每个fragment事务,你可以在commit()方法前通过调用setTransition()方法设置一个渐变动画。
调用了commit()方法后并不会立即执行。而是一旦主线程能够运行到时,就把这个事务添加到activity的UI线程(主线程)调度表中。假如有必要的话,你可以在你的UI主线程中调用executePendingTransaction()方法来立即执行commit()提交的事务。通常情况下是没有必要这样做的,除非在别的线程工作中对这个事务有依赖。
警告:你只能在activity保存它的状态前使用commit()提交fragment事务。假如你在那个点之后提交的话将会抛出异常。这是因为如果activity需要restore,提交之后activity的state可能会丢失。对于这种丢失activity状态是你可以接受的情况,你就使用commitAllowingStateLoss()方法吧。
虽然fragment是作为一个独立于activity的对象实现的,并且可以插入到多个activity中,但一个给定的fragment实例化对象是直接被绑定到包含它的activity上的。
特别地,fragment可以通过getActivity()方法来访问到activity实例对象,并且可以轻易的执行比如查找activity layout中的某个view:
View listView =getActivity()
.findViewById
(R.id.list);
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
public static class FragmentA extends ListFragment { ... // Container Activity must implement this interface public interface OnArticleSelectedListener { public void onArticleSelected(Uri articleUri); } ... }
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"); } } ... }
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);
}
...
}
Resumed这个fragment在正在运行的activity中是可见的。Paused另一个activity处与前台并且获取了焦点,但是包含这个fragment的activity还可见到(那个前台activity部分透明或者没有完全覆盖整个屏幕)。Stoped这个fragment是不可见的。fragment的宿主activity已经停止,或者fragment被activity移除并且被保存到了返回栈中。一个停止的fragment是仍然存活着的,但是,它不再对用户可见,并且随着activity被killed而killed。
@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>
<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>
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(); if (index == 0) { ft.replace(R.id.details, details); } else { ft.replace(R.id.a_item, 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); } } }
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; } }
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(); } } }