1.2.1 Fragments - 碎片
在activity中,Fragment代表了一种行为和用户界面的一部分。在一个activity里,你可以联合多个fragment来创建一个多面板的UI,你也可以在多个activity里重复使用同一个fragment。你可以把fragment当作activity里一个模块化的部分。fragment有自己的生命周期,接收自己的输入事件,当activity运行时,你可以添加移除fragment。
一个fragment总是被嵌入到activity中,并且它的生命周期会受到宿主activity生命周期的影响。例如,当activity暂停时,它里面的fragment都暂停了,当activity停止时,它里面的fragmentation都暂停了。尽管如此,但是,当actiivty运行时(即生命周期中的resumed状态),你可以独立的操作每个fragment,例如添加和移除fragment。当你执行一个fragment操作时,你也出可以把它添加到由activity管理的后退堆里--activity里的每个返回堆实体都是fragment的记录。返回堆允许用户通过点击返回键恢复一个fragment事务。
当你在activity布局里添加了fragment时,fragment会存活在activity层次的ViewGroup里,并且定义了自己的视图布局。通过在activity的布局文件里申明fragment来把fragment插入到activity的布局里,是一个<fragment>标签,或者在代码里把一个fragment添加到一个 ViewGroup里。尽管如此,但是,fragment不是activity布局必须的部分。
该文档描述了如何在你的应用里使用fragment,包括当把fragment添加到回退堆栈后,如何维持它的状态,如何与activity里其它的fragments来共享事件,把fragment添加到activity的action bar里,等等。
Design Philosophy - 设计哲学
在Adnroid 3.0(API level 11)时引入了fragment,最初的意图是在大的屏幕上支持更多动态的、灵活的UI布局,例如tablet。因为tablet的屏幕比手机的屏幕大多了,联合UI组件和交换UI组件有了更多的空间来使用。fragment允许这样的设计不需要你来管理复杂的视图层次的变化。通过把一个activity的布局分割成为不同的碎片,你就可以更改activity在运行时的展现,并且在一个由activity管理的后退堆栈里保存这些改变。
例如,一个新闻的应用可以使用fragment在左边显示一个文章的列表,然后使用另外一个fragment在右边显示文章的内容--也就是说,在一个activity里并排地显示了两个fragment,每一个fragment都有自己生命周期回调方法的设置,并且各自处理各自的输入事件。因此,不是使用一个activity来显示列表,另外一个activity来显示内容,而是在同一个activity里,用户既可以看到列表,又可以选择列表的某个内容来阅读,就像是图表1里显示的布局一样。
你应该把每一个activity设计成为模块化的、可重用的activity组件。也就是说,因为每个activity都在自己的生命周期回调里定义了自己的布局和行为,所以你可以在多个activity里包含同一个fragment。这点是非常的重要,因为在不同尺寸的屏幕上,模块化的fragment可以允许你改变你的fragment组合。如果你的应用设计为即支持平板,又支持手机的应用,那么,基于可用的屏幕空间,你可以在不同配置的布局里重用你的fragment来使用户的体验达到最佳。例如,在手机上,同一个activity不能满足多于一个控件的需求,你就可能需要提供单独的fragment来提供单独的UI。
图表1:显示了在tablet上设计时,如何把由fragment定义的两个UI模块放入同一个activity里,但是在手机上设计时,如何将其分离。
例如--接着上面提到的新闻应用的例子--当运行在tablet尺寸的设备上时,在activity A里,可以嵌入两个fragment。尽管如此,但是,在手机屏幕上,这里没有足够的空间来显示两个fragment,因此,activity A仅仅包含了显示列表的fragment,当用户选择了列表上的某一顶时,打开activity B,在activity B里,包含了用于显示内容的第二个fragment。因此,通过以不同组合方式来重用activity的方法,应用既支持tablet,又支持手机,就像图表1显示的那样。
为不同的屏幕配置选择不同的fragment组合来为你的应用做设计,这些相关的知识请参见 Supporting Tablets and Handsets章节。
Creating a Fragment - 创建一个fragment
为了创建一个fragment,你必须创建一个Fragment的子类(或者一个它已经存在的子类)。Fragment里的代码看起来很像是activity里的代码。它包含了和activity里类似的回调方法,例如 onCreate(), onStart(), onPause(),和onStop()。事实上,如果你把一个android应用转换为使用fragment,你或许只需要把你原来activity里的回调方法里的内容移动到fragment里相应的回调方法里就可以了。
通常来说,你至少应该实现下面的回调方法:
onCreate():系统创建Fragment对象后回调该方法,实现代码中只初始化想要在Fragment中保持的必要组件,当fragment被暂停或者停止后可以恢复。
onCreateView():fragment首次绘制用户界面时系统会调用该方法。为了给你的fragment绘制UI界面,在这个方法里,你必须返回一个View,这个View是你fragment布局的根元素。如果fragment不需要提供一个UI,那你可以返回null。
onPause():当用户首次离开了fragment时系统会调用该方法(尽管它不总是意味着fragment被销毁了)。通常来说,在这个方法里,你应该提交基于当前用户session的任何改变,并保存这些改变(因为用户或许不会回来了)。
大多数应用应该为每个fragment都实现这三个方法,还有其它几个方法,你可以使用它们在fragment生命周期的不同时期处理一些东东。fragment的所有生命周期方法的详情将会在 Handling the Fragment Lifecycle章节进行讨论。
图表2:fragment的生命周期(当activity运行时)
下面是一些你可能想继承的 Fragment 的子类,并重写它们:
DialogFragment:显示一个浮动的对话框。使用该类来创建一个对话框,这是一个在activity里创建对话框的很好的、可选择的方法,因为你可以把fragment的对话框混合进由activity管理的fragment的后退堆栈里,允许用户返回到一个已经消失了的fragment。
ListFragment:显示由一个适配器管理的元素列表(适配器和SimpleCursorAdapter类似),这个和ListActivity类似。它提供了几个管理列表视图的方法,例如:处理点击事件的onListItemClick()回调方法。
PreferenceFragment:把Preference 对象的层次显示为一个列表,和 PreferenceActivity相似。当为应用创建一个设置页面时,这个子类很有用。
Adding a user interface - 添加一个用户界面
fragment通常是activity用户界面的一部分,并贡献它自己的布局,把它组合到activity里。
为了给fragment提供一个布局,你必须实现onCreateView()回调方法,当fragment绘制它的布局时,android系统会调用这个方法。要实现这个方法,必须返回一个 View ,作为你fragment的布局。
注意:如果fragment是ListFragment的子类,onCreateView()默认会返回一个 ListView,因此你不需要实现它。
为了从onCreateView()方法里返回一个布局,你可以从定义在xml里的layout resource里inflate这个布局。为了帮助你实现它,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 - 为fragment inflate布局。
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
传递给 onCreateView() 方法的参数container是父级ViewGroup (来自activity的布局),也就是fragment布局将被插入的地方。savedInstanceState参数是Bundle类型的,如果fragment被恢复了,它会提供fragment前一个实例的有关数据(存储状态的更多内容会在Handling the Fragment Lifecycle章节进行讨论)。
在上面的例子中,R.layout.example_fragment是对于保存在应用资源里名叫example_fragment.xml布局资源的引用。关于在xml里如何创建布局的更多内容,请参见User Interface 文档。
inflate()方法需要三个参数:
1. 你想inflate的布局的资源ID。
2. 第二个参数ViewGroup 会成为要被inflate布局的父。传递container这个参数是很重要的,是为系统应用这个layout参数到被inflate布局的根元素上,根元素是由fragment将要进入的父视图所决定的。
3. 一个布尔值,标识了被inflate的布局,在inflation期间是否应该被绑定到ViewGroup上(第二个参数)。(在上面的代码中被设置为false,这是因为系统已经把被inflate的布局插入到container里了--如果传递true的话,会在最终的布局上创建多余的视图)。
到现在为止,已经显示了怎么创建一个带布局的fragment。接下来,你需要把fragment添加到你的activity里。
Adding a fragment to an Activity - 把fragment添加到Activity上
一般来说,fragment把自己的布局贡献给宿主activity,也就是说把自己的布局内嵌到activity整体的布局层次中,成为它的一部分。有两种方式可以把fragment添加到一个activity中:
1.在activity的布局文件里申明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,并且为每一个fragment调用onCreateView()方法来检索每一个fragment的布局。系统把fragment返回的 View直接插入到布局文件里,替换原来的<fragment>标签。
注意:每一个fragment都需要唯一的标识,如果activity重启时,系统可以使用这个唯一标识来还原fragment(你也可以根据这个唯一标识获取fragment来执行某些事务,例如移除它)。给fragment提供标识有三种方法:
(1). 使用android:id属性,提供唯一标识。
(2). 使用android:tag属性,提供唯一的字符串。
(3). 如果以上两种属性都没有提供,系统会使用容器视图(译者注:即父视图)的ID。
2. Or,programmatically add the fragment to an existing ViewGroup - 以编码的方式给一个存在的ViewGroup添加fragment
在你activity运行的任意时刻,你都可以把fragment添加到你的activity里。你只需要指定一个放置fragment的 ViewGroup就可以了。
为了在activity里做一些fragment事务(例如添加、移除或者替换),你必须使用 FragmentTransaction类里的API。你可以在activity里获取 FragmentTransaction的实例:
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
在获取FragmentTransaction以后,你可以使用add()方法添加一个fragment,并且要指定把fragment添加到哪个view里。例如:
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
add() 方法的第一个参数的意思是要把fragment添加到的哪个ViewGroup,第二个参数是要添加的fragment。
一旦你使用FragmentTransaction对fragment做了改变,你必须调用commit() 方法来使改变生效。
3. Adding a fragment without a UI - 添加一个没有UI的fragment
上面的例子展示了如何给activity添加一个有UI布局的fragment。尽管如此,但是,你也可以给activity添加一个后台运行的、没有布局的fragment。
使用 add(Fragment, String)方法给activity添加一个没有布局的fragment(为fragment添加一个唯一的tag字符串,而不是给view添加一个ID)。如果这样做了,fragment就没有和activity布局里的某个view联系起来,因此,它就没有接收onCreateView()回调的功能,也就是说你不用实现这个方法。
对于没有UI的fragment,提供一个tag字符串是不严格的--你也可以给有UI的fragment提供tag字符串--但是fragment没有UI,那么tag字符串是唯一识别fragment的方法。在activity里,如果你想获取fragment,你需要使用findFragmentByTag()方法。
对于如何使用一个后台工作的fragment的内容,请查看示例:FragmentRetainInstance.java 。
Managing Fragments - 管理Fragments
为了在activity里管理fragment,你需要使用FragmentManager。在activity里调用getFragmentManager()方法可以获得FragmentManager。
你使用FragmentManager可以做的事情如下:
1. 使用findFragmentById()方法可以获取在activity里存在的fragment(在activity布局文件里提供了UI的fragment),或者使用findFragmentByTag()方法(没有提供UI的fragment)。
2. 使用popBackStack()方法把fragment从后退堆栈里弹出(类似于用户按返回键)。
3. 使用addOnBackStackChangedListener()方法注册一个监听,来监听后退堆栈的改变。
关于FragmentManager的更多方法的详情介绍,请阅读FragmentManager类文档。
Performing Fragment Transactions - 执行fragment事务
activty里使用fragment多的操作是添加、移除、替换、执行其它操作、响应用户交互。你提交给activity的每一系列的改变被称作事务,你可以使用 FragmentTransaction里的APIs来执行事务。你也可以把每一个事务保存在由activity管理的后退堆里,这样就可以让用户在事务堆里不断的进行后退的导航(和在activity里使用back键类似)。
如下所示,你可以从FragmentManager里获取一个FragmentTransaction的实例:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
每一个事务都是一系列的改变,这些改变是你一次执行的东东。对于一个事务,通过调用add(), remove()和replace()方法,可以设置你想执行的所有的改变。然后,调用 commit()方法来应用事务。
尽管如此,但是,在调用 commit()之前,为了给fragment事务的回退堆里添加事务,你或许需要调用addToBackStack()方法。这个回退堆由activity来管理,并且允许用户通过按返回按钮返回到前一个回退堆。
例如,下面的代码展示了如何用一个fragment来替换另外一个,并把原来的状态保存在回退堆里:
// Create new fragment and transaction - 创建新的fragment和事务
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 -- 用新的fragment替换fragment_container视图里的fragment,并把事务添加到回退堆里
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// Commit the transaction -- 提交事务
transaction.commit();
在例子中,newFragment代替了布局里的fragment。通过调用 addToBackStack()方法,被替换的事务保存到后退堆,因此,用户可以通过点击后退键回退事务,回到前一个fragment。
如果你给事务添加了多个改变后调用了addToBackStack()方法,那么在你调用commit() 方法之前所有被应用的改变都被作为一个单独的事务添加到后退堆中,如果点击回退按钮,将把这些改变一起恢复。
你给FragmentTransaction添加改变的顺序无关紧要,除了:
1. 你必须在最后调用commit()方法。
2. 如果你给同一个容器里添加多个fragment,那么,你添加它们的顺序决定了它们在视图层次里显示的顺序。
当你执行移除fragment的事务时,如果你没有调用addToBackStack()方法,那么,当事务提交时,fragment被销毁,用户再也不能返回到这个fragment了。然而,如果你在移除fragment时,确实调用了addToBackStack() 方法,那么,当用户返回时,被停止的方法又会被恢复的。
提示:对于每一个fragment事务来说,在你提交事务之前,通过调用setTransition()方法都可以应用一个过滤动画。
调用commit()方法不会立即执行事务。而是在activity的UI线程("main"线程)上,按照安排来执行,一有机会就会执行。尽管如此,但是,如果有需要的话,你可以在UI线程上调用executePendingTransactions()方法来立即执行通过commit()方法提交的事务。做这一切通常来说不是必须的,除非该事务是另一线程所必须的工作。
警告:在activity保存它的状态之前(当用户离开activity时),你可以使用commit()方法来提交一个事务。如果你尝试在这个点之后提交事务,系统会抛出异常。这是因为如果activity需要被恢复,提交后的状态将会丢失。你想让失去状态的情况变成合法的,那么就请使用commitAllowingStateLoss()方法。
Communicating with the Activity - 和Activity通信
尽管Fragment被实现为从Activity独立出来的一个对象,并且可以在多个activity里使用,但是,fragment的实现直接和包含它的activity相关联的。
特别地,fragment可以使用getActivity()获取的Activity 实例,并且很容易的执行一些任务,例如:从activity布局里找到一个视图:
View listView = getActivity().findViewById(R.id.list);
同样地,通过从FragmentManager对象获取Fragment 的实例(使用findFragmentById() or findFragmentByTag()方法),activity可以调用fragment里的方法,例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
Creating event callbacks to the activity - 创建activity的回调事件
在某些情况下,你或许需要fragment和activity共享事件。实现这个功能好的办法是在fragment里实现回调接口,同时,也需要宿主activity实现该接口。当activity通过接口收到回调时,如果需要的话,它会和一个布局里的fragment共享信息。
例如,如果新闻应用的一个activity里有两个fragment--一个展示文章列表(fragment A)另一个显示文章内容(fragment B)--那么,当列表上的某一元素被点击时,fragment A必须通知activity,activity再通知fragment B来显示某一篇文章。在这种情况下,fragment A里申明了OnArticleSelectedListener接口:
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface -- 宿主activity必须实现这个接口
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
fragment的宿主activity实现了OnArticleSelectedListener接口并重写了onArticleSelected()方法来给fragment B通知来自于fragment A的事件。为了确保宿主activity实现这个接口,fragment A的onAttach()回调方法(当给activity添加fragment时系统调用该方法)通过强制转换给onAttach()方法传递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成员变量会获得OnArticleSelectedListener的activity实现的引用,因此fragment A可以通过调用定义的OnArticleSelectedListener接口来和activity共享事件。例如,如果fragment A是ListFragment的继承者,用户每次点击列表元素,系统都会调用fragment里的onListItemClick()方法,fragment然后调用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是被点击元素的行ID,activity(或者其它fragment)使用它来从应用的ContentProvider里获取文章。
关于使用内容提供器的更多详情请参见Content Providers章节。
Adding items to the ActionBar -- 给ActionBar添加元素
通过实现 onCreateOptionsMenu()方法,fragment也可以把菜单元素贡献给activity的Options Menu。尽管如此,但是,为了使该方法可以接收回调,你在 onCreate()方法里必须调用setHasOptionsMenu()方法,这样做是为了标识fragment将要给Options Menu添加元素(如果不这样做,fragment不会接收到onCreateOptionsMenu()方法的回调)。
任何时候你从fragment添加到Options Menu里的东东都会被追加到已经存在的菜单项里。当菜单项被选择时,fragment也会接收到onOptionsItemSelected()方法的回调。
你也可以在fragment布局里注册一个view,通过调用registerForContextMenu()方法来提供一个上下文菜单。当用户打开上下文菜单时,fragment会接收到onCreateContextMenu()方法的回调。当用户选择元素时,fragment会接收到onContextItemSelected()方法的回调。
注意:当每个菜单项添加时,尽管fragment会接收到一个on-item-selected 的回调,但是,当用户选择一个菜单项时,activity首先会接收到各自的回调。如果activity的on-item-selected回调的实现没有处理被选择的元素,那么,事件会被传递给fragment的回调。上述情况对于Options Menu和上下文菜单也是成立的。
更多的内容,请参见Menus 和Action Bar 开发者引导。
Handling the Fragment Lifecycle - 处理fragment的生命周期
管理fragment的生命周期和管理activity的生命周期是类似的。和activity类似,fragment也存在以下三种状态:
Resumed:在正在运行的activity里,fragment是可见的。
Paused:另一个activity处于前台并且可以接收用户输入,但是fragment所在的activity仍然可见(前台的activity是半透明或者没有完全遮盖屏幕)。
Stopped:fragment不可见。要么是宿主activity已经停止,要到是fragment已经从activity里移除,并且没有添加到回退堆里。停止的fragment仍然是活着的(所有的状态和成员变量的信息都被系统保存下来了)。尽管如此,但是,它对用户来说不再可见,并且当activity被杀死时,它也被杀死了。
图表3:基于activity生命周期的fragment生命周期的效果
和activity类似,你可以使用Bundle来保存fragment的状态,为了防止activity被杀死,当activity被重新创建时,你需要存储fragment的状态。你可以在fragmentation的onSaveInstanceState()回调方法里存储状态,并在onCreate(), onCreateView(), 或者onActivityCreated()方法里恢复它们。保存状态的更多信息,请参见 Activities章节。
activities和fragment生命周期最重要的不同点是在各自的返回堆里如何存储。当activity停止时,默认地,它被放入由系统管理的后退堆里(这样的话,用户就可以使用后退键回到先前的activity,具体的讨论在Tasks and Back Stack章节)。虽然如此,但是,fragment被放入由宿主activity管理的回退堆里,仅仅当你明确地请求在事务里通过调用addToBackStack() 方法保存的实例时,才会从回退堆里移除fragment。
另外,管理fragment的生命周期和管理activty的生命周期是类似的。因此,练习managing the activity lifecycle 也同样适合于fragment。你所需要理解的是,activity的生命周期如何影响fragment的生命周期。
警告:在Fragment里,如果你需要获取Context对象,要调用 getActivity()方法。虽然如此,但是,当fragment绑定到一个activity时,调用该方法时一定要小心。当fragment还没有被绑定,或者在它生命周期的末尾,它已经和activity分离了,这时你再调用该方法就会返回null。
Coordinating with the activity lifecycle - 与activity的生命周期相协调
fragment用于生存的activity的生命周期直接影响了fragment的生命周期,例如,每一个activity的生命周期回调方法会导致一个fragment类似的回调。例如,当activity收到onPause()回调方法时,每一个fragment也会收到onPause()回调方法。
fragment有几个其它的生命周期回调方法,这些方法是为了处理和activity的交互而执行一些动作的,例如构建和销毁fragment的UI。回调方法如下:
onAttach():当使用activity关联fragment时调用该方法(Activity 被当作参数传入)。
onCreateView():当创建与fragment关联的视图层次时调用该方法。
onActivityCreated():当activity的onCreate()方法返回结果时调用该方法。
onDestroyView():当与fragment相关联的视图层次被移除时调用该方法。
onDetach():当即从activity分离fragment时调用该方法。
如果图表3所示,fragment的生命周期流会受到其宿主activity的影响。从图表3中可以看出,activity每个连续的状态决定了fragment会接收哪些回调方法。
一旦activity到达了resumed状态,你可以给activity自由的添加和移除fragment。因此,仅仅当activity处于resumed状态,fragment的生命周期可以独立的改变。
虽然如此,但是,当activity离开了resumed状态,activity可以再次把fragment压入它的生命周期。
Example - 示例
为了把上面讨论的所有东东整合在一起,让我们做一个例子,例子里,activity有两个fragment,创建了两个面板的布局。activities用一个fragment来显示莎士比亚戏剧的标题列表,当点击某个标题时,另外的fragment显示戏剧的摘要。下面的例子也演示了,如何基于屏幕配置,提供不同的fragment配置。
注意:activity完整的代码是 FragmentLayout.java。
在主acitivty的onCreate()方法里,使用了常见的方式来应用布局。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_layout);
}
应用的布局是fragment_layout.xml:
<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>
当activity加载布局时,系统就实例化了TitlesFragment(以列表的形式显示戏剧标题),而FrameLayout(显示戏剧摘要的fragment)占据了屏幕右边的空间,但是在一开始时,该FrameLayout是空的。直到用户选择了某个戏剧标题时,显示内容的fragment才会被放入FrameLayout里。
虽然如此,但是,不是所有的屏幕都有足够的空间在一屏里并排的显示列表和内容。因此,上面的布局仅仅是为landspace screen配置使用的,把上面的文件保存在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来显示摘要,而不是读取第二个fragment。
接下来,你会看到如何完成fragment类。首先完成TitlesFragment,这是用来显示莎士比亚戏剧标题列表的fragment,它继承了ListFragment 类,用它来处理列表视图的工作。
当你查看代码时,你要注意:当用户点击列表元素时,有两种可能的行为:依赖于两个fragment的存活状态,要么是创建并显示一个新的fragment来在同一个activity里展示摘要(给FrameLayout里添加fragment),要么开启一个新的activity来显示fragment。
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. -- 确保UI在正确的状态
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. -- 显示选择项详情内容的方法。要么在当前UI的原位置显示fragment,要么开启一个新的activity显示
*/
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. -- 检查fragment当前显示的内容,如果需要的话替换内容。
DetailsFragment details = (DetailsFragment)
getFragmentManager().findFragmentById(R.id.details);
if (details == null || details.getShownIndex() != index) {
// Make new fragment to show this selection. -- 生成一个新的fragment来显示选择
details = DetailsFragment.newInstance(index);
// Execute a transaction, replacing any existing fragment
// with this one inside the frame. -- 执行事务,使用在框架里的fragment来替换已经存在的。
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. -- 需要开启一个新的activity来显示内容
Intent intent = new Intent();
intent.setClass(getActivity(), DetailsActivity.class);
intent.putExtra("index", index);
startActivity(intent);
}
}
}
第二个fragment DetailsFragment显示了戏剧的摘要:
public static class DetailsFragment extends Fragment {
/**
* Create a new instance of DetailsFragment, initialized to
* show the text at 'index'. -- 创建DetailsFragment的新实例,用于显示文本
*/
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;
}
}
如果用户点击了列表中的某项并且当前布局没有包含R.id.details视图(包含DetailsFragment)时,会回调TitlesFragment类,然后应用打开DetailsActivity 来显示列表对应的内容。
下面的代码是DetailsActivity类,当屏幕处理垂直(竖屏)时,内嵌在DetailsFragment 里显示戏剧的摘要。
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. -- 如果屏幕处于横屏模式,我们把摘要和列表并排的显示,这样就不需要当前activity了。
finish();
return;
}
if (savedInstanceState == null) {
// During initial setup, plug in the details fragment. -- 在初始化设置期间,插入详情fragment
DetailsFragment details = new DetailsFragment();
details.setArguments(getIntent().getExtras());
getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
}
}
}
注意:如果屏幕是横屏,activity会结束自己,这样主activity就会在TitlesFragment旁边显示DetailsFragment。当是竖屏时进入该activity,但是放置到横屏时就退出了该activity。
更多fragment的例子,请参见ApiDemos 。