第三部分:Android 应用程序接口指南---第一节:应用程序组件---第一章1-1.Fragment
第1-1章 Fragments
在Activity中的fragment代表的是一种行为或用户界面的一部分。你可以在activity中结合多个fragments创建一个多面板UI,并可以在多个activity中重复使用fragment。你可以把fragment看作是activity的模块化的一个部分,它有自己的生命周期,接收它自己的输入事件,并且当activity正运行时,可以添加或移除fragment(这有点像在不同的activity中重复使用的“sub activity”)。
Fragment必须被嵌入到activity中,并且它的生命周期直接受到activity生命周期的影响。例如,当activity被暂停时,在它里面的fragment也会暂停,当activity被销毁,所有的fragment也会销毁。然而,当activity正在运行(它正处于resumed生命周期状态),你可以单独操作fragment,如添加或移除他们。当你执行这样一个fragment的事务处理时,你也可以把它添加到由activity管理的后台堆栈里,在activity中的每个后台堆栈都是一条记录,记录了fragment发生的事务处理。后台堆栈还允许用户按“返回”键倒退到fragment的事务处理(向后导航)。
当添加一个fragment作为你的activity布局的一部分时,fragment就属于activity view层级里中的ViewGroup,它可以定义自己的view布局。你可以通过声明在activity布局文件里的fragment,来插入一个fragment,这个fragment是作为<fragment>节点插入到activity布局,或把它添加到一个现有的ViewGroup。但是,fragment也可以不成为activity布局的一部分,你可以把一个没有自己UI的fragment当作是一个activity的隐藏工人。
接下来将介绍如何使用fragment来构建应用程序,包括当fragment添加到后台堆栈时,它如何保留状态,它是如何与activity和其他在activity中的fragments共享事件,以及对activity的action bar的促进作用。
1-1.1 设计理念
Android 3.0(API level 11)采用了fragments,主要是为了在大屏幕上能支持更具动态和灵活性的UI设计,如平板电脑。因为平板电脑的屏幕要比手机的屏幕大得多,这样就有更大的空间来组合和互换UI组件。Fragments允许这样设计,就是它不用管理你view层次的复杂变化。当fragment成为activity布局的一部分时,你就能修改运行时activity的外观,并且可以保存那些后台堆栈中的改变。
例如,一个新闻类的应用程序可以用一个fragment在左边展示文章列表,在右边就用另一个fragment来展示文章,两个fragment在同一个activity中并排出现,并且每一个fragment都有它自己的一套生命周期回调方法,处理他们自己的用户输入事件。因此,用户不是在一个activity中选择文章,另一个activity中阅读文章,而是在同一activity中选择文章并且阅读,正如下面图1-1的平板电脑布局。
你应该把每个fragment设计成一个模块化和能重复使用的activity组件。也就是说,因为fragment定义自己的布局和有自己的生命周期回调方法,所以你应该把它设计成重复使用,可以直接避免操控到另一个fragment。尤其重要的是,一个模块化的fragment能够使你的fragment组合有着不同的屏幕大小。当要把你的应用程序设计成能同时支持平板电脑和手机时,你就在可用屏幕空间基础上,重复使用在不同布局配置上的fragments来优化用户体验。例如,在手机上,多个fragments不能应用到同一activity时,就有必要单独分开一个fragment来提供一个单面板 UI。
图1-1 平板电脑和手机里fragment布局
图1-1介绍了fragment是如何定义出两个UI模块,把这两个UI模块结合在一起的就是为平板电脑设计的activity,而分开来就是为手机设计的一个activity。
继续上面的例子,当运行平板类设备时,信息应用程序就可以在Activity A中嵌入两个fragment。然而,在手机屏幕上,就没有足够的空间来嵌入两个fragment,所以Activity A就只包含一个展示文章列表的fragment,当用户选择一篇文章时,就会启动Activity B,其中就包含了一个阅读文章的fragment。因此,应用程序可以通过重复使用不同组合的fragments来同时支持平板电脑和手机,正如图1-1所示。
1-1.2 创建一个fragment
要创建一个fragment,就必须先创建一个fragment的子类(或fragment的现有子类)。Fragment的子类代码看起来非常像一个activity。它包含一些类似activity的回调方法,如onCreate()、onStart()、onPaused()和onStop()。事实上,如果你想用fragments来转换一个现有的Android应用程序,你可能只要简单的把activity回调方法的代码移到你的fragment。通常,你至少应该实现下面几种生命周期回调方法:
1.onCreate():
当创建fragment时系统会调用这个方法。在这个方法的实现中,你应该初始化一些fragment的重要组件,这些是当fragment被暂停或停止,然后恢复它时你想要保留的组件。
2.onCreateView():
当fragment首次绘制用户界面时,系统会调用这个方法。你要为fragment绘制一个UI,就必须从这个方法返回一个View,这个view是fragment布局的根view。如果fragment不提供一个UI,那么返回的就是一个null。
3.onPaused():
系统调用此方法是用户正离开你的fragment(尽管它并不总是意味着fragment正被销毁)的第一个迹象。通常你应该提交一些改变(因为用户可能不回来)。
大多数应用程序的fragment都至少应该实现上面三种回调方法,但这有几个其他的回调方法,你也应该用它们来处理fragment生命周期的各个阶段,如图1-2所示:
图1-2 fragment的生命周期(activity正在运行)
下面是几个你可能想要延伸的子类,这并不是fragment的基础类:
1.DialogFragment:
显示了一个浮动的对话框。使用这个类根据Activity类中的对话框辅助方法,来创建一个对话框是一个很好的选择,因为你能合并一个fragment 对话框到一个后台堆栈中,所以允许用户返回一个已经被解散的 fragment。请注意,这个返回的堆栈是属于由activity管理的fragment。
2.ListFragment:
显示了一个由adapter(适配器)管理的列表项,类似于ListActivity。这个类提供了几个用来管理view列表的方法,如处理click事件的onListItemClick()方法。
3.PreferenceFragment:
显示了一个Preference对象的列表层次结构,类似于PreferenceActivity。当要为你的应用程序创建一个“settings”activity时这个类是有用的。
1.添加一个用户界面:
Fragment通常是作为一个activity用户界面的一部分并且把自己的布局贡献给activity。
为了给fragment提供一个布局,你必须实现onCreateView()回调方法,当fragment要绘制它的布局时系统会调用这个方法。这个方法的实现中,必须返回一个fragment布局的根View。
注意:如果你的fragment是ListFragment的子类,那么onCreateView()方法的默认实现就会返回一个ListView,所以你不需要去实现这个方法。
为了从onCreateView()中返回一个布局,你可以在XML中把一个定义好了的layout 资源给找出来。onCreateView()方法提供了一个LayoutInflater对象来帮助你。
下面是一个fragment的子类加载example_fragment.xml文件中的一个布局,如代码清单1-1所示:
public static class ExampleFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // 为这个fragment Inflate布局 return inflater.inflate(R.layout.example_fragment, container, false); } }
代码清单1-1
传递onCreateView()方法的container参数是ViewGroup(从activity布局)的父类,你的fragment布局将插入到ViewGroup中。savedInstanceState参数是一个Bundle,如果fragment恢复的话,它就会提供有关fragment先前实例的数据。上面的例子中,R.layout.example_fragment是保存在应用程序资源里一个名为example_fragment.xml的文件,它是布局资源里的一个引用。
Inflate()方法需要三个参数:
1.你想要找出来的布局的资源ID。
2.ViewGroup要成为inflated布局的父类。为了使系统能把布局参数应用到父类指定的inflated布局里的根view,所以传递container参数是很重要的。
3.一个boolean值表示在inflated期间是否需要inflated布局附属到ViewGroup上(第二个参数)。(在这种情况下,这是错误的,因为系统已经把inflated布局插入到container中,如果传递的是一个true值,那么将在最终的布局里创建一组冗余的view)。
你已经知道了如何创建一个提供布局的fragment,下面就需要把fragment添加到你的activity中。
2.给activity添加一个fragment:
通常,fragment贡献UI的一部分给activity,并把它作为view整体层次的一部分嵌入到activity中。这有两个方法你可以用来把fragment添加到activity布局:
- 声明activity布局文件里的fragment:
在这种情况下,你可以指定fragment的布局属性,就好像它是一个view。例如,下面这个activity里有两个fragment的布局文件,如代码清单1-2所示:
<?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>
代码清单1-2
<fragment>中的android:name属性会初始化在布局中指定好了的fragment类。当系统创建activity布局时,它就会初始化每个在布局中指定好了的fragment,并且调用onCreateView()方法,检索每个fragment的布局。系统将把从fragment返回的view直接代替<fragment>节点。
注意:每个fragment都需要一个唯一的标识符,当系统重启时,系统可以用它来保留fragment(你可以用它来捕获fragment,从而执行事务处理如移除它)。这有三种可以为fragment提供一个ID的方法。
(1) 给android:id属性提供一个唯一的ID。
(2) 给android:tag属性提供一个唯一的ID。
(3) 如果上面两种都没有提供,那么系统就会使用container view中的ID。
- 以编程方式把fragment添加到现有的ViewGroup中
在你的应用程序运行的任何时候,你都可以把fragments添加到你的activity布局中。你只需指定一个ViewGroup来放置fragment即可。在activity中进行fragment事务处理(如添加、移除或替换一个fragment),你都必须使用FragmentTransaction中的APIs。下面是FragmentTransaction的一个实例,如代码清单1-3所示:
FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
代码清单1-3
然后,你可以使用add()方法添加fragment,通过指定要添加的fragment和在其中插入fragment的view。如代码清单1-4所示:
ExampleFragment fragment = new ExampleFragment(); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit();
代码清单1-4
传递给add()的第一个参数是ViewGroup,可以把fragment放到里面,并且有指定的资源ID,而第二个参数是你要添加的fragment。一旦使用FragmentTransaction改变了fragment,你就必须调用commit()方法来使它生效。
3.添加一个没有UI的fragment:
上面的例子展示了如何把有一个UI的fragment添加到activity。然而,你也可以用没有额外UI的fragment为你的activity提供后台行为。
添加一个没有UI的fragment,你只要在activity中用add(Fragment,String)(提供一个有唯一“tag”字符串的fragment,而不是一个view ID)方法添加一个fragment。但是因为它并没有与activity布局中的view相关联,所以它不会接收到onCreateView()方法的调用,所以你也不需要实现这个方法。
为fragment提供一个字符串tag,并不严格局限于没有UI的fragment,你也可以为带有UI的fragment提供一个字符串tag,但是如果fragment没有UI,那么字符串tag是识别它的唯一方法。如果之后你想从activity获得这个fragment,那你可以使用findFragmentByTag()方法。
1-1.3 管理fragments
要管理activity中的fragments,你就需要使用FragmentManager。为了做到这一点,你需要从activity中调用getFragmentManager()方法。使用FragmentManager你可以做一些事情包括:
1.用findFragmentByID()(为在activity布局中的fragments提供一个UI)和findFragmentByTag()(为fragments提供或不提供一个UI)方法可以从现有activity中获得fragments。
2.用popBackStack()(模拟一个用户返回命令)方法,fragments就会出栈。
3.用addOnBackStackChangeListener()方法注册一个listener,以便改变能回到堆栈。
正如上面演示的,你也可以用FragmentManager开启一个FragmentTransaction,它允许你执行transaction,如添加和移除fragments。
1-1.4 执行fragment的transaction
在activity中使用fragments的一个很大特点是它的添加、移除和执行,以及响应用户交互的能力。你提交到activity的每一组改变都被称作transaction,你可以在FragmentTransaction中通过APIs来执行他们。你也可以把每个transaction都保存在托管的后台堆栈中,它允许用户通过fragment的改变向后导航(与通过activities向后导航类似)。
下面是FragmentTransaction的一个实例,如代码清单1-5所示:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
代码清单1-5
每个transaction都是一组你想在同一时间执行的改变。对于一个给定的transaction来说,你可以通过一些方法如add()、remove()和replace()方法来设置所有你想要执行的改变。然后要把transaction应用到activity,你就必须调用commit()方法。
然而,在你调用commit()方法之前,你也许想要调用addToBackStack()方法,把transaction添加到fragment transaction的后台堆栈中。这个后台堆栈是由activity管理的,并且它允许用户按Back键返回到先前fragment的状态。
下面是一个如何用一个fragment替换另一个fragment,并在后台堆栈中保存先前状态的例子,如代码清单1-6所示:
// 创建新的fragment 和 transaction Fragment newFragment = new ExampleFragment(); FragmentTransaction transaction = getFragmentManager().beginTransaction(); // 使用这个fragment 替换ID为fragment_container的 view ,并添加一个transaction到后台栈中 transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // 提交transaction transaction.commit();
代码清单1-6
在上面例子中,newFragment可以随便替换任何一个当前布局中包含R.id.fragment_container ID的fragment。通过调用addToBackStack()方法,替换transaction会保存在后台堆栈中,所以用户按Back键就能回退transaction并恢复先前的fragment。
如果你把多项改变一同添加到fragment中并调用addToBackStack()方法,那么在你用commit()将一个单一的transaction添加到回堆栈之前,所有的改变都会应用并且按下Back键将会把他们一起倒退。
把改变添加到FragmentTransaction的顺序是不重要的,除非:
1.你必须在最后调用commit()方法。
2.如果你把多个fragments添加到同一container,那么你添加他们的顺序将决定他们在view层次中显示的顺序。
当执行一个移除fragment的transaction时,你不调用addToBackStack()方法,那么当这个transaction被提交后时,fragment会被销毁,并且用户不能导航回它。相反,如果你调用了这个方法,那么fragment会暂停,用户向后导航时它可以恢复。
方法:对于每个fragment的transaction来说,你在提交之前,通过调用setTransaction()方法,你就可以使用一个过渡动画。
调用commit()方法并不会立即就执行transaction。相反,只要activity的UI线程有能力去完成,它就会把这个提交列入计划让他在activity的UI线程(“主”线程)上运行。如果有必要的话,你可以从你的UI线程上调用executePendingTransaction()方法来立即执行由commit()提交的transactions。通常这样做是没有必要的,除非transaction是其他线程工作上的依赖。
注意:只有在activity保存它的状态(当用户离开activity时)之前你才可以用commit()来提交一个transaction。如果你在这个时间点之后尝试提交,那么将会收到一个异常。这是因为activity想要恢复的状态会在提交transaction后丢失。在你觉得会丢失这次提交的transaction时,你可以调用commitAllowingStateLoss()方法。
1-1.5 与activity通信
尽管fragment已经作为一个依赖activity的对象来实现,并能在多个activity中使用,但一个给定的fragment实例是直接与包含它的activity绑定的。
具体来说,fragment可以通过getActivity()方法来访问activity实例,并且轻松地执行任务比如在activity布局中查找view,如代码清单1-7所示:
View listView = getActivity().findViewById(R.id.list);
代码清单1-7
同样的,你的activity也可以通过使用findFragmentByID()或findFragmentByTag()方法从FragmentManager中获得一个fragment引用,从而调用fragment中的方法,如代码清单1-8所示:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
代码清单1-8
1.创建activity的事件回调:
在一些情况下,你可能需要一个fragment来与activity共享事件。一个好的方式是在fragment中定义一个回调接口,并要求activity去实现它。当activity通过这个接口接收到回调时,如果需要的话它就能与布局里的其他fragments分享信息。
例如,如果一个消息应用程序在activity中有两个fragment,一个用来显示文章列表(fragment A),另一个用来展示文章(fragment B),然后当选中了一个列表项时,fragment A就必须告诉activity,这样activity才会通知fragment B来展示此文章。在下面这个例子中,fragment A中的OnArticleSelectedListener 接口就声明了,如代码清单1-9所示:
public static class FragmentA extends ListFragment { ... // Container Activity必须实现这个接口 public interface OnArticleSelectedListener { public void onArticleSelected(Uri articleUri); } ... }
代码清单1-9
然后activity就会实现OnArticleSelectedListener接口并且重新写入onArticleSelected()方法把来自fragment A 的事件通知给fragment B。为了确保宿主activity能实现这个接口,fragment A的onAttach()回调方法(当添加fragment到activity时系统会调用的方法)会实例化一个OnArticleSelectedListener的对象并通过被计算后的Activity传入到onAttach(),如代码清单1-10所示:
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"); } } ... }
代码清单1-10
如果activity没有实现接口,那么fragment会抛出一个ClassCastException异常。如果实现了的话,mlistener成员变量会持有一个实现了OnArticleSelectedListener接口的activity引用,这样fragment A通过调用由OnArticleSelectedListener定义好的回调方法,可以与activity共享事件。例如,如果fragment A是ListFragment的一个扩展,那么每当用户点击一个列表项时,系统就会调用fragment中的onListItemClick()方法,其中fragment调用onArticleSelected()方法来与activity共享事件,如代码清单1-11所示:
public static class FragmentA extends ListFragment { OnArticleSelectedListener mListener; ... @Override public void onListItemClick(ListView l, View v, int position, long id) { // 用content provider Uri 来Append 被点击item的 row ID Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id); // Send 事件和Uri到host activity mListener.onArticleSelected(noteUri); } ... }
代码清单1-11
传递给onListItemClick()方法的Id参数是点击项的行ID,activity(或其他fragment)使用这个参数可以从应用程序的ContentProvider中获取文章。
2.向Action Bar添加条目:
你的fragment通过实现onCreateOptionsMenu()方法可以把选项菜单贡献给activity的Options Menu(和Action Bar)。然而,为了让这个方法可以接收到调用,你必须在OnCreate()方法的实现中调用setHasOptionsMenu()方法,来表明fragment想要将条目添加到Option Menu(否则,fragment不会接收到onCreateOptionsMenu()方法的调用)。
任何你要添加到fragment中的Option Menu的东西都会被追加到现有的菜单项。Fragment也会在选中一个菜单项时接收到onOptionsItemSelected()方法的回调。
你也可以通过registerForContextMenu()方法注册一个在你fragment布局里的view来提供一个context menu。当用户打开context menu时,fragment会接收到一个onCreateContextMenu()方法的调用。当用户选中一个条目时,fragment就接收到onContextItemSelected()方法的调用。
注意:尽管fragment中的每一个添加的menu都会接收到on-item-selected回调,但当用户选择一个menu选项时,activity会首先接收到相应的回调。如果activity中on-item-selected回调方法的实现没有处理选中的项,那么事件会传递给activity的回调。对于Options Menu和context menu都是如此。
1-1.6 处理fragment的生命周期
管理一个fragment的生命周期就很像管理一个activity的生命周期。与activity类似,一个fragment 也存在三种状态:
1.Resumed(已恢复):activity在运行时,fragment是可见的。
2.Paused(已暂停):当另一个activity处于屏幕前台并获取了用户焦点时,我们这个activity就处于Paused状态了,但这个仍然是运行的(处于屏幕前台的activity是半透明或没有覆盖整个屏幕)。
3.Stopped(已停止):这种状态的fragment就不再是可见的。要么是activity已经停止,要么是fragment已经从activity移除但被添加到回堆栈中。一个已停止的fragment仍然是活着的(所有的状态和成员变量的信息都被保留在系统里)。然而,fragment却不再对用户可见并且如果activity被关闭,它也会被关闭。
你也可以用Bundle来保存fragment的状态,万一activity的进程被kill掉并且当重新创建activity时你需要恢复fragment的状态。你可以在fragment的onSaveInstanceState()调用期间保存状态,在onCreate()、onCreateView()或onActivityCreated()调用期间恢复其状态。
Activity与fragment的生命周期最重要的区别是如何保存到他们各自的后台堆栈中。当activity停止时,它就会被默认放置到由系统管理的activities的后台堆栈中(所以用户可以通过Back按钮导航回它)。然而,只有当你明确的要求要在移除fragment这一操作期间,通过调用addToBackStack()方法来保存这个fragment实例时,fragment才会被放置到一个托管的后台堆栈中。
其他方面,管理fragment的生命周期与管理activity的生命周期都十分相似。所以,与管理activity生命周期相同的练习也适用于fragments。你要理解的是,activity的生命是如何影响到fragment的生命。
注意:如果在你的fragment里你需要一个Context对象,那么你可以调用getActivity()方法。然而,要注意的是,只有当fragment附加到activity上时,你才可以调用getActivity()方法。如果fragment没有附加到activity或者在生命周期完成期间被分离,getActivity将返回一个null。
1. 与activity生命周期的协调:
Fragment宿主activity的生命周期会直接影响到fragment的生命周期,以至于对每一个fragment来说,activity每一次生命周期的回调都会导致一个相似的回调。例如,当activity接收到onPause()时,activity中的每一个fragment也会收到onPause()。
然而,Fragment有几个附加的生命周期回调,他们处理与activity的独特交互,这是为了执行一些如创建和销毁fragment UI的操作。下面是这几个附加的回调方法:
1.onAttach():当fragment与activity相关时调用这个方法(Activity传递到这儿)。
2.onCreateView():这个方法的调用是为了创建与fragment相关的view层次。
3.onActivityCreated():当activity的onCreate()方法已经返回时调用。
4.onDestroyView():当要移除与fragment相关的view层次时调用这个方法。
5.onDetach():当fragment从activity中分离时调用这个方法。
下面让我们看activity生命周期在fragment生命周期上的影响,如图1-3所示:
图1-3 activity生命周期对fragment生命周期的影响
在这个图中,我们可以看出每一个activity依次的状态是如何决定fragment会接收到哪一个方法的回调。例如,当activity收到它的onCreate()回调时,那么activity中的fragment至多会接收到onActivityCreate()方法的回调。
一旦activity到了resumed状态,你就可以在activity中随意添加或移除fragment。因此,只有当activity处于resumed状态,fragment的生命周期才可以单独改变。
然而,当activity离开resumed状态,fragment会被activity又一次挤入它的下一个生命周期。
1-1.7代码示例:
下面一个例子是关于activity是如何使用两个fragments来创建一个two-pane布局。下面的activity包括一个fragment用来展示莎士比亚话剧的标题,当选中一个标题时另一个fragment用来展示话剧的摘要。它还演示如何根据屏幕配置来提供不同的fragment配置。
注意:activity中的完整代码在FragmentLayout.java中可用。
Main activity在onCreate()方法的实现中以惯常的方式来应用一个布局,如代码清单1-12所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_layout); }
代码清单1-12
应用的布局是fragment_layout.xml,如代码清单1-13所示:
<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>
代码清单1-13
一旦activity加载上面的布局,系统就会实例化TitlesFragment(显示话剧标题的fragment),然而FrameLayout(fragment显示话剧摘要的地方)会在屏幕的右边消耗资源,但最初仍然会保持是空的。正如下面你将看到的,只有当用户从列表里选择一个项时,fragment才会被放到FragmentLayout中。
然而,并不是所有的屏幕配置都有足够大的空间来同时并排展示话剧列表和摘要。所以上面的布局也只是通过在res/layout-land/fragment_layout.xml中保存它来用于横向屏幕配置。
因此,当屏幕是纵向的时,系统会应用下面这些布局,它保存在res/layout/fragment_layout.xml中,如代码清单1-14所示:
<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>
代码清单1-14
这个布局仅包含TitlesFragment。这就意味着,当设备是纵向的,那只有展示话剧标题的fragment可用。所以,当用户在这个配置点击了一个列表项时,应用程序将启动一个新的activity来展示话剧的摘要,而不是去加载第二个fragment。
下面,你将看到它是如何在fragment类中实现的。首先是TitlesFragment。它显示莎士比亚话剧的标题列表。这个fragment会延伸ListFragment并且依赖它用来处理大部分的view列表工作。
注意,当你检查这个代码时,你会发现在用户点击一个列表项时会出现两个可能的行为:这取决于两个布局中较活跃的那一个,它既可以创建和在同一activity中展示一个新的fragment来显示细节(把fragment添加到FragmentLayout),又可以启动一个新的activity(可以展示fragment的地方),如代码清单1-15所示:
public static class TitlesFragment extends ListFragment { boolean mDualPane; int mCurCheckPosition = 0; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 使用标题的静态数组来填充list setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES)); // 一个用于查看显示细节的嵌入框 View detailsFrame = getActivity().findViewById(R.id.details); mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE; if (savedInstanceState != null) { // 恢复上次状态的check位置 mCurCheckPosition = savedInstanceState.getInt("curChoice", 0); } if (mDualPane) { // 在双窗体模式下, list view会高亮被选择的item getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); // 确保我们的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 函数用来显示一个被选择的item中的细节 */ void showDetails(int index) { mCurCheckPosition = index; if (mDualPane) { // 高亮被选择的item并显示数据We can display everything in-place with fragments, so update // getListView().setItemChecked(index, true); // 检查当前被已经显示过的fragment,如果需要的话可以被替换 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); // 执行一个transaction, 在这个框中替换存在的fragment FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.replace(R.id.details, details); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); } } else { // 否则我们需要启动一个新的activity来显示被选择的文本 Intent intent = new Intent(); intent.setClass(getActivity(), DetailsActivity.class); intent.putExtra("index", index); startActivity(intent); } } }
代码清单1-15
第二个fragment:DetailsFragment显示了从TitlesFragment中选定的列表项的摘要,如代码清单1-16所示:
public static class DetailsFragment extends Fragment { /** * 创建一个新的DetailsFragment实例,在”index”初始化显示的文本 initialized to * show the text at 'index'. */ public static DetailsFragment newInstance(int index) { DetailsFragment f = new DetailsFragment(); //把输入的索引当成一个参数 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) { // 我们有不同的布局,但这些fragment的框一个都不存在。fragment 也许仍然根据它已保存的状态被创建, // 但不需要创建它的view层级,因为它不用被显示出来。因为在这里我们已经返回出去了, // 下面的代码是永远不会被执行的 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; } }
代码清单1-16
之前说到TitlesFragment类,也就是如果用户点击一个列表项和当前布局不包含R.id.details view(DetailsFragment所属的地方),那么应用程序会启动DetailsActivity来展示项的内容。
下面就是DetailesFragment,当屏幕是纵向时,它仅嵌入到DetailsFragment来展示选中话剧的摘要,如代码清单1-17所示:
public static class DetailsActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { // 如果屏幕现在是横屏模式,我们能在同一行显示list,所以不需要这个activity finish(); return; } if (savedInstanceState == null) { // 在初始化设置的期间,插入details fragment. DetailsFragment details = new DetailsFragment(); details.setArguments(getIntent().getExtras()); getFragmentManager().beginTransaction().add(android.R.id.content, details).commit(); } } }
代码清单1-17
注意,如果配置是纵向的,上面的那个activity就会结束进程,所以main activity可以接管并且会在TitlesFragment旁边展示DetailsFragment。如果用户在纵向屏幕时启动DetailsFragment,但又把设备旋转成横屏(重新启动当前activity)这种情况就会发生。
本文来自jy02432443,QQ78117253。转载请保留出处,并保留追究法律责任的权利