Android5 高级教程(三)
原文:Pro Android 5
协议:CC BY-NC-SA 4.0
八、片段
到目前为止,我们已经探索了 Android 应用的一些片段,并且您已经运行了一些为智能手机大小的屏幕定制的简单应用。您所要考虑的就是如何在屏幕上为一个活动安排 UI 控件,以及一个活动如何流向下一个活动,等等。对于 Android 的前两个主要版本,小屏幕是它。然后是安卓平板电脑:屏幕尺寸为 10 英寸的设备。这让事情变得复杂。为什么?因为现在屏幕空间太大了,以至于一个简单的活动很难在保持单一功能的同时填满整个屏幕。让一个电子邮件应用在一个活动中只显示标题(占据一个大屏幕),而在一个单独的活动中显示一封电子邮件(也占据一个大屏幕),这已经没有意义了。有了这么大的空间,应用可以在屏幕的左侧显示电子邮件标题列表,在屏幕的右侧显示选定的电子邮件内容。它能在一个布局单一的活动中完成吗?是的,但是你不能在任何小屏幕设备上重复使用这个活动或布局。
Android 3.0 中引入的核心类之一是 Fragment 类,它是专门为帮助开发人员管理应用功能而设计的,因此它将提供出色的可用性以及大量的重用。本章将向你介绍片段,它是什么,它如何适应应用的架构,以及如何使用它。片段让很多以前很难的有趣的事情成为可能。几乎在同一时间,谷歌发布了一个片段 SDK,可以在旧的 Androids 上工作。因此,即使你对编写平板电脑应用不感兴趣,你也可能会发现片段让你在非平板设备上的生活更加轻松。现在,为智能手机、平板电脑甚至电视和其他设备编写优秀的应用比以往任何时候都更容易。
让我们从 Android 片段开始。
什么是片段?
第一部分将解释什么是片段以及它的作用。但是首先,让我们来看看为什么我们需要片段。正如您之前了解到的,小屏幕设备上的 Android 应用使用活动向用户显示数据和功能,每个活动都有一个相当简单、定义明确的目的。例如,一个活动可能向用户显示他们地址簿中的联系人列表。另一个活动可能允许用户键入电子邮件。Android 应用是将这些活动组合在一起以实现更大目的的一系列活动,例如通过阅读和发送消息来管理电子邮件帐户。这对于小屏幕设备来说很好,但是当用户的屏幕非常大(10 英寸或更大)时,屏幕上的空间可以做不止一件简单的事情。应用可能希望让用户查看收件箱中的电子邮件列表,同时在列表旁边显示当前选择的电子邮件文本。或者应用可能希望显示联系人列表,同时在详细视图中显示当前选择的联系人。
作为一名 Android 开发人员,你知道这个功能可以通过用列表视图和布局以及各种其他视图为 xlarge 屏幕定义另一种布局来实现。我们所说的“另一种布局”是指除了那些你可能已经为小屏幕定义的布局之外的布局。当然,你会希望有单独的布局为纵向案件以及横向案件。对于 xlarge 屏幕的大小,这可能意味着所有标签、字段和图像等的视图非常多,您需要对其进行布局,然后提供代码。如果有一种方法可以将这些视图对象组合在一起并整合它们的逻辑,以便应用的大部分可以跨屏幕大小和设备重用,从而最大限度地减少开发人员维护应用的工作量,那该多好。这就是为什么我们有片段。
将片段视为子活动的一种方式。事实上,片段的语义很像一个活动。一个片段可以有一个与之关联的视图层次结构,并且它有一个生命周期,就像一个活动的生命周期一样。片段甚至可以像活动一样响应后退按钮。如果你在想,“要是我能在平板电脑的屏幕上同时进行多项活动就好了”,那么你就对了。但是因为在平板电脑屏幕上同时激活一个应用的多个活动太麻烦了,所以创建了片段来实现这个想法。这意味着片段包含在活动中。片段只能存在于活动的上下文中;没有活动就不能使用片段。片段可以与活动的其他元素共存,这意味着您不需要转换活动的整个用户界面来使用片段。您可以像以前一样创建一个活动的布局,并且只对用户界面的一部分使用一个片段。
然而,当涉及到保存状态和稍后恢复它时,片段不像活动。片段框架提供了几个特性,使得保存和恢复片段比您需要在活动上做的工作简单得多。
如何决定何时使用片段取决于几个考虑因素,这些将在下面讨论。
何时使用片段
使用片段的一个主要原因是,你可以跨设备和屏幕尺寸重用用户界面和功能。对于平板电脑来说尤其如此。想想当屏幕和平板电脑一样大时会发生什么。它更像一个桌面而不是电话,你的许多桌面应用都有一个多面板用户界面。如前所述,您可以在屏幕上同时拥有所选项目的列表和详细视图。这很容易在横向中描述,列表在左边,细节在右边。但是,如果用户将设备旋转到纵向模式,这样屏幕的高度就比宽度大了,会怎么样呢?也许您现在希望列表在屏幕的顶部,详细信息在底部。但是,如果这个应用运行在一个小屏幕上,屏幕上没有空间同时显示这两个部分,该怎么办呢?难道您不希望列表和细节的独立活动能够在大屏幕上共享您构建到这些部分中的逻辑吗?我们希望你的回答是肯定的。片段能帮上忙。图 8-1 让这一点更加清晰。
图 8-1 。用于平板电脑用户界面和智能手机用户界面的片段
在横向模式下,两个片段可以很好地并排放置。在肖像模式下,我们可以把一个片段放在另一个上面。但是如果我们试图在一个较小屏幕的设备上运行相同的应用,我们可能需要显示片段 1 或片段 2,但不能同时显示两者。如果我们试图用布局来管理所有这些场景,我们会创建相当多的布局,这意味着很难在许多不同的布局中保持一切正确。当使用片段时,我们的布局保持简单;每个活动布局将片段作为容器来处理,并且活动布局不需要指定每个片段的内部结构。每个片段都有自己的内部结构布局,并且可以在许多配置中重用。
让我们回到旋转方向的例子。如果您必须为活动的方向更改编写代码,您就会知道保存活动的当前状态并在活动被重新创建后恢复状态是一件非常痛苦的事情。如果您的活动有块可以在方向改变时容易地保留,这样您就可以避免每次方向改变时的所有拆除和重新创建,这不是很好吗?当然会。片段能帮上忙。
现在想象一个用户在你的活动中,他们已经做了一些工作。想象一下,在同一个活动中,用户界面发生了变化,用户想要后退一步、两步或三步。在旧式活动中,按下 Back 按钮将使用户完全退出活动。使用片段,Back 按钮可以在一堆片段中后退,同时保持在当前活动中。
接下来,当一大块内容改变时,考虑活动的用户界面;你想让过渡看起来平滑,就像一个完美的应用。片段也能做到。
现在你对什么是片段以及为什么要使用片段有了一些概念,让我们更深入地研究一下片段的结构。
片段的结构
如前所述,片段就像一个子活动:它有一个相当具体的目的,并且几乎总是显示一个用户界面。但是在活动从上下文被子类化的地方,片段从包 android.app 中的对象被扩展。片段是而不是活动的扩展。然而,像活动一样,您将总是扩展片段(或者它的一个子类),以便您可以覆盖它的行为。
一个片段可以有一个视图层次结构来吸引用户。这个视图层次结构与任何其他视图层次结构相似,因为它可以从 XML layout 规范创建(展开),也可以用代码创建。如果要让用户看到视图层次结构,就需要将它附加到周围活动的视图层次结构上,我们很快就会看到。组成片段视图层次结构的视图对象与 Android 中其他地方使用的视图类型相同。所以你所知道的关于视图的一切也适用于片段。
除了视图层次之外,一个片段还有一个 bundle 作为它的初始化参数。类似于一个活动,一个片段可以被保存,以后由系统自动恢复。当系统恢复一个片段时,它调用默认的构造函数(不带参数),然后将这个参数包恢复到新创建的片段。片段上的后续回调可以访问这些参数,并可以使用它们将片段恢复到以前的状态。为此,您必须
- 确保片段类有一个默认的构造函数。
- 创建一个新的片段后,立即添加一组参数,以便这些后续方法可以正确地设置您的片段,并且系统可以在必要时正确地恢复您的片段。
一个活动可以同时拥有多个片段;并且如果一个片段已经被另一个片段换出,则片段交换事务可以被保存在后栈上。后台堆栈由绑定到活动的片段管理器管理。back 堆栈是管理 Back 按钮行为的方式。片段管理器将在本章后面讨论。这里您需要知道的是,一个片段知道它被绑定到哪个活动,并且从那里它可以到达它的片段管理器。片段也可以通过它的活动获得活动的资源。
同样类似于活动,当片段被重新创建时,片段可以将状态保存到 bundle 对象中,并且这个 bundle 对象被返回给片段的 onCreate() 回调。这个保存的包也被传递给 onInflate() 、 onCreateView() 和 onActivityCreated() 。请注意,这与作为初始化参数附加的包不同。在这个包中,您可能会存储片段的当前状态,而不是应该用来初始化它的值。
片段的生命周期
在开始在示例应用中使用片段之前,您需要了解片段的生命周期。为什么?片段的生命周期比活动的生命周期更复杂,理解什么时候可以用片段做事情非常重要。图 8-2 显示了一个片段的生命周期。
图 8-2 。片段的生命周期
如果你将它与图 2-3(一个活动的生命周期)相比较,你会注意到几个不同之处,主要是由于活动和片段之间需要的交互。片段非常依赖于它所在的活动,当它的活动经历一个步骤时,它可以经历多个步骤。
在最开始,一个片段被实例化。它现在作为一个对象存在于内存中。第一件可能发生的事情是初始化参数将被添加到片段对象中。在系统从一个保存的状态重新创建你的片段的情况下,这肯定是正确的。当系统从保存的状态中恢复一个片段时,默认的构造函数被调用,随后是初始化参数包的附件。如果你在代码中创建片段,一个很好的使用模式是清单 8-1 中的模式,它显示了在 MyFragment 类定义中实例化器的工厂类型。
清单 8-1 。使用静态工厂方法实例化片段
public static MyFragment newInstance(int index) {MyFragment f = new MyFragment();Bundle args = new Bundle();args.putInt("index", index);f.setArguments(args);return f;
}
从客户端的角度来看,他们通过用单个参数调用静态 newInstance() 方法来获得一个新实例。它们取回实例化的对象,并且在 arguments 包中的这个片段上设置了初始化参数。如果这个片段被保存并在以后重新构造,系统将经历一个非常相似的过程,调用默认的构造函数,然后重新附加初始化参数。对于您的特殊情况,您将定义您的 newInstance() 方法(或方法)的签名,以获取适当数量和类型的参数,然后适当地构建参数包。这就是您想要您的 newInstance() 方法做的所有事情。随后的回调将负责您的片段的其余设置。
onInflate()回调
接下来发生的是布局视图膨胀。如果您的片段由布局中的 <片段> 标签定义,您的片段的 onInflate() 回调将被调用。这将传入一个对周围活动的引用、一个带有来自 <片段> 标签的属性的属性集和一个保存的包。保存的包中有保存的状态值,如果这个片段以前存在并且正在被重新创建,则由 onSaveInstanceState() 放在那里。 onInflate() 的期望是读取属性值并保存它们以备后用。在这个阶段,对用户界面做任何事情都为时过早。该片段甚至还没有与其活动相关联。但那是你的片段的下一个事件。
onAttach()回调
在您的片段与其活动相关联之后,调用 onAttach() 回调。如果您想使用活动参考,它会传递给您。您至少可以使用活动来确定有关封闭活动的信息。您还可以使用活动作为上下文来执行其他操作。需要注意的一点是,片段类有一个 getActivity() 方法,如果你需要的话,它总是会为你的片段返回附加的活动。请记住,在整个生命周期中,可以从片段的 getArguments() 方法中获得初始化参数包。但是,一旦片段被附加到它的 activity,就不能再调用 setArguments() 了。因此,除了在最开始的时候,你不能添加初始化参数。
onCreate()回调
接下来是 onCreate() 回调。虽然这与活动的 onCreate() 相似,但是不同之处在于,您不应该在这里放置依赖于活动视图层次结构的代码。您的片段现在可能已经关联到它的活动了,但是您还没有得到通知,活动的 onCreate() 已经完成。这就来了。这个回调获取传入的保存的状态包(如果有的话)。这个回调尽可能早地创建一个后台线程来获取这个片段需要的数据。您的片段代码正在 UI 线程上运行,并且您不想在 UI 线程上进行磁盘输入/输出(I/O)或网络访问。事实上,启动一个后台线程来做好准备是很有意义的。你的后台线程应该在阻塞调用的地方。稍后您将需要与数据挂钩,可能使用处理器或其他技术。
注意在后台线程中加载数据的方法之一是使用加载器类。这将在第二十八章中讲述。
onCreateView()回调
下一个回调是 onCreateView() 。这里的期望是你将为这个片段返回一个视图层次结构。传递给这个回调函数的参数包括一个 LayoutInflater (您可以用它来扩展这个片段的布局)、一个 ViewGroup 父对象(在清单 8-2 中称为容器)和一个保存的包(如果存在的话)。注意不要将视图层次附加到传入的视图组父视图,这一点非常重要。这种关联将在以后自动发生。如果您在这个回调中将片段的视图层次结构附加到父级,您很可能会得到异常——或者至少是奇怪和意外的应用行为。
清单 8-2 。在 onCreateView() 中创建片段视图层次结构
@Override
public View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) {if(container == null)return null;View v = inflater.inflate(R.layout.details, container, false);TextView text1 = (TextView) v.findViewById(R.id.text1);text1.setText(myDataSet[ getPosition() ] );return v;
}
提供了父类,因此您可以将它与 LayoutInflater 的 inflate() 方法一起使用。如果父容器值为 null,这意味着这个特定的片段不会被查看,因为没有视图层次结构可供它附加。在这种情况下,您可以简单地从这里返回 null。请记住,在您的应用中可能有一些没有显示出来的片段。清单 8-2 展示了你可能想在这个方法中做什么的一个例子。
在这里,您可以看到如何访问这个片段的一个布局 XML 文件,并将其展开为一个视图,然后返回给调用者。这种方法有几个优点。您总是可以用代码构建视图层次结构,但是通过膨胀一个布局 XML 文件,您可以利用系统的资源查找逻辑。根据设备的配置,或者您使用的设备,将选择适当的布局 XML 文件。然后,您可以访问布局中的特定视图——在本例中为 text1 TextView 字段——来执行您想要的操作。重复非常重要的一点:不要在这个回调中将片段的视图附加到容器父级。你可以在清单 8-2 中看到,你在对 inflate() 的调用中使用了一个容器,但是你也为 attachToRoot 参数传递了 false 。
onViewCreated()回调
这个在 onCreateView() 之后调用,但是在任何保存的状态被放入 UI 之前。传入的视图对象与从 onCreateView() 返回的视图对象相同。
onActivityCreated()回调
您现在已经接近用户可以与您的片段进行交互的点了。下一个回调是 onActivityCreated() 。这是在活动完成其 onCreate() 回调之后调用的。现在,您可以相信活动的视图层次结构(包括您自己的视图层次结构,如果您之前返回了一个视图层次结构的话)已经准备好并且可用了。在用户看到之前,您可以在这里对用户界面进行最后的调整。您还可以在这里确定该活动的任何其他片段是否已经附加到您的活动。
onViewStateRestored()回调
这是一个相对较新的版本,是在 JellyBean 4.2 中引入的。当这个片段的视图层次结构恢复了所有状态(如果适用)时,您的片段将调用这个回调。以前,您必须在 onActivityCreated() 中做出关于调整 UI 以恢复片段的决定。现在,您可以将该逻辑放入回调中,明确知道这个片段正在从保存的状态中恢复。
onStart()回调
片段生命周期中的下一个回调是 onStart() 。现在用户可以看到您的片段了。但是你还没有开始与用户互动。这个回调被绑定到活动的 onStart() 。因此,以前您可能将您的逻辑放在活动的 onStart() 中,现在您更可能将您的逻辑放在片段的 onStart() 中,因为这也是用户界面组件所在的位置。
onResume()回调
用户可以与您的片段交互之前的最后一个回调是 onResume() 。这个回调被绑定到活动的 onResume() 。当这个回调返回时,用户可以自由地与这个片段进行交互。例如,如果您的片段中有一个相机预览,您可能会在片段的 onResume() 中启用它。
所以现在你已经达到了应用忙于让用户开心的程度。然后用户决定退出你的应用,要么退出,要么按 Home 键,要么启动其他应用。下一个序列,类似于活动中发生的,与为交互设置片段的方向相反。
onPause()回调
片段上的第一个撤销回调是 onPause() 。这个回调被绑定到活动的 on pause();就像一个活动一样,如果您的片段或其他共享对象中有一个媒体播放器,您可以通过您的 onPause() 方法暂停、停止或返回它。同样的好公民规则也适用于此:如果用户正在接电话,你不希望播放音频。
onSaveInstanceState()回调
与活动类似,片段有机会保存状态以便以后重建。这个回调函数为这个片段传入一个 Bundle 对象,作为您想要保存的任何状态信息的容器。这是传递给前面提到的回调的保存状态包。为了防止内存问题,要小心保存到这个包中的内容。只保存你需要的。如果您需要保存对另一个片段的引用,不要试图保存或放置另一个片段,而是保存另一个片段的标识符,比如它的标签或 ID。当这个片段运行 onViewStateRestored() 时,您可以重新建立到这个片段所依赖的其他片段的连接。
虽然您可能会看到这个方法通常在 onPause() 之后被调用,但是这个片段所属的活动在认为片段的状态应该被保存时会调用它。这可能发生在 ondestory()之前的任何时候。
onStop()回调
下一个撤销回调是 onStop() 。这个与活动的 onStop() 绑定在一起,其作用类似于活动的 onStop() 。已经停止的片段可以直接返回到 onStart() 回调,然后导致 onResume() 。
onDestroyView()回调
如果你的片段正在被删除或保存,撤销方向的下一个回调是 onDestroyView() 。这将在您之前在 onCreateView() 回调中创建的视图层次结构从您的片段中分离出来之后被调用。
onDestroy()回调
接下来是 onDestroy() 。当片段不再被使用时,就调用这个函数。请注意,它仍然连接到活动,仍然可以找到,但它不能做太多。
onDetach()回调
片段生命周期中的最后一次回调是 onDetach() 。一旦这个被调用,这个片段就不再绑定到它的活动,它不再有视图层次结构,并且它的所有资源都应该被释放。
使用 setRetainInstance()
你可能已经注意到了图 8-2 中的虚线。片段的一个很酷的特性是,如果活动被重新创建,您可以指定不希望片段被完全销毁,因此您的片段也将被恢复。所以,片段自带了一个叫 setRetainInstance() 的方法,用一个布尔参数告诉它“Yes 我希望你在我的活动重新开始时留下来”或“不;走开,我会从头创建一个新的片段。”调用 setRetainInstance() 的一个好地方是在片段的 onCreate() 回调中,但是在 onCreateView() 中工作,就像 onActivityCreated() 一样。
如果参数为真,这意味着您希望将片段对象保存在内存中,而不是从头开始。但是,如果您的活动正在离开并被重新创建,您必须将您的片段从这个活动中分离出来,并将其附加到新的活动中。底线是,如果保留实例值为 true ,您实际上不会销毁您的片段实例,因此您不需要在另一端创建一个新的。图中的虚线表示您将在退出时跳过 onDestroy() 回调,当您的片段被重新附加到您的新活动时,您将跳过 onCreate() 回调,所有其他回调都将被触发。因为活动很可能是因为配置更改而重新创建的,所以您的片段回调应该假设配置已经更改,因此应该采取适当的操作。例如,这将包括扩展布局以在 onCreateView() 中创建新的视图层次。清单 8-2 中提供的代码会在编写时处理好这个问题。如果您选择使用 retain-instance 特性,您可能会决定不将一些初始化逻辑放在 onCreate() 中,因为它不会像其他回调那样总是被调用。
展示生命周期的示例片段应用
没有什么比看到一个真实的例子更能理解一个概念了。您将使用一个经过测试的示例应用,这样您就可以看到所有这些回调的运行情况。您将使用一个示例应用,它在一个片段中使用了一系列莎士比亚的标题;当用户点击其中一个标题时,该剧的一些文本将出现在一个单独的片段中。这个示例应用可以在平板电脑上以横向和纵向模式运行。然后将它配置为在较小的屏幕上运行,这样您就可以看到如何将文本片段分成一个活动。您将从清单 8-3 中的横向模式下活动的 XML 布局开始,当它运行时,看起来像图 8-3 中的。
清单 8-3 。用于横向模式的活动布局 XML
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout-land/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="match_parent"><fragment class="com.androidbook.fragments.bard.TitlesFragment"android:id="@+id/titles" android:layout_weight="1"android:layout_width="0px"android:layout_height="match_parent" /><FrameLayoutandroid:id="@+id/details" android:layout_weight="2"android:layout_width="0px"android:layout_height="match_parent" /></LinearLayout>
图 8-3 。您的示例片段应用的用户界面
注意:本章末尾的是您可以用来下载本章中的项目的 URL。这将允许您将这些项目直接导入到您的 IDE(比如 Eclipse 或 Android Studio)中。
这种布局看起来像你在整本书中看到的许多其他布局,水平地从左到右有两个主要对象。不过有一个特殊的新标签,叫做 <片段> ,这个标签有一个新属性叫做类。请记住,片段不是视图,因此片段的布局 XML 与其他所有内容的布局稍有不同。另一件要记住的事情是, <片段> 标签只是这个布局中的一个占位符。在布局 XML 文件中,不应将子标签放在 <片段> 下。
片段的其他属性看起来很熟悉,其用途类似于视图。片段标签的类属性为应用的标题指定了扩展类。也就是说,你必须扩展一个 Android 片段类来实现你的逻辑, <片段> 标签必须知道你扩展类的名字。一个片段有它自己的视图层次结构,这个层次结构将由片段自己创建。下一个标签是一个框架布局—而不是另一个 <片段> 标签。为什么会这样?我们稍后会更详细地解释,但是现在,你应该意识到你将要在文本上做一些转换,用一个片段替换另一个片段。您使用 FrameLayout 作为视图容器来保存当前的文本片段。对于标题片段,您有一个——也是唯一一个——需要担心的片段:没有交换和过渡。对于显示莎士比亚文本的区域,您将有几个片段。
MainActivity Java 代码在清单 8-4 中。实际上,清单只显示了有趣的代码。该代码带有日志消息,因此您可以通过 LogCat 看到发生了什么。请查看从网站上下载的莎士比亚乐器的源代码文件。
清单 8-4 。来自 MainActivity 的有趣源代码
public boolean isMultiPane() {return getResources().getConfiguration().orientation== Configuration.ORIENTATION_LANDSCAPE;
}/*** 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.*/
public void showDetails(int index) {Log.v(TAG, "in MainActivity showDetails(" + index + ")");if (isMultiPane()) {// Check what fragment is 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.Log.v(TAG, "about to run FragmentTransaction...");FragmentTransaction ft= getFragmentManager().beginTransaction();ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);//ft.addToBackStack("details");ft.replace(R.id.details, details);ft.commit();}} else {// Otherwise you need to launch a new activity to display// the dialog fragment with selected text.Intent intent = new Intent();intent.setClass(this, DetailsActivity.class);intent.putExtra("index", index);startActivity(intent);}
}
这是一个非常简单的活动。要确定 multipane 模式(即是否需要并排使用片段),只需使用设备的方向。如果你在风景模式下,你是多窗格;如果你在肖像模式下,你不是。helper 方法 showDetails() 是用来计算当标题被选中时如何显示文本的。索引是标题在标题列表中的位置。如果您处于多窗格模式,您将使用一个片段来显示文本。您将这个片段称为 DetailsFragment ,并使用工厂类型的方法创建一个带有索引的片段。 DetailsFragment 类的有趣代码如清单 8-5 所示(减去所有的日志代码)。正如我们之前在 TitlesFragment 中所做的那样, DetailsFragment 的各种回调都添加了日志记录,因此我们可以通过 LogCat 观察发生了什么。稍后您将回到您的 showDetails() 方法。
清单 8-5 。细节片段的源代码
public class DetailsFragment extends Fragment {private int mIndex = 0;public static DetailsFragment newInstance(int index) {Log.v(MainActivity.TAG, "in DetailsFragment newInstance(" +index + ")");DetailsFragment df = new DetailsFragment();// Supply index input as an argument.Bundle args = new Bundle();args.putInt("index", index);df.setArguments(args);return df;}public static DetailsFragment newInstance(Bundle bundle) {int index = bundle.getInt("index", 0);return newInstance(index);}@Overridepublic void onCreate(Bundle myBundle) {Log.v(MainActivity.TAG,"in DetailsFragment onCreate. Bundle contains:");if(myBundle != null) {for(String key : myBundle.keySet()) {Log.v(MainActivity.TAG, " " + key);}}else {Log.v(MainActivity.TAG, " myBundle is null");}super.onCreate(myBundle);mIndex = getArguments().getInt("index", 0);}public int getShownIndex() {return mIndex;}@Overridepublic View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) {Log.v(MainActivity.TAG,"in DetailsFragment onCreateView. container = " +container);// Don't tie this fragment to anything through the inflater.// Android takes care of attaching fragments for us. The// container is only passed in so you can know about the// container where this View hierarchy is going to go.View v = inflater.inflate(R.layout.details, container, false);TextView text1 = (TextView) v.findViewById(R.id.text1);text1.setText(Shakespeare.DIALOGUE[ mIndex ] );return v;}
}
DetailsFragment 类实际上也相当简单。现在您可以看到如何实例化这个片段。需要指出的是,您正在代码中实例化这个片段,因为您的布局定义了您的细节片段将要进入的视图组容器(一个框架布局)。因为片段本身不是在活动的布局 XML 中定义的,与 titles 片段不同,您需要在代码中实例化您的 details 片段。
要创建一个新的细节片段,您可以使用您的 newInstance() 方法。如前所述,这个工厂方法调用默认构造函数,然后用索引的值设置参数包。一旦 newInstance() 已经运行,您的细节片段就可以通过 getArguments() 引用参数包,在它的任何回调中检索索引的值。为了方便起见,在 onCreate() 中,您可以将 arguments 包中的索引值保存到 DetailsFragment 类的成员字段中。
您可能想知道为什么不简单地在 newInstance() 中设置 mIndex 值。原因是 Android 将在幕后使用默认的构造函数重新创建您的片段。然后,它将参数包设置为之前的样子。Android 不会使用您的 newInstance() 方法,因此确保设置 mIndex 的唯一可靠方法是从 arguments bundle 中读取值,并在 onCreate() 中设置它。便利的方法 getShownIndex() 检索该索引的值。现在细节片段中剩下的唯一要描述的方法是 onCreateView() 。这也很简单。
onCreateView() 的目的是返回片段的视图层次结构。请记住,基于您的配置,您可能希望这个片段有各种不同的布局。因此,最常见的做法是为您的片段使用一个布局 XML 文件。在您的示例应用中,您使用资源 R.layout.details 将片段的布局指定为 details.xml 。 details.xml 的 XML 在清单 8-6 中。
清单 8-6 。细节片段的 details.xml 布局文件
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout/details.xml -->
<LinearLayoutxmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"android:layout_width="match_parent"android:layout_height="match_parent"><ScrollView android:id="@+id/scroller"android:layout_width="match_parent"android:layout_height="match_parent"><TextView android:id="@+id/text1"android:layout_width="match_parent"android:layout_height="match_parent" /></ScrollView>
</LinearLayout>
对于您的示例应用,无论是在横向模式还是纵向模式下,您都可以使用完全相同的布局文件来获取详细信息。这个布局不是为活动设计的,它只是让你的片段显示文本。因为它可以被认为是默认布局,所以你可以将它存储在 /res/layout 目录中,即使你处于横向模式,也可以找到并使用它。当 Android 寻找细节 XML 文件时,它会尝试与设备配置紧密匹配的特定目录,但如果它在任何其他地方都找不到 details.xml 文件,它将最终出现在 /res/layout 目录中。当然,如果你想让你的片段在风景模式下有一个不同的布局,你可以定义一个单独的 details.xml 布局文件,并把它保存在 /res/layout-land 下。随意试验不同的 details.xml 文件。
当您的细节片段的 onCreateView() 被调用时,您只需获取适当的 details.xml 布局文件,对其进行膨胀,并将文本设置为来自 Shakespeare 类的文本。莎士比亚的全部 Java 代码在这里没有显示,但是有一部分在清单 8-7 中,所以你可以理解它是如何完成的。如需完整的源代码,请访问项目下载文件,如本章末尾的“参考资料”部分所述。
清单 8-7 。Shakespeare.java 的源代码
public class Shakespeare {public static String TITLES[] = {"Henry IV (1)","Henry V","Henry VIII","Romeo and Juliet","Hamlet","The Merchant of Venice","Othello"};public static String DIALOGUE[] = {"So shaken as we are, so wan with care,\n...
*... and so on ...*
现在,您的细节片段视图层次结构包含了所选标题的文本。你的细节片段已经准备好了。并且可以返回 MainActivity 的 showDetails() 方法来谈 FragmentTransactions 。
FragmentTransactions 和片段回栈
showDetails() 中的代码获取新的细节片段(在清单 8-8 中再次部分显示)看起来相当简单,但是这里有很多事情要做。花一些时间来解释正在发生的事情及其原因是值得的。如果您的活动处于 multipane 模式,您希望在标题列表旁边的片段中显示详细信息。您可能已经显示了细节,这意味着您可能有一个对用户可见的细节片段。不管怎样,资源 ID R.id.details 是为您的活动的框架布局准备的,如清单 8-3 所示。如果布局中有一个细节片段,因为您没有为它分配任何其他 ID,那么它将拥有这个 ID。因此,要找出布局中是否有细节片段,可以使用 findFragmentById()询问片段管理器。如果框架布局为空,这将返回 null,或者将给出当前的细节片段。然后,您可以决定是否需要在布局中放置一个新的细节片段,因为布局是空的,或者因为有其他标题的细节片段。一旦您决定创建并使用一个新的细节片段,您就可以调用工厂方法来创建一个细节片段的新实例。现在,您可以将这个新的片段放到用户可以看到的地方。
清单 8-8 。片段事务示例
public void showDetails(int index) {Log.v(TAG, "in MainActivity showDetails(" + index + ")");if (isMultiPane()) {// Check what fragment is 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.Log.v(TAG, "about to run FragmentTransaction...");FragmentTransaction ft= getFragmentManager().beginTransaction();ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);//ft.addToBackStack("details");ft.replace(R.id.details, details);ft.commit();}// The rest was left out to save space.
}
需要理解的一个关键概念是片段必须位于视图容器中,也称为视图组。视图组类包括布局和它们的派生类。 FrameLayout 是一个很好的选择,可以作为活动的 main.xml 布局文件中的细节片段的容器。一个框架布局很简单,你所需要的只是一个简单的容器来存放你的片段,没有其他类型布局带来的额外负担。框架布局是你的细节片段将要去的地方。如果您在活动的布局文件中指定了另一个 <片段> 标签,而不是一个框架布局 ,您将无法用一个新片段替换当前片段(即交换片段)。
FragmentTransaction 是你用来做交换的。您告诉片段事务,您希望用新的细节片段替换框架布局中的任何内容。你可以通过定位细节文本视图的资源 ID 并设置它的文本为新的莎士比亚标题的新文本来避免这一切。但是片段的另一面解释了为什么你使用片段事务。
正如您所知道的,活动被安排在一个堆栈中,随着您越来越深入地了解一个应用,同时进行几个活动的堆栈并不罕见。当您按下返回按钮时,最上面的活动消失,您将返回到下面的活动,该活动将为您恢复。这可以持续到你再次回到主屏幕。
当一个活动只有一个目的时,这很好,但现在一个活动可以同时有几个片段,因为你可以在不离开最上面的活动的情况下更深入地进入你的应用,Android 真的需要扩展后退按钮栈的概念,以包括片段。事实上,片段更需要这一点。当在一个活动中有几个片段同时相互交互,并且有一次跨越几个片段到新内容的转换时,按 Back 按钮应该会导致每个片段一起回滚一步*。为了确保每个片段正确地参与回滚,创建了一个 FragmentTransaction 并对其进行管理以执行该协调。*
*请注意,在活动中不需要片段的后台堆栈。您可以对应用进行编码,让 Back 按钮在活动级别工作,而不是在片段级别工作。如果你的片段没有后台栈,按 back 按钮会弹出当前的活动栈,让用户回到底层。如果你选择利用片段的后栈,你将需要在清单 8-8 中取消注释 ft . addtobackstack(" details ")这一行。对于这个特殊的例子,您已经将标记参数硬编码为字符串“细节”。这个标记应该是一个适当的字符串名称,代表事务时片段的状态。标签不一定是特定片段的名称,而是片段事务和事务中所有片段的名称。您将能够使用标记值在代码中查询后台堆栈来删除条目,以及弹出条目。您将希望这些交易上有意义的标记能够在以后找到合适的交易。
片段事务转换和动画
片段事务的一个非常好的地方是,您可以使用转换和动画来执行从旧片段到新片段的转换。这些不像后面的动画,在第十八章。这些要简单得多,不需要深入的图形知识。当您用新的细节片段替换旧的细节片段时,让我们使用片段事务转换来添加特殊效果。这可以为您的应用增添光彩,使从旧片段到新片段的切换看起来更加平滑。
完成这个的一个方法是 setTransition() ,如清单 8-8 所示。但是,有一些不同的转换可用。您在示例中使用了淡入淡出,但是您也可以使用 setCustomAnimations() 方法来描述其他特殊效果,例如当一个片段从左边滑入时,将另一个片段滑入右边。自定义动画使用新的对象动画定义,而不是旧的定义。旧的动画 XML 文件使用标签如 <翻译> ,而新的 XML 文件使用 <对象动画> 。旧的标准 XML 文件位于适当的 Android SDK 平台目录下的 /data/res/anim 目录中(比如蜂巢的平台/android-11 )。这里的 /data/res/animator 目录中也有一些新的 XML 文件。您的代码可能类似于
ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
这将导致新片段在旧片段淡出时淡入。第一个参数适用于进入的片段,第二个参数适用于退出的片段。请随意浏览 Android animator 目录以获取更多的库存动画。如果你想创建你自己的,在第十八章中有关于物体动画的章节可以帮助你。您需要的另一个非常重要的知识是,转换调用需要在替换()调用之前进行;否则,它们将不起作用。
使用 object animator 对片段进行特效处理是一种有趣的过渡方式。关于 FragmentTransaction 还有两个方法你应该知道: hide() 和 show() 。这两种方法都将一个片段作为参数,它们完全按照您的预期工作。对于与视图容器相关联的片段管理器中的片段,这些方法只是在用户界面中隐藏或显示片段。在这个过程中,片段不会从片段管理器中删除,但是为了影响它的可见性,它肯定会被绑定到一个视图容器中。如果一个片段没有视图层次,或者如果它的视图层次没有绑定到显示的视图层次,那么这些方法不会做任何事情。
一旦你为你的片段事务指定了特效,你必须告诉它你想要完成的主要工作。在您的例子中,您用新的细节片段替换了框架布局中的任何内容。这就是 replace() 方法的用武之地。这相当于为已经在框架布局中的任何片段调用 remove() ,然后为新的细节片段调用 add() ,这意味着您可以根据需要调用 remove() 或 add() 。
使用片段事务时,您必须采取的最后一个动作是提交它。 commit() 方法不会让事情立即发生,而是将工作安排在 UI 线程准备好的时候。
现在你应该明白了,为什么要大费周章的去改变一个简单片段里的内容。不仅仅是你想改文字;在过渡过程中,您可能需要特殊的图形效果。您可能还希望将转换详细信息保存在一个片段事务中,以便以后可以撤销。最后一点可能会令人困惑,所以我们要澄清一下。
这不是真正意义上的交易。当您从后台堆栈中弹出片段事务时,您并没有撤消可能已经发生的所有数据更改。如果活动中的数据发生了更改,例如,在后台堆栈上创建片段事务时,按 back 按钮不会导致活动数据更改恢复到以前的值。您仅仅是按照您进来的方式在用户界面视图中后退,就像您处理活动一样,但是在这种情况下,它是针对片段的。由于保存和恢复片段的方式,从保存状态恢复的片段的内部状态将取决于与片段一起保存的值以及如何恢复它们。因此,您的片段可能看起来与以前一样,但您的活动不会,除非您在恢复片段时采取措施恢复活动状态。
在您的示例中,您只使用了一个视图容器,并引入了一个细节片段。如果您的用户界面更复杂,您可以在片段事务中操作其他片段。您实际做的是开始事务,用新的细节片段替换细节框架布局中的任何现有片段,指定淡入动画,并提交事务。您注释掉了将该事务添加到后台堆栈的部分,但是您当然可以取消对它的注释以加入后台堆栈。
片段管理器
FragmentManager 是一个组件,负责管理属于活动的片段。这包括后面堆栈上的片段和可能只是挂在周围的片段。我们会解释的。
片段应该只在活动的上下文中创建。这可以通过扩展活动的布局 XML 来实现,也可以通过使用类似于清单 8-1 中的代码直接实例化来实现。当通过代码实例化时,片段通常使用片段事务附加到活动。在这两种情况下, FragmentManager 类用于访问和管理活动的这些片段。
您可以在活动或附加片段上使用 getFragmentManager() 方法来检索片段管理器。您在清单 8-8 中看到,片段管理器是获取片段事务的地方。除了获取片段事务之外,您还可以使用片段的 ID、标签或者包和键的组合来获取片段。如果片段是从 XML 中膨胀出来的,那么片段的 ID 将是片段的资源 ID,如果片段是使用片段事务放入视图中的,那么它将是容器的资源 ID。片段的标签是一个字符串,可以在片段的 XML 定义中分配,或者在片段通过片段事务被放置在视图中时分配。检索片段的 bundle 和 key 方法只适用于使用 putFragment() 方法持久化的片段。
获取一个片段,getter 方法有 findFragmentById() 、 findFragmentByTag() 和 getFragment() 。 getFragment() 方法将与 putFragment() 结合使用,后者也接受一个包、一个密钥和要放置的片段。这个包很可能是 savedState 包,而 putFragment() 将在 onSaveInstanceState() 回调中使用,以保存当前活动(或另一个片段)的状态。 getFragment() 方法可能会在 onCreate() 中被调用,以对应于 putFragment() ,尽管对于一个片段来说,这个包可用于其他回调方法,如前所述。
显然,您不能在还没有附加到活动的片段上使用 getFragmentManager() 方法。但是,您也可以将一个片段附加到一个活动,而不使它对用户可见。如果你这样做,你应该关联一个字符串标签到这个片段,这样你就可以在将来访问它。您最有可能使用这种 FragmentTransaction 方法来实现:
public FragmentTransaction add (Fragment fragment, String tag)
事实上,您可以拥有一个不展示视图层次结构的片段。这样做可能是为了将某些逻辑封装在一起,这样它就可以附加到一个活动中,但仍然保留一些来自活动生命周期和其他片段的自主权。当一个活动由于设备配置的改变而经历一个重新创建的周期时,这个非 UI 片段可能在活动离开并再次回来的时候基本上保持不变。对于 setRetainInstance() 选项来说,这是一个很好的选择。
片段后栈也是片段管理器的领域。片段事务用于将片段放入后台堆栈,而片段管理器可以从后台堆栈中取出片段。这通常是使用片段的 ID 或标签来完成的,但也可以基于在后台堆栈中的位置来完成,或者只是弹出最顶层的片段。
最后,片段管理器有一些调试特性的方法,比如使用 enableDebugLogging() 打开调试消息到 LogCat,或者使用 dump() 将片段管理器的当前状态转储到流中。请注意,您在清单 8-4 中的活动的 onCreate() 方法中打开了片段管理器调试。
引用片段时要小心
是时候重温一下之前关于片段生命周期、参数和保存状态包的讨论了。Android 可以在不同的时间保存你的一个片段。这意味着当您的应用想要检索该片段时,它可能不在内存中。出于这个原因,我们提醒你不要认为一个片段的变量引用将在很长一段时间内保持有效。如果使用片段事务在容器视图中替换片段,任何对旧片段的引用现在都指向可能在后台堆栈上的片段。或者,在应用配置更改(如屏幕旋转)期间,片段可能会脱离活动的视图层次结构。小心点。
如果你要保留一个对片段的引用,要知道它什么时候可以被保存;当您需要再次找到它时,使用片段管理器的 getter 方法之一。如果您想保留一个片段引用,比如当一个活动正在经历一个配置变更时,您可以使用带有适当包的 putFragment() 方法。在活动和片段的情况下,合适的包是在 onSaveInstanceState() 中使用的 savedState 包,它在 onCreate() 中重新出现(或者,在片段的情况下,是片段生命周期的其他早期回调)。您可能永远不会将直接的片段引用存储到片段的参数包中;如果你想这么做,请先仔细考虑一下。
获得特定片段的另一种方法是使用已知的标签或 ID 查询它。前面描述的 getter 方法将允许以这种方式从片段管理器中检索片段,这意味着您可以选择只记住片段的标签或 ID,以便您可以使用这些值之一从片段管理器中检索它,而不是使用 putFragment() 和 getFragment() 。
保存片段状态
Android 3.2 引入了另一个有趣的类:片段。保存的状态。使用 FragmentManager 的 saveFragmentInstanceState()方法,你可以给这个方法传递一个片段,它会返回一个表示该片段状态的对象。然后,您可以在初始化片段时使用该对象,使用片段的 setInitialSavedState() 方法。第九章对此进行了更详细的讨论。
列表片段和
为了使您的示例应用完整,还需要做一些事情。第一个是 TitlesFragment 类。这是通过主活动的 main.xml 文件创建的。 <片段> 标签作为该片段的占位符,并不定义该片段的视图层次结构。你的 TitlesFragment 的有趣代码在清单 8-9 中。对于所有的代码,请参考源代码文件。标题片段显示应用的标题列表。
清单 8-9 。 TitlesFragment Java 代码
public class TitlesFragment extends ListFragment {private MainActivity myActivity = null;int mCurCheckPosition = 0;@Overridepublic void onAttach(Activity myActivity) {Log.v(MainActivity.TAG,"in TitlesFragment onAttach; activity is: " + myActivity);super.onAttach(myActivity);this.myActivity = (MainActivity)myActivity;}@Overridepublic void onActivityCreated(Bundle savedState) {Log.v(MainActivity.TAG,"in TitlesFragment onActivityCreated. savedState contains:");if(savedState != null) {for(String key : savedState.keySet()) {Log.v(MainActivity.TAG, " " + key);}}else {Log.v(MainActivity.TAG, " savedState is null");}super.onActivityCreated(savedState);// Populate list with your static array of titles.setListAdapter(new ArrayAdapter<String>(getActivity(),android.R.layout.simple_list_item_1,Shakespeare.TITLES));if (savedState != null) {// Restore last state for checked position.mCurCheckPosition = savedState.getInt("curChoice", 0);}// Get your ListFragment's ListView and update itListView lv = getListView();lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);lv.setSelection(mCurCheckPosition);// Activity is created, fragments are available// Go ahead and populate the details fragmentmyActivity.showDetails(mCurCheckPosition);}@Overridepublic void onSaveInstanceState(Bundle outState) {Log.v(MainActivity.TAG, "in TitlesFragment onSaveInstanceState");super.onSaveInstanceState(outState);outState.putInt("curChoice", mCurCheckPosition);}@Overridepublic void onListItemClick(ListView l, View v, int pos, long id) {Log.v(MainActivity.TAG,"in TitlesFragment onListItemClick. pos = "+ pos);myActivity.showDetails(pos);mCurCheckPosition = pos;}@Overridepublic void onDetach() {Log.v(MainActivity.TAG, "in TitlesFragment onDetach");super.onDetach();myActivity = null;}
}
与 DetailsFragment 不同,对于这个片段,您不需要在 onCreateView() 回调中做任何事情。这是因为您扩展了 ListFragment 类,其中已经包含了一个 ListView 。一个 ListFragment 的默认 onCreateView() 为你创建这个 ListView 并返回它。直到 onActivityCreated() 你才做任何真正的应用逻辑。到目前为止,在您的应用中,您可以确定活动的视图层次结构以及这个片段的视图层次结构已经创建。列表视图的资源 ID 是 android。R.id.list1 ,但是如果你需要获取对它的引用,你可以随时调用 getListView() ,你可以在 onActivityCreated() 中这样做。因为列表片段管理列表视图,所以不要将适配器直接连接到列表视图。您必须使用 ListFragment 的 setListAdapter() 方法。活动的视图层次现在已经设置好了,所以您可以安全地返回到活动中进行 showDetails() 调用。
在示例活动的这一阶段,您已经向列表视图添加了一个列表适配器,您已经恢复了当前位置(如果您是从恢复中恢复过来的,可能是由于配置更改),并且您已经要求活动(在 showDetails() 中)将文本设置为与所选的莎士比亚的标题相对应。
您的 TitlesFragment 类在列表上也有一个监听器,因此当用户单击另一个标题时,调用 onListItemClick() 回调,您切换文本以对应于该标题,再次使用 showDetails() 方法。
这个片段与之前的细节片段的另一个区别是,当这个片段被销毁和重新创建时,您将状态保存在一个 bundle 中(列表中当前位置的值),并在 onCreate() 中读回它。不像在你的活动布局上的框架布局中交换的细节片段,只需要考虑一个标题片段。因此,当配置发生变化,并且您的标题片段正在经历保存和恢复操作时,您需要记住您在哪里。使用细节片段,您可以重新创建它们,而不必记住以前的状态。
需要时调用单独的活动
有一段代码我们还没有谈到,那就是在 showDetails() 中,当你处于纵向模式时,细节片段将无法与标题片段正确地放在同一页面上。如果屏幕空间不允许对一个片段进行可行的查看,否则该片段将与其他片段一起显示,您将需要启动一个单独的活动来显示该片段的用户界面。对于您的示例应用,您实现了一个细节活动;代码在清单 8-10 中。
清单 8-10 。当片段不适合时显示新的活动
public class DetailsActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {Log.v(MainActivity.TAG, "in DetailsActivity onCreate");super.onCreate(savedInstanceState);if (getResources().getConfiguration().orientation== Configuration.ORIENTATION_LANDSCAPE) {// If the screen is now in landscape mode, it means// that your MainActivity is being shown with both// the titles and the text, so this activity is// no longer needed. Bail out and let the MainActivity// do all the work.finish();return;}if(getIntent() != null) {// This is another way to instantiate a details// fragment.DetailsFragment details =DetailsFragment.newInstance(getIntent().getExtras());getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();}}
}
这段代码有几个有趣的方面。首先,它很容易实现。您可以简单地确定设备的方向,只要您处于纵向模式,就可以在这个细节活动中设置一个新的细节片段。如果您处于横向模式,您的 MainActivity 能够显示标题片段和细节片段,所以根本没有理由显示这个活动。你可能想知道如果你在横向模式下,为什么你会发起这个活动,答案是,你不会。但是,一旦此活动以纵向模式启动,如果用户将设备旋转到横向模式,此详细信息活动将由于配置更改而重新启动。现在活动开始了,而且是在横向模式下。在那一刻,结束这个活动并让主活动 接管并完成所有工作是有意义的。
关于这个细节活动的另一个有趣的方面是,您从不使用 setContentView() 来设置根内容视图。那么用户界面是如何创建的呢?如果您仔细观察片段事务上的 add() 方法调用,您会看到添加片段的视图容器被指定为资源 android。R.id.content 这是活动的顶级视图容器,因此当您将片段视图层次结构附加到该容器时,您的片段视图层次结构将成为活动的唯一视图层次结构。您使用与之前完全相同的 DetailsFragment 类和另一个 newInstance() 方法来创建片段(将 bundle 作为参数的方法),然后您简单地将它附加到活动视图层次结构的顶部。这将导致片段显示在这个新的活动中。
从用户的角度来看,他们现在看到的只是细节片段视图,这是莎士比亚戏剧中的文本。如果用户想要选择一个不同的标题,他们按下 Back 按钮,弹出这个活动来显示你的主活动(只有标题片段)。用户的另一个选择是旋转设备以回到风景模式。然后您的细节活动将调用 finish() 并离开,显示下面同样旋转的主活动。
当设备处于纵向模式时,如果你没有在主活动中显示细节片段,你应该有一个单独的 main.xml 布局文件用于纵向模式,就像清单 8-11 中的那个。
清单 8-11 。肖像主活动的布局
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is res/layout/main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><fragment class="com.androidbook.fragments.bard.TitlesFragment"android:id="@+id/titles"android:layout_width="match_parent"android:layout_height="match_parent" />
</LinearLayout>
当然,您可以将这种布局设计成您想要的样子。出于这里的目的,您只需让它单独显示标题片段。您的 titles fragment 类不需要包含太多代码来处理设备重新配置,这非常好。
请花点时间查看这个应用的清单文件。你可以在其中找到一个类别为 LAUNCHER 的主活动,这样它就会出现在设备的应用列表中。然后您有单独的细节活动,类别为默认。这允许您从代码启动详细信息活动,但不会将详细信息活动显示为应用列表中的一个应用。
片段的持久性
当您使用这个示例应用时,请确保旋转设备(按 Ctrl+F11 在模拟器中旋转设备)。你会看到设备旋转,片段也随之旋转。如果您观察 LogCat 消息,您会看到这个应用的许多消息。特别是,在设备轮换期间,要特别注意有关片段的消息;不仅活动被破坏和重新创建,而且片段也被破坏和重新创建。
到目前为止,您只在 titles 片段上编写了一小段代码来记住重启后 titles 列表中的当前位置。您没有在细节片段代码中做任何事情来处理重新配置,这是因为您不需要这样做。Android 会保留片段管理器中的片段,保存它们,然后在重新创建活动时恢复它们。您应该意识到,在重新配置完成后,您获得的片段很可能与您之前在内存中获得的片段不同。这些片段已经为你复原了。Android 保存了 arguments bundle 和它是哪种类型的片段的知识,并且它为每个片段存储了保存状态 bundle,其中包含关于该片段的保存状态信息,用于在另一端恢复它。
LogCat 消息向您展示了片段与活动同步地经历它们的生命周期。您将看到您的细节片段被重新创建,但是您的 newInstance() 方法没有被再次调用。相反,Android 使用默认的构造函数,将 arguments 包附加到它上面,然后开始调用片段上的回调。这就是为什么在 newInstance() 方法中不做任何花哨的事情是如此重要:当片段被重新创建时,它不会通过 newInstance() 来完成。
到目前为止,您还应该意识到,您已经能够在一些不同的地方重用您的片段。titles 片段在两种不同的布局中使用,但是如果您查看 titles 片段代码,它并不担心每个布局的属性。您可以使布局彼此大相径庭,而标题片段代码看起来是一样的。细节片段也是如此。它被用于你的主要景观布局和细节活动中。同样,细节片段的布局在两者之间可能有很大的不同,而细节片段的代码可能是相同的。细节活动的代码也非常简单。
到目前为止,您已经探索了两种片段类型:基本片段类和列表片段子类。片段还有其他子类: DialogFragment 、 PreferenceFragment 和 WebViewFragment 。我们将在第十章和第十一章中分别介绍对话片段和偏好片段。
片段通信
因为片段管理器知道附加到当前活动的所有片段,所以活动或该活动中的任何片段可以使用前面描述的 getter 方法请求任何其他片段。一旦获得了片段引用,活动或片段就可以适当地转换引用,然后直接在该活动或片段上调用方法。这将导致您的片段比通常所期望的拥有更多的关于其他片段的知识,但是不要忘记您是在移动设备上运行这个应用,所以抄近路有时是合理的。清单 8-12 中提供了一个代码片段,展示了一个片段如何与另一个片段直接通信。该片段将是你的扩展片段类的一部分,而片段是一个不同的扩展片段类。
清单 8-12 。直接片段到片段通信
FragmentOther fragOther =(FragmentOther)getFragmentManager().findFragmentByTag("other");
fragOther.callCustomMethod( arg1, arg2 );
在清单 8-12 中,当前片段直接知道另一个片段的类,以及该类中存在哪些方法。这可能没问题,因为这些片段是一个应用的一部分,简单地接受一些片段将知道其他片段的事实可能更容易。我们将在第十章的对话片段示例应用中向您展示一种更简洁的片段间通信方式。
使用 startActivity()和 setTargetFragment()
片段的一个非常类似于活动的特性是片段启动活动的能力。片段有一个 startActivity() 方法和 startActivityForResult() 方法。这些工作就像那些活动;当一个结果被传回时,它将导致 onActivityResult() 回调在启动活动的片段上触发。
你应该知道另一种沟通机制。当一个片段想要启动另一个片段时,有一个特性可以让调用片段设置它与被调用片段的身份。清单 8-13 展示了一个可能的例子。
清单 8-13 。片段到目标片段设置
mCalledFragment = new CalledFragment();
mCalledFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(mCalledFragment, "work").commit();
通过这几行代码,您已经创建了一个新的 CalledFragment 对象,将被调用片段上的目标片段设置为当前片段,并使用片段事务将被调用片段添加到片段管理器和活动中。当被调用的片段开始运行时,它将能够调用 getTargetFragment() ,这将返回对调用片段的引用。有了这个引用,被调用的片段可以调用调用片段上的方法,甚至直接访问视图组件。例如,在清单 8-14 中,被调用的片段可以直接在调用片段的 UI 中设置文本。
清单 8-14 。目标片段对片段通信
TextView tv = (TextView)getTargetFragment().getView().findViewById(R.id.text1);
tv.setText("Set from the called fragment");
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
- :与本书相关的可下载项目列表。名为 proandroid 5 _ Ch08 _ fragments . zip 的文件包含本章的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 zip 文件之一导入到 IDE 中。它还包括一些为旧的机器人使用片段兼容性 SDK 的项目。
Developer . Android . com/Guide/components/fragments . html
:Android 开发者的片段指南页面。developer . android . com/design/patterns/multi-pane-layouts . html
:多面板布局的 Android 设计指南。developer . Android . com/training/basics/fragments/index . html
:Android 培训页面为片段。
摘要
本章介绍了片段类及其相关的管理器类、事务类和子类。这是本章内容的总结:
- 片段类,它做什么,以及如何使用它。
- 为什么片段不被附加到一个且只有一个活动就不能被使用。
- 虽然片段可以用静态工厂方法实例化,比如 newInstance() ,但是您必须始终拥有一个默认的构造函数,并且有一种方法可以将初始化值保存到初始化参数包中。
- 片段的生命周期,以及它如何与拥有该片段的活动的生命周期交织在一起。
- FragmentManager 及其特性。
- 使用片段管理设备配置。
- 将片段合并到一个活动中,或者将它们拆分到多个活动中。
- 使用片段事务来改变向用户显示的内容,并使用酷炫的效果来制作这些转换的动画。
- 使用片段时后退按钮可能出现的新行为。
- 在布局中使用 <片段> 标签。
- 当你想使用过渡时,使用一个框架布局作为片段的占位符。
- ListFragment 以及如何使用适配器来填充数据(非常类似于 ListView )。
- 当一个片段不能适应当前屏幕时启动一个新的活动,以及当一个配置改变使得可以再次看到多个片段时如何调整。
- 片段之间以及片段与其活动之间的通信。*
九、响应配置更改
到目前为止,我们已经介绍了相当多的内容,现在似乎是介绍配置更改的好时机了。当应用在设备上运行时,如果设备的配置发生变化(例如旋转 90 度),应用需要做出相应的响应。新配置看起来很可能与以前的配置不同。例如,从纵向模式切换到横向模式意味着屏幕从又高又窄变成了又短又宽。UI 元素(按钮、文本、列表等等)需要重新排列、调整大小,甚至删除以适应新的配置。
在 Android 中,默认情况下,配置更改会导致当前活动消失并被重新创建。应用本身继续运行,但是它有机会改变活动的显示方式以响应配置的改变。在极少数情况下,您需要在不破坏和重新创建活动的情况下处理配置更改,Android 也提供了一种处理方式。
请注意,配置更改可以采取多种形式,而不仅仅是设备旋转。如果设备连接到 dock,这也是一种配置更改。改变设备的语言也是如此。无论新的配置是什么,只要你已经为该配置设计了你的活动,Android 会处理大部分事情来过渡到它,给用户一个无缝的体验。
本章将从活动和片段两个角度带您了解配置变更的过程。我们将向您展示如何为这些转换设计您的应用,以及如何避免可能导致您的应用崩溃或行为不当的陷阱。
默认配置更改流程
Android 操作系统会跟踪运行它的设备的当前配置。配置包括许多因素,而且新的因素一直在增加。例如,如果设备被插入扩展坞,这表示设备配置发生了变化。当 Android 检测到配置更改时,运行中的应用会调用回调来告诉它们正在发生更改,因此应用可以正确地对更改做出响应。我们稍后将讨论这些回调,但是现在让我们刷新一下关于资源的记忆。
Android 的一个伟大特性是根据设备的当前配置为你的活动选择资源。您不需要编写代码来确定哪个配置是活动的;你只需按名称访问资源,Android 就会为你获取相应的资源。如果设备处于纵向模式,并且您的应用请求布局,您将获得纵向布局。如果设备处于横向模式,您将获得横向布局。代码只是请求一个布局,而没有指定应该获得哪个布局。这是非常强大的,因为当引入新的配置因子或配置因子的新值时,代码保持不变。开发人员需要做的就是决定是否需要创建新的资源,并根据新配置的需要创建它们。然后,当应用经历配置更改时,Android 向应用提供新的资源,一切继续按预期运行。
由于非常希望事情简单,当配置改变时,Android 破坏当前的活动,并在它的位置创建一个新的活动。这可能看起来相当苛刻,但事实并非如此。更大的挑战是采取一个运行的活动,并找出哪些部分会保持不变,哪些不会,然后只处理需要改变的部分。
一个即将被销毁的活动首先被适当地通知,给你一个机会去保存任何需要保存的东西。当新活动被创建时,它有机会使用前一个活动的数据来恢复状态。为了获得良好的用户体验,显然您不希望这种保存和恢复花费很长时间。
保存你需要保存的任何数据,然后让 Android 扔掉其余的并重新开始,这是相当容易的,只要应用及其活动的设计不包含大量非 UI 的东西,这些东西需要很长时间来重新创建。成功的配置变更设计的秘密就在于此:不要把“东西”放在一个在配置变更期间不容易被重新创建的活动中。
请记住,我们的应用并没有被销毁,所以应用上下文中的任何东西,不属于我们当前活动的一部分,都将在新活动中继续存在。Singletons 仍然是可用的,任何后台线程也是可用的,这些线程可能是我们为了应用而分离出来的。我们合作过的任何数据库或内容供应器也将继续存在。利用这些优势可以快速、轻松地更改配置。如果可能的话,将数据和业务逻辑放在活动之外。
活动和片段之间的配置变更过程有些相似。当一个活动被销毁和重新创建时,该活动中的片段被销毁和重新创建。我们需要担心的是关于我们的片段和活动的状态信息,比如当前显示给用户的数据,或者我们想要保留的内部值。我们将保存我们想要保留的东西,并在片段和活动被重新创建时在另一边再次拾起它。您会希望通过不让数据在默认的配置更改过程中被破坏来保护不容易重新创建的数据。
活动的破坏/创建周期
在处理活动中的默认配置更改时,需要注意三个回调:
- onSaveInstanceState()
- onCreate()
- onRestoreInstanceState()
第一个是 Android 在检测到配置发生变化时调用的回调。在配置更改结束时创建新活动时,活动有机会保存它想要恢复的状态。在调用 onStop() 之前,将调用 onSaveInstanceState() 回调。任何存在的状态都可以被访问并保存到一个 Bundle 对象中。当活动被重新创建时,这个对象将被传递给其他两个回调函数( onCreate() 和 onRestoreInstanceState())。您只需要在其中一个中放入逻辑来恢复活动的状态。
默认的 onSaveInstanceState() 回调为您做了一些好事。例如,它遍历当前活动的视图层次结构,并保存每个具有 android:id 的视图的值。这意味着如果您的 EditText 视图接收到一些用户输入,那么该输入将在活动销毁/创建周期的另一端可用,以在用户取回控制权之前填充 EditText 。您不需要亲自经历并保存这个状态。如果您确实覆盖了 onSaveInstanceState() ,请确保使用 bundle 对象调用 super . onSaveInstanceState(),以便它可以为您处理此事。保存的不是视图,而是它们状态的属性,这些属性应该在销毁/创建边界上保持不变。
要将数据保存在 bundle 对象中,可以对整数使用 putInt() ,对字符串使用 putString() 等方法。 android.os.Bundle 类有不少方法;不限于整数和字符串。例如, putParcelable() 可以用来保存复杂对象。每个 put 都与一个字符串键一起使用,稍后您将使用与放入该值相同的键来检索该值。示例 onSaveInstanceState() 可能类似于清单 9-1 。
清单 9-1 。样本 onSaveInstanceState()
@Override
public void onSaveInstanceState(Bundle icicle) {super.onSaveInstanceState(icicle);icicle.putInt("counter", 1);
}
有时这个包被称为冰柱,因为它代表了一项活动的一小块冻结部分。在这个示例中,您只保存了一个值,它有一个计数器的键。您可以通过简单地向这个回调函数添加更多的 put 语句来保存更多的值。本例中的计数器值是临时的,因为如果应用被完全破坏,当前值将会丢失。例如,如果用户关闭了他们的设备,就会发生这种情况。在第十一章中,你会学到更持久地保存价值的方法。这个实例状态只意味着在应用运行时保留值。对于需要长期保存的重要状态,不要使用这种机制。
为了恢复活动状态,您可以访问 bundle 对象来检索您认为存在的值。同样,您使用 Bundle 类的方法,比如 getInt() 和 getString() ,并传递适当的键来告诉您想要返回哪个值。如果键在包中不存在,则传回值 0 或 null (取决于被请求对象的类型)。或者您可以在适当的 getter 方法中提供默认值。清单 9-2 显示了一个示例 onRestoreInstanceState()回调。
清单 9-2 。样本 onRestoreInstanceState()
@Override
public void onRestoreInstanceState(Bundle icicle) {super.onRestoreInstanceState(icicle);int someInt = icicle.getInt("counter", -1);// Now go do something with someInt to restore the// state of the activity. -1 is the default if no// value was found.
}
是在 onCreate() 还是在 onRestoreInstanceState() 恢复状态,由你决定。许多应用将在 onCreate() 中恢复状态,因为许多初始化工作都是在那里完成的。将两者分开的一个原因是,如果您正在创建一个可以扩展的 activity 类。进行扩展的开发人员可能会发现,与重写所有的 onCreate() 相比,用代码重写 onRestoreInstanceState() 来恢复状态会更容易。
这里需要注意的非常重要的一点是,当当前活动被完全销毁时,您需要非常关注对活动和视图以及其他需要进行垃圾收集的对象的引用。如果在保存的包中放入了引用被销毁的活动的内容,那么该活动就不能被垃圾收集。这很可能是一种内存泄漏,这种泄漏会越来越严重,直到您的应用崩溃。束中要避免的对象包括 Drawable s、 Adapter s、 View s,以及任何与活动上下文相关的东西。不要把一个 Drawable 放到包中,而是序列化位图并保存它。或者更好的是,在活动和片段之外管理位图,而不是在内部。将位图的某种引用添加到包中。当需要为新片段重新创建任何 Drawable 时,使用引用访问外部位图来重新生成您的 Drawable s。
片段的破坏/创建循环
片段的销毁/创建周期与活动的周期非常相似。处于销毁和重新创建过程中的片段将调用其 onSaveInstanceState() 回调,允许该片段将值保存在 Bundle 对象中以备后用。一个区别是,当一个片段被重新创建时,六个片段回调接收这个捆绑对象: onInflate() , onCreate() , onCreateView() , onActivityCreated() , onViewCreated() 和 onViewStateRestored() 。最后两次回调是最近的,分别来自 Honeycomb 3.2 和 JellyBean 4.2。这给了我们很多机会从先前的状态重建我们的重建片段的内部状态。
Android 只保证在 onDestroy() 之前的某个时候会为片段调用 onSaveInstanceState() 。这意味着当调用 onSaveInstanceState() 时,可能会也可能不会附加视图层次。因此,不要指望遍历 onSaveInstanceState() 中的视图层次。例如,如果片段在片段后栈上,就不会显示 UI,所以就不会存在视图层次结构。这当然是可以的,因为如果没有 UI 显示,就没有必要试图捕获视图的当前值来保存它们。在试图保存其当前值之前,您需要检查视图是否存在,如果视图不存在,不要认为这是一个错误。
与 activities 一样,注意不要在 bundle 对象中包含引用一个 activity 或一个片段的条目,当这个片段被重新创建时,这个片段可能不存在。保持包的大小尽可能小,尽可能在活动和片段之外存储持久的数据,并简单地从您的活动和片段中引用它。那么您的销毁/创建周期将会更快,您产生内存泄漏的可能性会更小,并且您的活动和片段代码应该更容易维护。
使用 FragmentManager 保存片段状态
除了 Android 通知片段应该保存它们的状态之外,片段还有另一种保存状态的方式。在 Honeycomb 3.2 中, FragmentManager 类得到了一个 saveFragmentInstanceState()方法,可以调用该方法来生成类 Fragment 的对象。保存的状态。前面提到的保存状态的方法是在 Android 内部完成的。虽然我们知道状态正在被保存,但我们无法直接访问它。这种保存状态的方法为您提供了一个对象,该对象表示一个片段的保存状态,并允许您控制是否以及何时从该状态创建一个片段。
使用片段的方法。SavedState 对象恢复一个片段是通过片段类的 setInitialSavedState() 方法实现的。在第八章的中,您了解到最好使用静态工厂方法(例如, newInstance() )来创建新的片段。在这个方法中,您看到了如何调用默认构造函数,然后附加一个参数包。您可以调用 setInitialSavedState() 方法来设置它以恢复到以前的状态。
关于这种保存片段状态的方法,您应该知道一些注意事项:
- 要保存的片段当前必须连接到片段管理器。
- 使用此保存状态创建的新片段必须与创建它的片段具有相同的类类型。
- 保存的状态不能包含对其他片段的依赖。当重新创建保存的片段时,其他片段可能不存在。
在片段上使用 setRetainInstance
片段可以避免在配置改变时被破坏和重新创建。如果使用参数 true 调用 setRetainInstance() 方法,当其活动被销毁和重新创建时,该片段将保留在应用中。不会调用该片段的 onDestroy() 回调,也不会调用 onCreate() 。将调用 onDetach() 回调,因为该片段必须从即将离开的活动中分离,并且将调用 onAttach() 和 onActivityCreated() ,因为该片段附加到一个新的活动。这只适用于不在后台堆栈上的片段。这对于没有 UI 的片段尤其有用。
这个特性非常强大,因为您可以使用一个非 UI 片段来处理对数据对象和后台线程的引用,并在这个片段上调用 setRetainInstance(true) ,这样它就不会在配置更改时被破坏和重新创建。额外的好处是,在正常的配置更改过程中,非 UI 片段回调 onDetach() 和 onAttach() 会将活动引用从旧的切换到新的。
不推荐使用的配置更改方法
关于活动的几个方法已经被否决,所以您不应该再使用它们:
- getastnonconfiguration instance()
- on retaining no configuration instance()
这些方法以前允许您保存正在被销毁的活动中的任意对象,并将其传递给正在创建的活动的下一个实例。尽管这些方法很有用,但是您现在应该使用前面描述的方法来管理销毁/创建周期中活动实例之间的数据。
自己处理配置更改
到目前为止,您已经看到了 Android 如何为您处理配置更改。它负责销毁和重新创建活动和片段,为新配置引入最佳资源,保留任何用户输入的数据,并让您有机会在一些回调中执行一些额外的逻辑。这通常是你的最佳选择。但如果不是这样,当你不得不自己处理配置变更时,Android 提供了一条出路。不建议这样做,因为这完全取决于您来确定由于这种变化需要改变什么,然后由您负责做出所有的变化。如前所述,除了方向变化之外,还有许多配置变化。幸运的是,您不必亲自处理所有的配置更改。
自己处理配置更改的第一步是在 AndroidManifest.xml 文件的 < activity > 标签中声明您将使用 android:configChanges 属性处理的更改。Android 将使用前面描述的方法处理其他配置更改。您可以根据需要指定任意多的配置更改类型,方法是将它们与“|”符号进行“或”运算,如下所示:
<activity ... android:configChanges="orientation|keyboardHidden" ... >
配置更改类型的完整列表可以在 R.attr 的参考页面上找到。请注意,如果您的目标是 API 13 或更高版本,并且您需要处理方向,您还需要处理屏幕尺寸。
配置更改的默认过程是调用回调来销毁和重新创建活动或片段。当您已经声明您将处理特定的配置更改时,流程会发生变化,因此只有 onConfigurationChanged() 回调会在活动及其片段上被调用。Android 传入一个配置对象,因此回调知道新的配置是什么。由回调来确定可能发生了什么变化;然而,由于您自己可能只处理少量的配置更改,所以找出这一点应该不会太难。
当您可以跳过销毁和重新创建的时候,您真的只想自己处理配置更改。例如,如果纵向和横向的活动布局是相同的布局,并且所有图像资源都是相同的,则破坏和重新创建活动实际上并没有完成任何事情。在这种情况下,声明您将处理方向配置更改是相当安全的。在活动的方向改变期间,活动将保持不变,并使用现有资源(如布局、图像、字符串等)简单地以新的方向重新呈现自己。但是如果可以的话,让 Android 来处理这些事情真的没什么大不了的。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
- :与本书相关的可下载项目列表。对于这一章,寻找一个名为 pro Android 5 _ Ch09 _ config changes . ZIP 的 ZIP 文件。这个 ZIP 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 ZIP 文件之一导入到您的 IDE 中。
Developer . Android . com/Guide/topics/fundamentals/activities . html # SavingActivityState
:Android 开发者指南,讨论保存和恢复状态。developer . Android . com/Guide/topics/resources/Runtime-Changes . html
:处理运行时变更的 Android API 指南。
摘要
让我们通过快速列举您所学到的关于处理配置更改的内容来结束本章:
- 默认情况下,活动会在配置更改期间被销毁和重新创建。片段也是。
- 避免将大量数据和逻辑放入活动中,以便快速进行配置更改。
- 让 Android 提供适当的资源。
- 使用单例来保存活动之外的数据,以便在配置更改期间更容易销毁和重新创建活动。
- 利用默认的 onSaveInstanceState() 回调来保存带有 android:id s 的视图的 UI 状态
- 如果一个片段在活动销毁和创建周期中没有问题,使用 setRetainInstance() 告诉 Android 它不需要销毁和创建这个片段。
十、使用对话框
Android SDK 为对话框提供了广泛的支持。对话框是在当前窗口前面弹出的一个较小的窗口,用来显示紧急消息,提示用户输入内容,或者显示某种状态,如下载进度。通常期望用户与对话框交互,然后返回到下面的窗口继续应用。从技术上来说,Android 允许在一个活动的布局中嵌入一个对话片段,我们也会谈到这一点。
Android 中明确支持的对话框包括警告、提示、选择列表、单选、多选、进度、时间选择器和日期选择器对话框。(该列表可能因 Android 版本而异。)Android 还支持其他需求的自定义对话框。这一章的主要目的不是涵盖所有的对话框,而是通过一个示例应用来涵盖 Android 对话框的底层架构。从那里你应该可以使用任何 Android 对话框。
需要注意的是,Android 3.0 增加了基于片段的对话框。来自 Google 的期望是开发者只会使用片段对话框,即使是在 Android 之前的版本中。这可以通过片段兼容性库来完成。为此,本章重点介绍对话片段 。
使用 Android 中的对话框
Android 中的对话框是异步的,这提供了灵活性。然而,如果您习惯于对话框主要是同步的编程框架(比如 Microsoft Windows,或者网页中的 JavaScript 对话框),您可能会发现异步对话框有点不直观。对于同步对话框,对话框显示后的代码行直到对话框关闭后才运行。这意味着下一行代码可以询问按下了哪个按钮,或者在对话框中输入了什么文本。然而在 Android 中,对话框是异步的。对话框一显示出来,下一行代码就会运行,即使用户还没有接触到对话框。您的应用必须通过从对话框实现回调来处理这一事实,以允许应用被通知用户与对话框的交互。
这也意味着您的应用能够从代码中消除对话框,这是非常强大的。如果对话框因为你的应用正在做一些事情而显示一条繁忙的消息,一旦你的应用完成了那个任务,它就可以从代码中关闭对话框。
理解对话片段
在本节中,您将学习如何使用对话框片段来呈现一个简单的警报对话框和一个用于收集提示文本的自定义对话框。
基本对话片段
在我们向您展示提示对话框和警告对话框的工作示例之前,我们想先介绍一下对话框片段的高级概念。与对话框相关的功能使用一个名为 DialogFragment 的类。一个 DialogFragment 从类 Fragment 派生而来,其行为很像一个片段。然后,您将使用 DialogFragment 作为您的对话框的基类。一旦你有了一个从这个类派生的对话框,比如
public class MyDialogFragment extends DialogFragment { ... }
然后,您可以使用片段事务将这个对话框片段 MyDialogFragment 显示为一个对话框。清单 10-1 展示了一个代码片段。
清单 10-1 。显示对话片段
public class SomeActivity extends Activity
{//....other activity functionspublic void showDialog(){//construct MyDialogFragmentMyDialogFragment mdf = MyDialogFragment.newInstance(arg1,arg2);FragmentManager fm = getFragmentManager();FragmentTransaction ft = fm.beginTransaction();mdf.show(ft,"my-dialog-tag");}//....other activity functions
}
注意我们在本章末尾的“参考资料”部分提供了一个可下载项目的链接。您可以使用该下载来试验本章中介绍的代码和概念。
从清单 10-1 开始,显示对话片段的步骤如下:
- 创建一个对话框片段。
- 获取片段交易。
- 使用步骤 2 中的片段事务显示对话框。
让我们来谈谈其中的每一个步骤。
构造一个对话片段
当构建一个对话框片段时,规则与构建任何其他类型的片段时是一样的。推荐的模式是像以前一样使用工厂方法,比如 newInstance() 。在 new instance()方法中,您使用对话框片段的默认构造函数,然后添加一个包含传入参数的 arguments 包。你不想在这个方法里面做其他的工作,因为你必须确保你在这里做的和 Android 从一个保存的状态恢复你的对话框片段时做的是一样的。Android 所做的就是调用默认的构造函数,并在其上重新创建参数包。
覆盖 onCreateView
当您从对话框片段继承时,您需要重写两个方法中的一个来为您的对话框提供视图层次结构。第一个选项是覆盖 onCreateView() 并返回一个视图。第二个选项是覆盖 onCreateDialog() 并返回一个对话框(类似于由 AlertDialog 构造的对话框。构建器,我们稍后会谈到)。
清单 10-2 展示了一个覆盖 onCreateView() 的例子。
清单 10-2 。覆盖 DialogFragment 的 onCreateView()
public class MyDialogFragment extends DialogFragmentimplements View.OnClickListener
{.....other functionspublic View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState){//Create a view by inflating desired layoutView v =inflater.inflate(R.layout.prompt_dialog, container, false);//you can locate a view and set valuesTextView tv = (TextView)v.findViewById(R.id.promptmessage);tv.setText(this.getPrompt());//You can set callbacks on buttonsButton dismissBtn = (Button)v.findViewById(R.id.btn_dismiss);dismissBtn.setOnClickListener(this);Button saveBtn = (Button)v.findViewById(R.id.btn_save);saveBtn.setOnClickListener(this);return v;}.....other functions
}
在清单 10-2 中,你正在加载一个由布局标识的视图。然后寻找两个按钮,并在它们上面设置回调。这与你在《??》第八章中创建细节片段的方式非常相似。然而,与前面的片段不同,对话框片段有另一种方式来创建视图层次结构。
「突出显示」对话方块「??」
作为在 onCreateView() 中提供视图的替代方法,您可以覆盖 onCreateDialog() 并提供一个对话框实例。清单 10-3 提供了这种方法的示例代码。
清单 10-3 。覆盖对话框片段的 onCreateDialog()
public class MyDialogFragment extends DialogFragmentimplements DialogInterface.OnClickListener
{.....other functions@Overridepublic Dialog onCreateDialog(Bundle icicle){AlertDialog.Builder b = new AlertDialog.Builder(getActivity()).setTitle("My Dialog Title").setPositiveButton("Ok", this).setNegativeButton("Cancel", this).setMessage(this.getMessage());return b.create();}.....other functions
}
在此示例中,您使用警报对话框生成器来创建要返回的对话框对象。这对于简单的对话框很有效。覆盖 onCreateView() 的第一个选项同样简单,并且提供了更大的灵活性。
警报对话框。Builder 实际上是 3.0 之前 Android 的遗留物。这是创建对话框的老方法之一,您仍然可以在 DialogFragment s 中创建对话框。正如您所看到的,通过调用各种可用的方法来创建对话框相当容易,就像我们在这里所做的那样。
显示对话片段
一旦构建了一个对话片段,就需要一个片段事务来显示它。像所有其他片段一样,对对话片段的操作是通过片段事务进行的。
对话片段上的 show() 方法将片段事务作为输入。你可以在清单 10-1 中看到这一点。 show() 方法使用片段事务将这个对话框添加到活动中,然后提交片段事务。但是, show() 方法不会将事务添加到后台堆栈中。如果要这样做,需要先把这个事务添加到后台栈,然后传递给 show() 方法。对话框片段的 show() 方法具有以下签名:
public int show(FragmentTransaction transaction, String tag)
public int show(FragmentManager manager, String tag)
第一个 show() 方法通过将这个片段添加到带有指定标记的传入事务中来显示对话框。然后,该方法返回已提交事务的标识符。
第二个 show() 方法自动从事务管理器获取事务。这是一个快捷的方法。然而,当您使用第二种方法时,您没有将事务添加到后台堆栈的选项。如果您想要该控件,您需要使用第一种方法。如果您只想显示对话框,并且当时没有其他理由使用片段事务,那么可以使用第二种方法。
对话作为片段的一个好处是底层的片段管理器完成了基本的状态管理。例如,即使设备在显示对话框时旋转,对话框也会在您不执行任何状态管理的情况下再现。
对话框片段还提供了控制显示对话框视图的框架的方法,例如框架的标题和外观。参考 DialogFragment 类文档以查看这些选项的更多信息;本章末尾提供了该 URL。
消除对话片段
有两种方法可以消除对话框片段。第一种是显式调用对话框片段上的 dismisse()方法来响应对话框视图上的按钮或某些动作,如清单 10-4 所示。
清单 10-4 。调用解除()
if (someview.getId() == R.id.btn_dismiss)
{//use some callbacks to advise clients//of this dialog that it is being dismissed//and call dismissdismiss();return;
}
对话框片段的 dissolve()方法从片段管理器中移除该片段,然后提交该事务。如果这个对话片段有一个后台堆栈,那么 dissolve()将当前对话弹出事务堆栈,并呈现前一个片段的事务状态。不管有没有回栈,调用 disass()都会导致调用标准的对话框片段销毁回调,包括 onDismiss() 。
需要注意的一点是,你不能依靠 onDismiss() 来断定你的代码已经调用了一个 disass()。这是因为当设备配置改变时 onDismiss() 也被调用,因此不能很好地指示用户对对话框本身做了什么。如果用户旋转设备时对话框正在显示,即使用户没有按下对话框中的按钮,对话框片段也会看到 onDismiss() 被调用。相反,你应该总是依赖于对话框视图上的显式按钮点击。
如果用户在对话框片段显示时按下后退按钮,这将导致对话框片段触发 onCancel() 回调。默认情况下,Android 让对话框片段消失,所以你不需要自己对片段调用 dismisse()。但是如果您想让调用活动得到对话框被取消的通知,您需要从 onCancel() 内部调用逻辑来实现这一点。这就是 onCancel() 和 onDismiss() 对于对话片段的区别。对于 onDismiss() ,您不能确定到底发生了什么导致了 onDismiss() 回调的触发。您可能还注意到,一个对话框片段没有 cancel() 方法,只有 dissolve();但是正如我们所说的,当一个对话框片段被按下返回按钮取消时,Android 会帮你取消/消除它。
消除对话片段的另一种方法是呈现另一个对话片段。关闭当前对话框并显示新对话框的方式与关闭当前对话框略有不同。清单 10-5 显示了一个例子。
清单 10-5 。为后台堆栈设置对话框
if (someview.getId() == R.id.btn_invoke_another_dialog)
{Activity act = getActivity();FragmentManager fm = act.getFragmentManager();FragmentTransaction ft = fm.beginTransaction();ft.remove(this);ft.addToBackStack(null);//null represents no name for the back stack transactionHelpDialogFragment hdf =HelpDialogFragment.newInstance(R.string.helptext);hdf.show(ft, "HELP");return;
}
在单个事务中,您将删除当前的对话片段并添加新的对话片段。这具有使当前对话框在视觉上消失并使新对话框出现的效果。如果用户按下 Back 按钮,因为您已经在 back stack 上保存了这个事务,新的对话框被关闭,并显示上一个对话框。例如,这是显示帮助对话框的一种便捷方式。
对话框解除的含义
当您向片段管理器添加任何片段时,片段管理器会对该片段进行状态管理。这意味着当设备配置发生变化时(例如,设备旋转),活动会重新启动,片段也会重新启动。在第八章的中,您在运行莎士比亚示例应用时旋转了设备。
设备配置的改变不会影响对话框,因为它们也由片段管理器管理。但是 show() 和 disass()的隐含行为意味着如果你不小心的话,你会很容易忘记一个对话片段。 show() 方法自动将片段添加到片段管理器中;方法自动从片段管理器中删除片段。在开始显示对话片段之前,您可能有一个指向该片段的直接指针。但是你不能将这个片段添加到片段管理器中,之后再调用 show() ,因为一个片段只能添加一次到片段管理器中。您可以计划通过恢复活动来检索该指针。但是,如果您显示并关闭这个对话框,这个片段将被隐式地从片段管理器中删除,从而拒绝该片段被恢复和重新打印的能力(因为片段管理器在它被删除后不知道这个片段的存在)。
如果你想在对话框关闭后保持它的状态,你需要在父活动中或者在一个非对话框片段中保持对话框外的状态。
对话片段示例应用
在本节中,您将回顾一个示例应用,它演示了对话框片段的这些概念。您还将检查片段和包含它的活动之间的通信。要实现这一切,您需要五个 Java 文件:
- MainActivity.java:你申请的主要活动。它显示了一个简单的视图,其中包含帮助文本和一个菜单,可以从该菜单启动对话框。
- PromptDialogFragment.java:一个对话框片段的例子,它用 XML 定义了自己的布局,并允许用户输入。它有三个按钮:保存、消除(取消)和帮助。
- AlertDialogFragment.java:一个对话框片段的例子,它使用 AlertBuilder 类在这个片段中创建一个对话框。这是创建对话的传统方式。
- HelpDialogFragment.java:一个非常简单的片段,显示了来自应用资源的帮助消息。特定的帮助消息是在创建帮助对话框对象时标识的。这个帮助片段可以在主活动和提示对话框片段中显示。
- OnDialogDoneListener.java:一个接口,您需要您的活动来实现它,以便从片段中获取消息。使用接口意味着你的片段不需要知道太多关于调用活动的信息,除非它已经实现了这个接口。这有助于封装功能。从活动的角度来看,它有一个通用的方法来接收来自片段的信息,而不需要了解太多。
这个应用有三种布局:主活动、提示对话框片段和帮助对话框片段。请注意,您不需要警告对话框片段的布局,因为 AlertBuilder 会在内部为您处理该布局。当你完成后,应用看起来像图 10-1 。
图 10-1 。对话框片段示例应用的用户界面
对话示例:主活动
让我们来看看源代码,您可以从本书的网站上下载(参见“参考资料”一节)。我们将使用 DialogFragmentDemo 项目。在我们继续之前,开放 MainActivity.java 的源代码。
主活动的代码非常简单。您显示一个简单的文本页面并设置一个菜单。每个菜单项调用一个活动方法,每个方法基本上做同样的事情:获取一个片段事务,创建一个新的片段,并显示该片段。注意,每个片段都有一个用于片段事务的唯一标签。这个标记与片段管理器中的片段相关联,因此您可以在以后通过标记名来定位这些片段。该片段还可以用片段上的 getTag() 方法确定自己的标签值。
主活动中的最后一个方法定义是 onDialogDone() ,它是一个回调,是您的活动正在实现的 OnDialogDoneListener 接口的一部分。正如您所看到的,回调提供了一个调用您的片段的标签、一个指示对话框片段是否被取消的布尔值和一条消息。就您的目的而言,您只想将信息记录到 LogCat 您还可以使用吐司向用户展示它。吐司将在本章后面介绍。
样本对话框:ondialogdonelistener
为了让您能够知道对话框何时消失,请创建一个由对话框调用者实现的侦听器接口。接口的代码在 OnDialogDoneListener.java。
如你所见,这是一个非常简单的界面。您只能为此接口选择一个回调,活动必须实现该回调。您的片段不需要知道调用活动的细节,只需要知道调用活动必须实现 OnDialogDoneListener 接口;因此,片段可以调用这个回调来与调用活动通信。根据片段正在做的事情,接口中可能有多个回调。对于这个示例应用,您将显示与片段类定义分开的接口。为了更容易管理代码,您可以将片段侦听器接口嵌入片段类定义本身,从而更容易保持侦听器和片段之间的同步。
对话框示例:PromptDialogFragment
现在让我们来看看你的第一个片段, PromptDialogFragment ,它的布局在/res/layout/prompt_dialog.xml 中,Java 代码在 PromptDialogFragment.java 的/src 下。
这个提示对话框布局看起来像你以前见过的许多。有一个 TextView 作为提示;一个 EditText 接受用户的输入;以及用于保存输入、消除(取消)对话片段和弹出帮助对话框的三个按钮。
Java 代码开始看起来就像你之前的片段。您有一个 newInstance() 静态方法来创建新对象,在这个方法中,您调用默认的构造函数,构建一个参数包,并将其附加到您的新对象。接下来,您在 onAttach() 回调中有了新的东西。您希望确保您刚刚附加到的活动已经实现了 OnDialogDoneListener 接口。为了测试这一点,您将传递的活动转换为 OnDialogDoneListener 接口。下面是代码:
try {OnDialogDoneListener test = (OnDialogDoneListener)act;
}
catch(ClassCastException cce) {// Here is where we fail gracefully.Log.e(MainActivity.LOGTAG, "Activity is not listening");
}
如果活动没有实现这个接口,就会抛出一个 ClassCastException 。您可以更优雅地处理这个异常,但是这个示例尽可能保持代码简单。
接下来是 onCreate() 回调。与片段一样,您不需要在这里构建您的用户界面,但是您可以设置对话框样式。这是对话片段所特有的。您可以自己设置样式和主题,也可以只设置样式并使用主题值零(0)让系统为您选择合适的主题。下面是代码:
int style = DialogFragment.STYLE_NORMAL, theme = 0;
setStyle(style,theme);
在 onCreateView() 中,您为您的对话框片段创建视图层次结构。就像其他片段一样,您没有将视图层次结构附加到传入的视图容器中(也就是说,通过将 attachToRoot 参数设置为 false )。然后继续设置按钮回调,并将对话框提示文本设置为最初传递给 newInstance() 的提示。
没有显示 onCancel() 和 onDismiss() 回调,因为它们所做的只是记录日志;您将能够看到这些回调在片段的生命周期中何时触发。
提示对话框片段中的最后一个回调是针对按钮的。再一次,您获取对您的封闭活动的引用,并将其转换为您期望该活动已经实现的接口。如果用户按下了 Save 按钮,您获取输入的文本并调用接口的回调函数 onDialogDone() 。这个回调函数接受这个片段的标记名、一个指示这个对话框片段是否被取消的布尔值和一条消息,在这个例子中,这条消息是用户输入的文本。这是来自的主要活动:
public void onDialogDone(String tag, boolean cancelled,CharSequence message) {String s = tag + " responds with: " + message;if(cancelled)s = tag + " was cancelled by the user";Toast.makeText(this, s, Toast.LENGTH_LONG).show();Log.v(LOGTAG, s);
}
为了完成对 Save 按钮的点击操作,您可以调用 dissolve()来删除对话框片段。记住 dismisse()不仅使片段在视觉上消失,而且还将片段从片段管理器中弹出,因此它不再对您可用。
如果按下的按钮是 dissolve,那么再次调用接口回调,这次没有消息,然后调用 dissolve()。最后,如果用户按下了帮助按钮,您不想丢失提示对话框片段,所以您做了一些稍微不同的事情。我们之前描述过。为了记住提示对话框片段,以便以后可以回到它,您需要创建一个片段事务来删除提示对话框片段,并使用 show() 方法添加帮助对话框片段;这个需要放到后面的堆栈里。还要注意,帮助对话框片段是如何通过引用资源 ID 来创建的。这意味着您的帮助对话框片段可以与您的应用可用的任何帮助文本一起使用。
对话框示例:HelpDialogFragment
您创建了一个片段事务,从提示对话框片段转到帮助对话框片段,并将该片段事务放在后台堆栈上。这具有使提示对话框片段从视图中消失的效果,但是仍然可以通过片段管理器和后台堆栈访问它。新的帮助对话框片段出现在它的位置,并允许用户阅读帮助文本。当用户消除帮助对话框片段时,弹出片段后栈条目,帮助对话框片段的效果被消除(从视觉上和从片段管理器中),并且提示对话框片段恢复到视图。这是实现这一切的一个非常简单的方法。它非常简单却非常强大;如果用户在显示这些对话框时旋转设备,它甚至可以工作。
看一下 HelpDialogFragment.java 文件的源代码及其布局( help_dialog.xml )。这个对话框片段的目的是显示帮助文本。布局是一个文本视图和一个关闭按钮。您应该开始对 Java 代码感到熟悉了。有一个 newInstance() 方法创建新的帮助对话框片段,一个 onCreate() 方法设置样式和主题,一个 onCreateView() 方法构建视图层次结构。在这个特殊的例子中,您希望找到一个字符串资源来填充 TextView ,因此您通过活动访问资源,并选择传递给 newInstance() 的资源 ID。最后, onCreateView() 设置一个按钮点击处理器来捕获关闭按钮的点击。这种情况下,解散的时候不需要做什么有趣的事情。
这个片段有两种调用方式:从活动调用和从提示对话框片段调用。当这个帮助对话框片段显示在主活动中时,消除它只是将片段从顶部弹出,并显示下面的主活动。当该帮助对话框片段从提示对话框片段中显示时,因为帮助对话框片段是后台堆栈上片段事务的一部分,所以消除它会导致片段事务回滚,这将弹出帮助对话框片段,但会恢复提示对话框片段。用户看到提示对话框片段再次出现。
对话框示例:AlertDialogFragment
在这个示例应用中,我们还有最后一个对话框片段要向您展示:警报对话框片段。虽然您可以以类似于帮助对话框片段的方式创建一个警告对话框片段,但是您也可以使用旧的 AlertBuilder 框架来创建一个对话框片段,该框架已经在许多 Android 版本中运行。看看 AlertDialogFragment.java 的源代码。
你不需要为它设计一个布局,因为 AlertBuilder 会为你做好准备。注意,这个对话框片段开始时和其他的一样,但是不是一个 onCreateView() 回调,而是一个 onCreateDialog() 回调。您可以实现 onCreateView() 或 onCreateDialog() ,但不能同时实现两者。从 onCreateDialog() 返回的不是视图;这是一个对话。这里感兴趣的是,要获得对话框的参数,应该访问 arguments 包。在这个示例应用中,您只对警报消息这样做,但是您也可以通过 arguments bundle 访问其他参数。
还要注意,对于这种类型的对话框片段,您需要片段类来实现 DialogInterface。OnClickListener ,这意味着您的对话框片段必须实现 onClick() 回调。当用户操作嵌入的对话框时,这个回调被触发。同样,您会得到一个对触发的对话框的引用,以及按下了哪个按钮的指示。和以前一样,您应该小心不要依赖于 onDismiss() ,因为当设备配置发生变化时,这可能会触发。
对话框示例:嵌入式对话框
您可能已经注意到了 DialogFragment 的另一个特性。在应用的主布局中,文本下面是一个框架布局,它可以用来保存一个对话框。在应用的菜单中,最后一项导致片段事务将一个 PromptDialogFragment 的新实例添加到主屏幕。无需任何修改,对话框片段可以嵌入在主布局中显示,并且它的功能与您预期的一样。
这项技术的一个不同之处是,显示嵌入式对话框的代码与显示弹出对话框的代码不同。嵌入的对话框代码如下所示:
ft.add(R.id.embeddedDialog, pdf, EMBED_DIALOG_TAG);
ft.commit();
这看起来和第八章第一节中的一样,当我们在一个框架布局中显示一个片段的时候。但是,这一次,您要确保传入一个标记名,当对话框片段通知您用户输入的活动时会用到这个标记名。
对话示例:观察
当您运行这个示例应用时,请确保在设备的不同方向上尝试所有的菜单选项。显示对话片段时旋转设备。您应该很高兴看到对话框与旋转一起出现;您不需要担心大量的代码来管理由于配置更改而导致的片段的保存和恢复。
我们希望你欣赏的另一件事是你可以在片段和活动之间轻松交流。当然,活动有对所有可用片段的引用,或者可以获得对所有可用片段的引用,因此它可以访问由片段本身公开的方法。这不是片段和活动之间唯一的交流方式。您总是可以使用片段管理器上的 getter 方法来检索托管片段的实例,然后适当地转换该引用并直接调用该片段上的方法。您甚至可以在另一个片段中这样做。您使用接口和通过活动将片段相互隔离的程度,或者使用片段到片段的通信构建依赖关系的程度,取决于您的应用有多复杂,以及您想要实现多少重用。
使用吐司工作
一个 Toast 就像一个迷你警告对话框,有一条消息,显示一段时间,然后自动消失。它没有任何按钮。所以可以说是一个瞬态的告警信息。它被称为吐司,因为它像吐司一样从烤面包机里蹦出来。
清单 10-10 展示了一个如何使用 Toast 显示消息的例子。
清单 10-10 。使用 Toast 进行调试
//Create a function to wrap a message as a toast
//show the toast
public void reportToast(String message)
{String s = MainActivity.LOGTAG + ":" + message;Toast.makeText(activity, s, Toast.LENGTH_SHORT).show();
}
清单 10-10 中的 makeText() 方法不仅可以接受活动,还可以接受任何上下文对象,比如传递给广播接收器或服务的对象。这扩展了吐司在活动之外的用途。
参考
- :本章考试项目。ZIP 文件的名称是 pro Android 5 _ ch10 _ dialogs . ZIP。下载内容包括 PickerDialogFragmentDemo 中日期和时间选择器对话框的示例。
- :Android SDK 文档,提供了使用 Android 对话框的优秀介绍。你会在这里找到如何使用管理对话框的解释和各种可用对话框的例子。
developer . Android . com/reference/Android/content/dialog interface . html
:为对话框定义的众多常量。developer . Android . com/reference/Android/app/alert dialog。Builder.html
??:警报对话框生成器类的 API 文档。developer . Android . com/reference/Android/app/progress dialog . html
:针对 ProgressDialog 的 API 文档。developer . android . com/guide/topics/ui/controls/pickers . html
:使用日期选择器和时间选择器对话框的 Android 教程。
摘要
本章讨论了异步对话框以及如何使用对话框片段,包括以下主题:
- 什么是对话框,为什么要使用对话框
- Android 中对话框的异步特性
- 让对话框显示在屏幕上的三个步骤
- 创建一个片段
- 对话框片段如何创建视图层次结构的两种方法
- 片段事务如何参与显示对话片段,以及如何获得一个片段事务
- 当用户在查看对话片段时按下后退按钮会发生什么
- 后台堆栈和管理对话片段
- 当一个对话框片段上的按钮被点击时会发生什么,你如何处理它
- 一种从对话片段与调用活动通信的干净方式
- 一个对话片段如何调用另一个对话片段并仍然返回到前一个对话片段
- 这个 Toast 类以及它如何被用作一个简单的警告弹出窗口
十一、使用首选项和保存状态
Android 提供了一个强大而灵活的框架来处理设置,也称为偏好。所谓设置,我们指的是用户为定制他们喜欢的应用而做出并保存的那些特性选择。(在本章中,术语“设置”和“首选项”将互换使用。)例如,如果用户想要经由铃声或振动的通知或者根本不想要,则这是用户保存的偏好;应用会记住这个选择,直到用户改变它。Android 提供了简单的 API,这些 API 隐藏了首选项的管理和持久化。它还提供了预构建的用户界面,您可以使用这些界面让用户进行偏好选择。由于 Android preferences 框架内置了强大的功能,我们还可以使用 preferences 来更通用地存储应用状态,以允许我们的应用从它停止的地方重新开始,如果我们的应用离开并稍后回来的话。作为另一个例子,一个游戏的高分可以存储为首选项,尽管您希望使用自己的 UI 来显示它们。
本章涵盖了如何为您的应用实现您自己的设置屏幕,如何与 Android 系统设置交互,以及如何使用设置来秘密保存应用状态,它还提供了最佳实践指导。你将会发现如何让你的设置在小屏幕和大屏幕上看起来都不错,比如平板电脑。
探索偏好框架
Android 的首选项框架从单个设置选项构建到包含设置选项的屏幕层次结构。设置可以是二进制设置,如开/关、文本输入或数值,也可以是选项列表中的一个选项。Android 使用一个 PreferenceManager 向应用提供设置值。该框架负责进行和保持更改,并在设置更改或即将更改时通知应用。虽然设置保存在文件中,但应用并不直接处理文件。文件被藏了起来,你很快就会看到它们在哪里。
与第三章中的视图一样,可以用 XML 或通过编写代码来指定首选项。在本章中,您将使用一个示例应用来演示不同类型的选择。XML 是指定首选项的首选方式,所以应用就是这样编写的。XML 指定了最低级别的设置,以及如何将设置分组到类别和屏幕中。作为参考,本章的示例应用给出了如下设置,如图图 11-1T5 所示。
图 11-1 。示例应用首选项 UI 中的主要设置。由于屏幕的高度,它被显示为顶部在左边,底部在右边。请注意两幅图像之间的重叠部分
Android 提供了一个端到端的偏好框架。这意味着框架允许您定义首选项,向用户显示设置,并将用户的选择保存到数据存储中。您可以在 XML 中的 /res/xml/ 下定义您的首选项。为了向用户显示首选项,您需要编写一个 activity 类来扩展一个预定义的 Android 类,名为 Android . preference . preference activity,并使用片段来处理首选项的屏幕。框架负责剩下的事情(显示和持久化)。在您的应用中,您的代码将获得对特定首选项的引用。有了首选项引用,您可以获得首选项的当前值。
为了在用户会话中保存首选项,当前值必须保存在某个位置。Android 框架负责将首选项保存在设备上应用的 /data/data 目录下的 XML 文件中(参见图 11-2 )。
图 11-2 。应用保存偏好的路径
注意您将只能在模拟器中检查共享的偏好设置文件。在真实的设备上,由于 Android 的安全性,共享的首选项文件是不可读的(当然,除非你有 root 权限)。
应用的默认首选项文件路径是 /data/data/ 【包名】 /shared_prefs/ 【包名】 _preferences.xml,其中【包名】是应用的包。清单 11-1 显示了本例的 com . Android book . preferences . main _ preferences . XML 数据文件。
清单 11-1 。为我们的示例保存的首选项
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="notification_switch" value="true" />
<string name="package_name_preference">com.androidbook.win</string>
<boolean name="potato_selection_pref" value="true" />
<boolean name="show_airline_column_pref" value="true" />
<string name="flight_sort_option">2</string>
<boolean name="alert_email" value="false" />
<set name="pizza_toppings">
<string>pepperoni</string>
<string>cheese</string>
<string>olive</string>
</set>
<string name="alert_email_address">davemac327@gmail.com</string>
</map>
如您所见,值存储在一个映射中,首选项作为数据值的名称。有些值看起来很神秘,与显示给用户的内容不匹配。例如,航班排序选项的值是 2。Android 不会将显示的文本存储为首选项的值;相反,它存储了一个用户看不到的值,你可以独立于用户看到的内容来使用它。您希望能够根据用户的语言自由地更改显示的文本,还希望能够调整显示的文本,同时保持首选项文件中存储的值不变。如果值是一个整数而不是一些显示字符串,您甚至可以对首选项进行更简单的处理。您不必担心的是解析这个数据文件。Android 首选项框架提供了一个很好的 API 来处理首选项,这将在本章后面更详细地描述。
如果您将清单 11-1 中的偏好映射与图 11-1 中的截图进行比较,您会注意到并非所有的偏好都在偏好 XML 数据文件中列出了值。这是因为首选项数据文件不会自动为您存储默认值。您将很快看到如何处理默认值。
既然您已经看到了保存值的位置,那么您需要了解如何定义向用户显示的屏幕,以便他们可以进行选择。在您看到如何将偏好设置收集到屏幕中之前,您将了解您可以使用的不同类型的偏好设置,然后您将看到如何将它们收集到屏幕中。/data/data XML 文件中的每个持久值都来自特定的首选项。因此,让我们来了解一下其中每一项的含义。
了解复选框首选项和开关首选项
最简单的首选项是复选框首选项和开关首选项。这些共享一个公共的父类( TwoStatePreference ),或者打开(值为真)或者关闭(值为假)。对于示例应用,创建了一个带有五个复选框首选项的屏幕,如图 11-3 中的所示。清单 11-2 显示了 CheckBoxPreference 的 XML 外观。
图 11-3 。复选框首选项的用户界面
清单 11-2 。使用复选框首选项
<CheckBoxPreferenceandroid:key="show_airline_column_pref"android:title="Airline"android:summary="Show Airline column" />
注意我们会在本章末尾给你一个 URL,你可以用它来下载本章的项目。这将允许您将这些项目直接导入到 IDE 中。主示例应用名为 PrefDemo。您应该参考该项目,直到到达保存状态部分。
此示例显示了指定首选项所需的最低要求。关键字是首选项的引用或名称,标题是为首选项显示的标题,摘要是对首选项内容或当前设置状态的描述。回头看看清单 11-1 中保存的值,您会看到一个用于“show_airline_column_pref”(关键字)的 <布尔> 标记,它有一个属性值 true,这表明首选项已被选中。
使用复选框首选项,当用户设置状态时,保存首选项的状态。换句话说,当用户选中或取消选中首选项控件时,它的状态会被立即保存。
除了视觉显示不同之外, SwitchPreference 非常相似。用户看到的不是用户界面中的复选框,而是一个开关,如图图 11-1 中“通知是”旁边所示。
CheckBoxPreference 和 SwitchPreference 的另一个有用的特性是,你可以根据它是否被选中来设置不同的摘要文本。XML 属性是 summaryOn 和 summaryOff 。如果您在 main.xml 文件中查找名为“potato_selection_pref”的复选框 Preference ,您会看到一个这样的例子。
在学习其他首选项类型之前,现在是了解如何访问该首选项以读取其值并执行其他操作的好时机。
在代码中访问首选项值
现在您已经定义了一个首选项,您需要知道如何在代码中访问该首选项,以便可以读取值。清单 11-3 显示了访问 Android 中 SharedPreferences 对象的代码,该对象中存在首选项。这段代码来自 setOptionText() 方法中的【MainActivity.java】文件。
清单 11-3 。访问复选框首选项
SharedPreferences prefs =PreferenceManager.getDefaultSharedPreferences(this);
// This is the other way to get to the shared preferences:
// SharedPreferences prefs = getSharedPreferences(
// "com.androidbook.preferences.main_preferences", 0);boolean showAirline = prefs.getBoolean("show_airline_column_pref", false);
使用对首选项的引用,可以直接读取 show_airline_column_pref 首选项的当前值。如清单 11-3 所示,有两种方法可以获得首选项。所示的第一种方法是获取当前上下文的默认首选项。在这种情况下,上下文是我们应用的主活动的上下文。第二种情况是注释掉的,使用包名检索首选项。如果您需要在不同的文件中存储不同的首选项集,您可以使用您想要的任何包名。
一旦有了对首选项的引用,就可以用首选项的键和默认值调用适当的 getter 方法。由于 show_airline_column_pref 是一个 TwoStatePreference ,所以返回值是一个布尔值。show_airline_column_pref 的默认值在这里被硬编码为 false。如果这个偏好还没有被设置,硬编码值( false )将被分配给 showAirline 。但是,这本身并不会将首选项保持为 false 以备将来使用,也不会考虑 XML 规范中为该首选项设置的任何默认值。如果 XML 规范使用资源值来指定默认值,则可以在代码中引用相同的资源来设置默认值,如以下不同首选项所示:
String flight_option = prefs.getString(resources.getString(R.string.flight_sort_option),resources.getString(R.string.flight_sort_option_default_value));
注意这里首选项的键也使用了一个字符串资源值(r . string . flight _ sort _ option)。这可能是一个明智的选择,因为它减少了打字错误的可能性。如果资源名称输入错误,您很可能会得到一个构建错误。如果您只使用简单的字符串,除了您的首选项不起作用之外,输入错误可能会被忽略。
我们展示了一种在代码中读取首选项默认值的方法。Android 提供了另一种更优雅的方式。在 onCreate() 中,您可以改为执行以下操作:
PreferenceManager.setDefaultValues(this, R.xml.main, false);
然后,在 setOptionText() 中,您应该这样做来读取选项值:
String option = prefs.getString(resources.getString(R.string.flight_sort_option), null);
第一个调用将使用 main.xml 来查找默认值,并使用默认值为我们生成首选项 xml 数据文件。如果我们在内存中已经有了一个 SharedPreferences 对象的实例,它也会更新这个实例。然后,第二个调用将为 flight_sort_option 找到一个值,因为我们首先负责加载默认值。
第一次运行这段代码后,如果您查看 shared_prefs 文件夹,您将会看到 preferences XML 文件,即使 preferences 屏幕尚未被调用。您还会看到另一个名为 _ has _ set _ default _ values . XML 的文件。这个文件告诉您的应用,已经使用默认值创建了首选项 XML 文件。的第三个参数 setDefaultValues()—即 false—表示您希望在 preferences XML 文件中设置默认值,前提是以前没有这样做过。Android 通过这个新 XML 文件的存在记住了这些信息。然而,即使你升级你的应用并添加具有新默认值的新设置,Android 也会记住,这意味着这个技巧不会设置那些新默认值。您的最佳选择是始终使用资源作为默认值,并在获取首选项的当前值时始终提供该资源作为默认值。
理解 ListPreference
列表首选项包含每个选项的单选按钮,默认(或当前)选项是预先选定的。期望用户选择一个且仅一个选项。当用户选择一个选项时,对话框会立即关闭,选择会保存在 preferences XML 文件中。图 11-4 显示了的样子。
图 11-4 。list preference 的用户界面
清单 11-4 包含一个 XML 片段,表示航班选项偏好设置。这一次,该文件包含对字符串和数组的引用,这是指定这些内容而不是对字符串进行硬编码的更常见方式。如前所述,存储在 /data/data/{package} 目录下的 XML 数据文件中的列表首选项的值与用户在用户界面中看到的不同。密钥的名称与用户看不到的隐藏值一起存储在数据文件中。因此,要让 ListPreference 工作,需要两个数组:显示给用户的值和用作键值的字符串。这是你容易犯错的地方。entries 数组保存向用户显示的字符串,而 entryValues 数组保存将存储在首选项数据 XML 文件中的字符串。
清单 11-4 。在 XML 中指定 ListPreference
<ListPreferenceandroid:key="@string/flight_sort_option"android:title="@string/listTitle"android:summary="@string/listSummary"android:entries="@array/flight_sort_options"android:entryValues="@array/flight_sort_options_values"android:dialogTitle="@string/dialogTitle"android:defaultValue="@string/flight_sort_option_default_value" />
两个阵列之间的元素在位置上彼此对应。也就是说, entryValues 数组中的第三个元素对应于 entries 数组中的第三个元素。用 0,1,2 等很有诱惑力。,因为 entryValues 但这不是必需的,而且当以后必须修改数组时,这可能会导致问题。如果我们的选项本质上是数字(例如,一个倒数计时器的起始值),那么我们可以使用 60、120、300 等值。只要对开发人员有意义,这些值根本不需要是数字;用户看不到这些值,除非您选择公开它们。用户只能看到第一个字符串数组 flight_sort_options 中的文本。本章的示例应用展示了这两种方式。
这里需要注意的是:因为 preferences XML 数据文件只存储值而不存储文本,所以如果您升级了应用并更改了选项的文本或者向字符串数组添加了项,那么在升级之后,preferences XML 数据文件中存储的任何值都应该与适当的文本对齐。在应用升级期间,会保留首选项 XML 数据文件。如果 preferences XML 数据文件中有 a “1” ,并且这意味着升级前的" # of Stops “,那么它在升级后仍然意味着” # of Stops "。
由于最终用户看不到 entryValues 数组,所以最好在应用中只存储一次。因此,创建一个且只有一个/RES/values/prefvaluearrays . XML 文件来包含这些数组。对于不同的语言或不同的设备配置,每个应用很可能会多次创建条目数组。因此,为您需要的每一种变化制作单独的 predisplayarrays . XML 文件。例如,如果您的应用将使用英语和法语,则英语和法语将有单独的 prefdisplayarrays.xml 文件。您不希望在这些其他文件中包含 entryValues 数组。不过,在 entryValues 和 entries 数组之间必须有相同数量的数组元素。元素必须对齐。当你做出改变时,要注意保持一切都在一条线上。清单 11-5 包含示例的 ListPreference 文件的源。
清单 11-5 。我们示例中的其他 ListPreference 文件
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/values/prefvaluearrays.xml -->
<resources>
<string-array name="flight_sort_options_values"><item>0</item><item>1</item><item>2</item>
</string-array>
<string-array name="pizza_toppings_values"><item>cheese</item><item>pepperoni</item><item>onion</item><item>mushroom</item><item>olive</item><item>ham</item><item>pineapple</item>
</string-array>
<string-array name="default_pizza_toppings"><item>cheese</item><item>pepperoni</item>
</string-array>
</resources><?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/values/prefdisplayarrays.xml -->
<resources>
<string-array name="flight_sort_options"><item>Total Cost</item><item># of Stops</item><item>Airline</item>
</string-array>
<string-array name="pizza_toppings"><item>Cheese</item><item>Pepperoni</item><item>Onions</item><item>Portobello Mushrooms</item><item>Black Olives</item><item>Smoked Ham</item><item>Pineapple</item>
</string-array>
</resources>
另外,不要忘记 XML 源文件中指定的默认值必须与 prefvaluearrays.xml 的数组中的 entryValue 相匹配。
对于 ListPreference,首选项的值是一个字符串。如果你使用数字串(例如,0,1,1138)作为的 entryValues ,你可以把它们转换成整数或者你在代码中需要的任何东西,就像在 flight_sort_options_values 数组中使用的那样。
您的代码可能想要显示来自首选项的条目数组的用户友好文本。这个例子采用了一个捷径,因为数组索引被用于 flight _ sort _ options _ values 中的元素。通过简单地将值转换成一个 int ,你就知道从 flight_sort_options 中读取哪个字符串。如果您为 flight _ sort _ options _ values 使用了其他值集,您将需要确定您偏好的元素的索引,然后使用该索引从 flight_sort_options 中获取您偏好的文本。 ListPreference 的 helper 方法 findindexoffvalue()可以对此有所帮助,它将索引提供给 values 数组,这样您就可以轻松地从 entries 数组中获取相应的显示文本。
现在回到清单 11-4 ,有几个字符串用于标题、摘要等等。名为的字符串 flight _ sort _ option _ default _ value 将默认值设置为 1 ,以表示示例中的“# of Stops”。为每个选项选择一个默认值通常是一个好主意。如果您没有选择默认值,也没有选择任何值,那么返回选项值的方法将返回空值。在这种情况下,您的代码必须处理空值。
了解编辑文本首选项
首选项框架还提供了一个名为 EditTextPreference 的自由格式文本首选项。该首选项允许您捕获原始文本,而不是要求用户做出选择。为了演示这一点,让我们假设您有一个为用户生成 Java 代码的应用。这个应用的首选项设置之一可能是用于生成的类的默认包名。在这里,您希望向用户显示一个文本字段,以便为生成的类设置包名。图 11-5 显示了 UI,清单 11-6 显示了 XML。
图 11-5 。使用编辑文本首选项
清单 11-6 。一个 EditTextPreference 的例子
<EditTextPreferenceandroid:key="package_name_preference"android:title="Set Package Name"android:summary="Set the package name for generated code"android:dialogTitle="Package Name" />
当选择“设置包名”时,会向用户显示一个对话框来输入包名。单击“确定”按钮时,首选项会保存到首选项存储中。
与其他首选项一样,您可以通过调用适当的 getter 方法来获取首选项的值,在本例中是 getString() 。
了解 MultiSelectListPreference
最后,Android 3.0 中引入了一个名为 multiselectlistposition 的偏好设置。这个概念有点类似于 ListPreference ,但是用户不是只能在列表中选择一个项目,而是可以选择几个或者一个都不选。在清单 11-1 中,multiselectlist preference 在 preferences XML 数据文件中存储一个标签,而不是单个值。与 multiselectlist preference 的另一个显著区别是,默认值是一个数组,就像 entryValues 数组一样。也就是说,对于该首选项,默认值数组必须包含零个或多个来自 entryValues 数组的元素。这也可以在本章的示例应用中看到;只需查看 /res/xml 目录中 main.xml 文件的结尾即可。
要获得一个 multiselectlist preference 的当前值,使用 SharedPreferences 的 getStringSet() 方法。要从 entries 数组中检索显示字符串,您需要遍历作为该首选项的值的字符串集,确定字符串的索引,并使用该索引从 entries 数组中访问适当的显示字符串。
更新 AndroidManifest.xml
因为示例应用中有两个活动,所以我们需要在 AndroidManifest.xml 中有两个活动标记。第一个是类别 LAUNCHER 的标准活动。第二个是针对一个 PreferenceActivity 的,所以根据意图的约定设置动作名称,并将类别设置为 PREFERENCE ,如清单 11-7 所示。你可能不希望 PreferenceActivity 出现在我们所有其他应用的 Android 页面上,这就是为什么你不使用 LAUNCHER 的原因。如果要添加其他偏好活动,您需要对 AndroidManifest.xml 进行类似的更改。
清单 11-7 。androidmanifest . XML 中的 PreferenceActivity 条目
<activity android:name=".MainPreferenceActivity"android:label="@string/prefTitle"><intent-filter><action android:name="com.androidbook.preferences.main.intent.action.MainPreferences" /><categoryandroid:name="android.intent.category.PREFERENCE" /></intent-filter></activity>
使用偏好类别
首选项框架支持您将首选项组织成类别。例如,如果您有很多偏好,您可以使用 PreferenceCategory ,它将偏好分组在一个分隔符标签下。图 11-6 显示了这可能是什么样子。注意名为“肉和“蔬菜的分隔符您可以在 /res/xml/main.xml 中找到这些的规范。
图 11-6 。使用偏好类别组织偏好
创建具有依赖关系的子首选项
另一种组织首选项的方法是使用首选项依赖关系。这在偏好之间创建了父子关系。例如,您可能有一个打开提醒的偏好设置;如果警报打开,可能会有几个其他与警报相关的首选项可供选择。如果主提醒首选项关闭,则其他首选项不相关,应被禁用。清单 11-8 显示了 XML,而图 11-7 显示了它的样子。
清单 11-8 。XML 中的首选项依赖
<PreferenceScreen><PreferenceCategoryandroid:title="Alerts"><CheckBoxPreferenceandroid:key="alert_email"android:title="Send email?" /><EditTextPreferenceandroid:key="alert_email_address"android:layout="?android:attr/preferenceLayoutChild"android:title="Email Address"android:dependency="alert_email" /></PreferenceCategory>
</PreferenceScreen>
图 11-7 。偏好依赖
带标题的首选项
Android 3.0 引入了一种新的方式来组织偏好。你可以在平板电脑的主设置应用下看到这个。因为平板电脑的屏幕空间比智能手机大得多,所以同时显示更多的偏好信息是有意义的。为此,您可以使用首选项标题。看一下图 11-8 。
图 11-8 。带有首选项标题的主设置页面
请注意,标题出现在左侧下方,就像一个垂直的标签栏。当您单击左侧的每个项目时,右侧的屏幕会显示该项目的首选项。在图 11-8 中,选择了声音,声音首选项显示在右侧。右边是一个 PreferenceScreen 对象,这个设置使用了片段。显然,我们需要做一些不同于本章所讨论的事情。
Android 3.0 最大的变化是在 PreferenceActivity 中添加了标题。这也意味着在 PreferenceActivity 中使用一个新的回调函数来设置标题。现在,当您扩展 PreferenceActivity 时,您会想要实现这个方法:
public void onBuildHeaders(List<Header> target) {loadHeadersFromResource(R.xml.preferences, target);
}
完整的源代码请参考 PrefDemo 示例应用。 preferences.xml 文件包含一些新标签,如下所示:
<preference-headersxmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"><header android:fragment="com.example.PrefActivity$Prefs1Fragment"android:icon="@drawable/ic_settings_sound"android:title="Sound"android:summary="Your sound preferences" />...
每个 header 标签指向一个扩展了 PreferenceFragment 的类。在刚刚给出的例子中,XML 指定了图标、标题和摘要文本(类似于副标题)。 Prefs1Fragment 是 PreferenceActivity 的内部类,看起来可能是这样的:
public static class Prefs1Fragment extends PreferenceFragment {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);addPreferencesFromResource(R.xml.sound_preferences);}
}
这个内部类需要做的就是拉入适当的首选项 XML 文件,如图所示。这个 preferences XML 文件包含了我们前面提到的首选项规范的类型,比如 ListPreference 、 CheckBoxPreference 、 PreferenceCategory 等等。非常好的是,当屏幕配置改变时,当偏好显示在小屏幕上时,Android 会注意做正确的事情。当屏幕太小而无法同时显示标题和右侧的偏好设置屏幕时,标题的行为类似于旧的偏好设置。也就是说,您只能看到标题;当您单击标题时,您只能看到相应的首选项屏幕。
首选屏幕
首选项的顶层容器是一个首选项屏幕。在平板电脑和 PreferenceFragment s 之前,你可以嵌套 PreferenceScreen s,当用户点击嵌套的 PreferenceScreen 项目时,新的 PreferenceScreen 将替换当前显示的 PreferenceScreen 。这在小屏幕上工作得很好,但是在平板电脑上看起来就不那么好了,尤其是如果你从标题和片段开始。您可能希望新的 PreferenceScreen 出现在当前片段所在的位置。
为了让 PreferenceScreen 在片段中工作,您需要做的就是为那个 PreferenceScreen 指定一个片段类名。清单 11-9 展示了样本应用中的 XML。
清单 11-9 。通过 PreferenceFragment 调用 PreferenceScreen
<PreferenceScreenandroid:title="Launch a new screen into a fragment"android:fragment="com.androidbook.preferences.main.BasicFrag" />
当用户点击此项时,当前片段被替换为 BasicFrag ,然后加载一个新的 XML 布局给 PreferenceScreen ,如 nested _ screen _ basic frag . XML 中所指定。在这种情况下,我们选择不使 BasicFrag 类成为 MainPreferenceActivity 类的内部类,主要是因为不需要来自外部类的共享,并向您展示如果您愿意,您可以这样做。
动态首选项摘要文本
您可能见过首选项摘要包含当前值的首选项。这实际上比你想象的要难实现一些。为了完成这一任务,您创建了一个侦听器回调函数,该函数检测首选项值何时会发生变化,然后相应地更新首选项摘要。第一步是让您的 PreferenceFragment 实现 OnPreferenceChangeListener 接口。然后您需要实现 onPreferenceChange() 回调。清单 11-10 显示了一个例子。回调中的 pkgPref 对象被提前设置为 onCreate() 方法中的首选项。
清单 11-10 。设置首选项监听器
public boolean onPreferenceChange(Preference preference,Object newValue) {final String key = preference.getKey();if ("package_name_preference".equals(key)) {pkgPref.setSummary(newValue.toString());}...return true;
}
您必须使用 setOnPreferenceChangeListener(this)在 onResume() 中将该片段注册为一个监听器,并在 onPause() 中通过使用 null 再次调用它来取消注册。现在,每当您注册的首选项有一个待定的更改时,这个回调将被调用,并传入首选项和潜在的新值。回调返回一个布尔值,指示是否继续将首选项设置为新值(真)或不设置(假)。假设您将返回 true 以允许新的设置,这也是您可以更新汇总值的地方。您也可以验证新值并拒绝更改。也许您希望一个 multiselectlistproperty 具有最大数量的选中项。您可以在回调中计算所选项目的数量,如果数量过多,则拒绝更改。
保存带有首选项的状态
首选项对于允许用户根据自己的喜好定制应用非常有用,但是我们可以使用 Android 首选项框架来做更多的事情。当您的应用需要跟踪应用调用之间的一些数据时,即使用户在首选项屏幕中看不到数据,首选项也是完成任务的一种方式。请查找名为 SavingStateDemo 的示例应用以及完整的源代码。
活动类有一个 getPreferences(int mode) 方法。实际上,这只是调用 getSharedPreferences() ,用活动的类名作为标签,加上传入的模式。结果是一个特定于活动的共享首选项文件,您可以用它来跨调用存储有关该活动的数据。清单 11-11 中显示了一个如何使用它的简单例子。
清单 11-11 。使用首选项保存活动的状态
final String INITIALIZED = "initialized";private String someString;[ ... ]SharedPreferences myPrefs = getPreferences(MODE_PRIVATE);boolean hasPreferences = myPrefs.getBoolean(INITIALIZED, false);if(hasPreferences) {Log.v("Preferences", "We've been called before");// Read other values as desired from preferences file...someString = myPrefs.getString("someString", "");}else {Log.v("Preferences", "First time ever being called");// Set up initial values for what will end up// in the preferences filesomeString = "some default value";}[ ... ]// Later when ready to write out valuesEditor editor = myPrefs.edit();editor.putBoolean(INITIALIZED, true);editor.putString("someString", someString);// Write other values as desirededitor.commit();
这段代码所做的是为我们的 activity 类获取一个对 preferences 的引用,并检查一个名为 initialized 的布尔“preference”是否存在。我们将“preference”写在双引号中,因为这个值不是用户将要看到或设置的;它只是一个值,我们希望存储在一个共享的首选项文件中,供下次使用。如果我们得到一个值,那么共享的首选项文件就存在,所以这个应用以前一定被调用过。然后,您可以从共享首选项文件中读取其他值。例如,someString 可以是一个活动变量,它应该在上次运行该活动时设置,或者如果这是第一次,则设置为默认值。
要将值写入共享首选项文件,您必须首先获得一个首选项编辑器。然后,您可以将值放入首选项,并在完成后提交这些更改。注意,在幕后,Android 正在管理一个真正共享的 SharedPreferences 对象。理想情况下,一次不会有超过一个编辑处于活动状态。但是调用 commit() 方法非常重要,这样才能更新 SharedPreferences 对象和共享首选项 XML 文件。在这个例子中, someString 的值被写出来,以便在下次运行这个活动时使用。
您可以随时访问、写入和提交值到您的首选项文件。可能的用途包括写出游戏的高分或记录应用最后一次运行的时间。您还可以使用不同名称的 getSharedPreferences() 调用来管理不同的首选项集,所有这些都在同一个应用甚至同一个活动中。
到目前为止,在我们的示例中,MODE_PRIVATE 用于模式。因为共享的首选项文件总是存储在您的应用的/数据/数据/{包} 目录中,因此其他应用无法访问,您只需使用模式 _ 私有。
使用对话框首选项
到目前为止,您已经看到了如何使用 preferences 框架的开箱即用功能,但是如果您想要创建一个定制的首选项呢?如果你想要一个类似于屏幕设置下亮度偏好设置的滑块的东西呢?这就是 DialogPreference 的用武之地。 DialogPreference 是 EditTextPreference 和 ListPreference 的父类。该行为是一个弹出的对话框,向用户显示选项,并通过按钮或后退按钮关闭。但是您可以扩展 DialogPreference 来设置您自己的自定义首选项。在您的扩展类中,您在 onDialogClosed() 中提供了自己的布局、自己的点击处理器和自定义代码,以便将您的首选项数据写入共享的首选项文件。
参考
以下是对您可能希望进一步探索的主题的有用参考:
- 【http://developer.android.com/design/patterns/settings.html】:安卓的设计设置指南。一些关于布局设置屏幕和选项的好建议。
developer.android.com/guide/topics/ui/settings.html
:安卓的 API 设置指南。本页描述了设置框架。developer . Android . com/Reference/Android/provider/settings . html
:引用页面,列出调用系统设置活动的设置常量。- :与本书相关的可下载项目列表。对于本章,请查找文件 pro Android 5 _ Ch11 _ preferences . zip。这个 ZIP 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,描述如何从这些 ZIP 文件之一将项目导入到您的 IDE 中。
摘要
本章讲述了在 Android 中管理偏好设置:
- 可用的首选项类型
- 将首选项的当前值读入您的应用
- 从嵌入式代码中设置默认值,并将默认值从 XML 文件写入保存的首选项文件
- 将首选项组织成组,并定义首选项之间的依赖关系
- 对首选项进行回调,以验证更改并设置动态摘要文本
- 使用首选项框架跨调用保存和恢复活动信息
- 创建自定义首选项
十二、为旧设备使用兼容性库
自从几年前首次推出以来,Android 平台已经经历了令人印象深刻的演变。虽然 Android 的意图一直是支持许多不同类型的设备,但它从一开始就不是为了满足这个目标而设计的。取而代之的是,谷歌的工程师们增加、删除和改变了 API 以提供新的特性。最大的变化之一是创建片段,以便处理更大的屏幕尺寸,如平板电脑和电视。但是还有其他的变化,比如动作栏和菜单。
新的 API 给开发人员带来了一个难题,他们希望自己的应用可以在带有新 API 的新设备上运行,也可以在没有这些 API 的旧设备上运行。许多老款设备无法升级安卓系统。即使谷歌将新的 API 添加到旧的 Android 操作系统的版本中,旧的设备也不会得到新的版本,因为设备制造商和移动运营商都需要测试和支持。谷歌想出的解决方案是创建兼容库,可以链接到应用中,这样它就可以利用新的 API 功能,但仍然可以在旧版本的 Android 上运行。该库指出了如何使用旧的 API 来实现新的特性。如果相同的应用运行在已经有这些新特性的新版 Android 上,兼容性库调用新版 Android 中的底层 API。
本章将深入探讨兼容性库,并解释如何使用它们以及需要注意什么。如果你没有为旧版本的 Android 开发应用,你可以安全地跳过这一章,因为你不需要这些库。只有当你想在一个没有新 API 的旧版本 Android 上运行的应用中包含新 API 的功能时,这些库才是有用的。
这一切都始于平板电脑
在支持平板电脑之前,Android 操作系统运行良好。应用的基本构件是活动,意味着为用户执行一个单一的任务,并填满设备的屏幕。但是平板电脑提供了更多的空间,因此用户可以在一个屏幕上同时看到和做一些事情。于是有了蜂巢(Android 3.0),谷歌引入了片段。这是一个全新的概念,它改变了开发人员创建 ui 的方式以及 ui 背后的逻辑。这本来很好,除了仍然有大量的 Android 设备(例如,智能手机)不支持片段。Google 发现可以编写一个兼容性库来提供片段的类似实现,等等。,它使用旧版本 Android 中的现有 API。如果一个应用链接到兼容性库中,它就可以使用片段,即使旧版本的 Android 在操作系统中不支持片段。
然后,谷歌工程师研究了新 Android 中的其他功能和 API,并提供了兼容库功能和 API 来匹配,以便这些功能也可以用于旧版本的 Android,而不必发布旧版本 Android 的更新。除了对片段的支持,兼容性库还提供对加载器、RenderScript、ActionBar 等的支持。
兼容性库并不总是让新旧版本完全相同。例如,新的 Activity 类知道片段。要使用兼容库,必须扩展 fragmentation Activity 类,而不是 Activity;正是 fragmentation activity 类在旧的 Android 版本中处理片段。
当您使用兼容性库时,您将为您的应用使用这些类,而不管它将在哪个版本的 Android 上运行。换句话说,你只需要在你的应用中使用 fragmentation activity,它将在所有版本的 Android 中做正确的事情,包括 Android 3.0 和更高版本。你不会试图在同一个应用中同时包含安卓 3.0 以上版本的活动和安卓 3.0 以下版本的片段活动。当 fragmentation Activity 在 Android 3.0 和更高版本上执行时,它几乎可以直接调用底层的 Activity 类。在最近的 Android 版本上使用兼容性库并没有真正的损失。
将库添加到项目中
在撰写本文时,有四个兼容性库;该集合一起被称为 Android 支持库,修订版 22.1.1:
- v4—包含 fragmentation activity, Fragment , Loader ,以及 Android 3.0 之后引入的相当多的其他类。数字 4 代表 Android API 版本 4(即 Donut 1.6)。这意味着这个库可以用于运行在 Android API 版本 4 及以上的应用。
- V7—使 ActionBar 、CardView、GridLayout、MediaRouter 、Palette 和 RecyclerView 类可用。该库可用于 Android API 版本 7(即艾克蕾尔 2.1)及以上版本。这里实际上有六个库:appcompat、cardview、gridlayout、mediarouter、palette 和 recyclerview
- V8—将 RenderScipt 功能添加到 Android API 版本 8(即 Froyo 2.2)及更高版本。RenderScript 允许跨设备处理器(CPU 内核、GPU、DSP)的工作并行化,并在 Android API 版本 11(即 Honeycomb 3.0)中引入。
- V13——为选项卡式和页面式界面添加了一些特殊的片段功能。这个库还包含许多来自 v4 的类,因此它可以包含在您的应用中,而不需要其他库。
- v17—添加了与 Android 电视应用相关的向后倾斜功能
有关按版本号列出的所有兼容性功能的完整列表,请参见本章末尾的参考资料。
要将 Android 支持库下载到您的计算机上,请使用 Android SDK 管理器,并在 Extras 下的列表底部找到它。如果你使用的是 Android Studio,下载 Android 支持库。否则,请下载 Android 支持库。这些文件将放在您的 Android SDK 目录下。android 支持库可以在 extras/android/support/ 中找到,Android 支持库可以在 extras/Android/m2 Repository 中找到。
正如你在前面的列表中看到的,并不是所有 Android 支持库的特性都可以在所有旧版本的 Android 上使用。因此,您必须在 AndroidManifest.xml 文件中正确设置 android:minSdkVersion 。如果你用的是 v7 的兼容库功能, android:minSdkVersion 应该不低于 7。
包括 v7 支持库
您很少会想要包含 v4 库而不包含 v7 库。因为 v7 库要求也包含 v4 库,以便为 v7 正常运行提供必要的类,所以您希望两者都包含。如果您使用的是 Eclipse,ADT 插件会让这一切变得非常简单。当您在 Eclipse 中创建新的 Android 项目时,您需要指定它将运行的 Android 的最低版本。如果 ADT 认为您可能希望包含兼容性库,它会自动包含它。
例如,如果您指定目标 SDK 为 16 (JellyBean 4.1),但最小 SDK 为 8 (Froyo 2.2),ADT 将自动设置一个 appcompat v7 库项目,将该库项目包括在您的新应用中,并且还将 v4 库包括在您的应用中。因此,v7 库中的资源可供您的应用使用,而无需您做额外的工作。然而,如果您想使用另外两个 v7 库(gridlayout 和/或 mediarouter)中的任何一个,就需要做一些额外的工作,下面将会解释。通过创建一个库项目并将其包含在您的应用中,它将包含您的应用将需要的兼容性库资源。
您将手动执行与 ADT 类似的操作,将 v7 appcompat 库自动包含到您的项目中。首先,您将选择文件导入,然后将现有 Android 代码导入工作区,然后导航到工作站上 Android SDK 所在的 extras 文件夹。找到 v7 gridlayout 或 mediarouter 文件夹并选择它。参见图 12-1 。
图 12-1 。导入 v7 mediarouter 兼容性库
单击 Finish,您将获得一个新的库项目。如果您选择为 v7 mediarouter 创建一个库项目,您会发现它缺少一些功能,因此有错误。您需要添加 v7 appcompat 库来消除这种情况。在 Eclipse 中右键单击 mediarouter 库项目,然后选择 Properties。在左边的列表中选择 Android。现在,单击库部分中的添加…按钮。参见图 12-2 。
图 12-2 。将 appcompat_v7 添加到 v7 mediarouter 兼容性库中
选择 appcompat_v7 库,然后单击确定。这应该可以清除 mediarouter 中的错误。现在,当您想要将 mediarouter 包含在您的应用项目中时,只需遵循相同的过程,但右键单击您的应用项目,当您单击“库”的“添加…”按钮时,选择 mediarouter 库。
有了 Android Studio,添加 v7 兼容库也一样简单。默认情况下,如果您创建的新项目的最小 SDK 值小于您的目标 SDK,您很可能会自动添加 v7 appcompat 库。您可以通过在应用的 build.gradle 配置文件的 dependencies 部分中查找以下行来检查这一点:
编译’ com . Android . support:app compat-V7:22 . 0 . 0 ’
因此,要添加另一个 v7 库,需要在 dependencies 部分插入另一个类似的编译行,但是要使用适当的名称,比如 cardview 或 mediarouter。
包括 v8 支持库
如果您想要使用 v8 renderscript 兼容性库,并且您使用 Eclipse 进行开发,那么您只需将以下三行添加到应用项目的 project.properties 文件中,而不管您的应用的目标版本如何:
renderscript.target=22
renderscript.support.mode=true
sdk.buildtools=22.1.1
在撰写本文时,在线 Android 文档说您应该使用 18 的目标和 18.1.0 的构建工具。但是,使用旧值会产生一个错误,要求使用 buildtools 的新版本。如果您在 Eclipse 控制台中看到关于版本号的错误,请尝试使用错误所指示的更高版本。
如果您使用 Android Studio 进行开发,要包含 v8 renderscript,您需要编辑应用的 build.gradle 文件,并在 defaultConfig 部分添加以下行:
renderscriptTargetApi 22
renderscriptSupportModeEnabled true
在您的代码中,确保您从 Android . support . v8 . renderscript 而不是 android.renderscript 导入。如果您正在修改 V8 库的现有 render script 应用,请确保清理您的项目。从您的生成的 Java 文件。rs 文件需要重新生成才能使用 v8 库。您现在可以像往常一样使用 RenderScript,并将您的应用部署到旧版本的 Android 上。
包括 v13 支持库
要使用 Eclipse 将 v13 兼容性库包含到您的应用中,请导航到 SDK extras 目录并找到 v13 jar 文件。将该文件复制到应用项目的/libs 目录中。一旦 v13 jar 文件就位,右键单击它以弹出菜单,然后选择构建路径 Add to Build Path。由于 ADT 的帮助,您的应用中很可能已经有了 v4 和 v7 appcompat 库。如果您不需要其中任何一个的功能,您可以选择去掉它们。例如,如果您的应用的最低 SDK 是 v11,您可以使用本机 ActionBar 类,而不需要 v7 appcompat 支持库。
v13 jar 文件包含许多与 v4 相同的类,所以您不希望因为在两个版本中包含相同的类而导致任何问题。如果您要在应用中使用所有三个库(即 v4、v7 和 v13),那么至少要确保 v13 在 v4 之前排序。这可以在“配置构建路径”对话框中完成。
如果您使用的是 Android Studio,只需确保 SDK 管理器已经下载了支持库,然后将以下编译行添加到应用的 build.gradle 文件中,就像您对 v7 库所做的那样:
编译“com . Android . support:support-v 13:22 . 0 . 0”
包括 v17 支持库
最后,包含 v17 兼容性库的方式与包含 v13 支持库的方式相同。
仅包括 v4 支持库
如果您真的必须有 v4 支持库,而没有其他库,那么您将遵循与 v13 库相同的过程。
用 Android 支持库改造应用
为了更好地了解这一切是如何工作的,你将带回你在第八章中开发的一个片段应用,并使它适用于不支持片段的旧版本 Android。
使用文件导入,选择常规,然后将现有项目导入工作区。从第八章的中导航到莎士比亚乐器项目并选择它。在点击“完成”之前,选中“将项目复制到工作区”。
现在,您将对这个应用进行改进,以便在低于 API 版本 11 的 Android 版本上工作。当您不需要兼容性库中的资源时,下面的代码可以工作,因为它只关心 JAR 文件中的复制。
- 右键单击您的项目并选择 Android 工具
添加支持库…接受许可,然后单击确定。
- 现在进入 MainActivity.java,将基类从活动更改为片段活动。您需要修复从 android.app.Activity 到 Android . support . v4 . app . fragmentation activity 的导入行。还要修复片段、片段管理器和片段事务的导入,以使用来自支持库的导入。
- 找到对 getFragmentManager() 的方法调用,并将其更改为 getSupportFragmentManager()。对 DetailsActivity.java 也这样做。
- 对于,将片段的导入改为支持库片段的导入(即 Android . support . v4 . app . Fragment)。
- 在 TitlesFragment.java,将 ListFragment 的导入改为支持库 ListFragment 的导入(即 Android . support . v4 . app . list fragment)。
新版本的 Android 使用与旧版本不同的动画制作人员。您可能需要修复 showDetails() 方法中 MainActivity.java 的动画。选择一个注释掉的调用 setCustomAnimations(),然后播放输入和输出动画。任何依赖于 ObjectAnimator 类的东西都不能在旧设备上工作,因为这个类是在 API 版本 11(即 Honeycomb 3.0)中引入的。它会编译,但是由于这个类没有在旧的 Android 中实现,也没有包含在兼容性库中,你会得到一个运行时异常。换句话说,避免使用 R.animator,尝试使用 R.anim。你可以把你想用的动画资源文件复制到你的项目中,或者你可以试着参考一下 android。R.anim 文件。
现在你可以进入 AndroidManifest.xml 并将 minSdkVersion 从 11 改为 8。那应该是你需要做的全部。尝试在 Froyo 设备或模拟器上运行此应用。如果一切顺利,您现在应该会看到一个基于片段的应用运行在 Android 3.0 之前的操作系统上。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
Developer . android . com/tools/Support-Library/index . html
:支持库包上的 Android 开发者指南。developer . android . com/tools/support-library/features . html
:各兼容库主要特性的 Android 文档。developer . android . com/tools/support-library/setup . html
:关于为您的项目设置兼容库的 Android 文档,同时适用于 Eclipse 和 Android Studio。在写这篇文章的时候,这些页面并不像本章一样流行。然而,事情发生了变化。如果您遇到问题,请查看在线文档或联系该书的作者。
摘要
让我们通过快速列举你对 Android 兼容性库的了解来结束本章:
- 要让您的应用在最广泛的设备上工作,请使用它们的 API 的兼容性库和代码,而不是最新和最好的 API。
- v7 支持库附带了一些资源,这些资源必须包含在您的应用中,API 才能正常工作。
十三、探索包、进程、线程和处理器
在本书中,我们已经关注了如何为 Android 平台编程的要点。在这一章中,我们想深入探讨一下 Android 程序的进程和线程模型。这个讨论将引导我们对包进行签名,在包之间共享数据,使用编译时库,Android 组件的性质以及它们如何使用线程,最后是对处理器的需求以及如何编写处理器。
当你阅读这一章时,请记住“包”这个词已经用得太多了。有时它指的是 Java 语言包,有时它指的是 Android 应用部署为的 APK 文件。
了解包和过程
我们将从 Android 包和流程模型开始。当你在 Android 中开发一个应用时,你最终会得到一个。apk 文件。你签这个。apk 文件并将其部署到设备上。每个。apk 文件由一个惟一的 java 语言风格的包名惟一标识,如清单 13-1 中的清单文件所示。
清单 13-1 。在清单文件中提供包名
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"package="com.androidbook.testapp"...>...rest of the xml nodes
</manifest>
如果您是这个包的开发者,那么一旦这个应用被部署,除了您之外,没有人能够更新它。Android 应用包名称是为您保留的。当你在不同的应用发行商那里注册你的应用时,就会发生这种捆绑。因此,选择这个 Android 应用包的名称非常类似于 Java 包的命名方式。这需要是世界上独一无二的。一旦发布了应用,就不能更改此包名称,因为它定义了应用的身份。
Android 使用包名作为运行这个包的组件的进程名。Android 还为这个进程分配了一个唯一的用户 id。这个用户 ID 实际上是底层 Linux 操作系统的用户 ID。由于此用户 ID 是在特定设备上安装时确定的,因此在安装应用的每个设备上都会有所不同。您可以通过 Android 模拟器中的开发人员工具查看已安装软件包的详细信息来发现这些信息。例如,已安装浏览器应用的包详细信息屏幕看起来像图 13-1 。(请注意,在不同的版本中,您查找的图像或工具可能会有所不同。图 13-1 中的图片取自 Android 模拟器上的开发者工具应用。)
图 13-1 。安卓包详情
图 13-1 显示了由清单文件中的 Java 包名和分配给该包的唯一用户 ID 所指示的进程名。这个进程或包创建的任何资源都将在这个 Linux 用户 ID 下得到保护。该屏幕还列出了该包中的组件。组件的例子有活动、服务和广播接收器。请注意,此图像可能会因 Android 版本而异。通过设备或模拟器的设置,您还可以卸载软件包,以便可以删除它。
因为进程与包名相关联,而包名与其签名相关联,所以签名在保护属于包的数据方面起着一定的作用。包通常用自签名 PKI(公钥基础设施)证书签名。证书识别包的作者是谁。这些证书不需要由证书颁发机构颁发。这意味着证书中的信息未经任何权威机构批准或验证。这意味着人们可以创建一个证书,表明他们的名字是谷歌。唯一的保证是,如果之前没有人在市场上要求该包名称,则该包名称将保留给该用户,并且对该包的任何后续更新将仅提供给该用户(由该证书标识)。
通过该包安装或创建的所有资产都属于其 ID 被分配给该包的用户。如果您的意图是允许一组依赖于一组公共数据的协作应用,那么您可以选择显式地指定一个对您来说是唯一的并且对您的需求来说是公共的用户 ID。这个共享用户 ID 也在清单文件中定义,类似于包名的定义。清单 13-2 显示了一个例子。
清单 13-2 。共享用户 ID 声明
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"package="com.androidbook.somepackage"sharedUserId="com.androidbook.mysharedusrid"...
>
...the rest of the xml nodes
</manifest>
如果多个应用共享相同的签名(用相同的 PKI 证书签名),则它们可以指定相同的共享用户 ID。拥有一个共享的用户 ID 允许多个应用共享数据,甚至在同一个进程中运行。为了避免共享用户 id 的重复,请使用类似于命名 Java 类的约定。以下是 Android 系统中共享用户 id 的一些例子:
"android.uid.system"
"android.uid.phone"
注意共享 ID 必须指定为原始字符串,而不是字符串资源。
注意,如果您计划使用共享用户 id,建议从一开始就使用它们。否则,当您将应用从非共享用户 ID 升级到共享用户 ID 时,它们就不能很好地工作。其中一个被引用的原因是,由于用户 id 的改变,Android 将不会在旧资源上运行 chown 。
用于共享数据的代码模式
本节探讨两个应用通过使用共享用户 ID 来共享资源和数据的机会。在运行时,每个包的资源和数据由该包的上下文拥有和保护。您需要访问想要从中共享资源或数据的包的上下文。
您可以在任何现有的上下文对象(比如您的活动)上使用 createPackageContext()API 来获取对您想要与之交互的目标上下文的引用。清单 13-3 提供了一个例子。
清单 13-3 。使用 createPackageContext()API
//Use the appropriate try/catch to detect errors
//Identify package you want to use
String targetPackageName="com.androidbook.samplepackage1";//Decide on an appropriate context flag
int flag=Context.CONTEXT_RESTRICTED;//Get the target context through one of your activities
//Need to catch NameNotFoundException
Activity myContext = ......;
Context targetContext =myContext.createPackageContext(targetPackageName, flag);//Use context to resolve file paths
Resources res = targetContext.getResources();
File path = targetContext.getFilesDir();
请注意我们如何能够获得对给定包名的上下文的引用,比如 com . androidbook . sample package 1。清单 13-3 中的目标上下文与应用启动时传递给目标应用的上下文相同。正如方法名所示(在其“create”前缀中),每个调用都返回一个新的上下文对象。然而,文档向我们保证,这个返回的上下文对象被设计成轻量级的,这意味着它不会消耗很多内存,并且被优化为引用目标包的资源、资产和代码。
无论两个上下文是否共享一个用户 ID,该 API 都适用。如果您共享用户 ID,那当然很好。如果不共享用户 ID,目标应用将需要声明外部用户可以访问它的资源。
CONTEXT_RESTRICTED 标志表示您感兴趣的只是加载资源和资产,而不是代码。因此,使用这个标志允许系统检测布局是否包含对回调代码的引用。回调的一个例子是布局中的一个按钮,该按钮引用一个将被调用的方法。此回调代码存在于源上下文中。因此,您可能希望系统抛出一个异常,这样您就可以检测到该情况或者忽略该特定的 XML 标记。本质上,您是在告诉系统,您在受限的意义上使用上下文,并且目标上下文可以基于该标志自由地做出合适的假设。底线似乎是,如果您不想使用来自目标上下文的代码,就使用这个标志。
CONTEXT_INCLUDE_ 代码 允许你在运行时将 Java 类从目标上下文加载到你的进程中,并调用该代码。文档表明,如果加载代码不安全,您可能会收到安全异常。但是,不清楚在什么情况下代码被认为是不安全的。一个有根据的猜测是,目标上下文没有与源上下文共享的用户 ID。您可以通过同时指定 CONTEXT_IGNOR_SECURITY 和 CONTEXT_INCLUDE_CODE 来克服这个限制。这两个标志一起始终将目标上下文代码加载到源上下文代码中,即使目标上下文属于不同的用户,也会被忽略。尽管代码是借用的,并在客户端进程中运行,但它对目标上下文数据没有权限。因此,请确保代码在您的数据上释放时会做什么。这种方法通常用于可以共享的工具代码。
了解图书馆项目
当我们谈论共享代码和资源时,一个值得问的问题是,“库”项目的想法有帮助吗?从 ADT 0.9.7 Eclipse 插件开始,Android 支持库项目的思想。从那时起,构建库的方法已经有了一些改变,而中心思想仍然保留在所有最近的版本中。
库项目是 Java 代码和资源的集合,看起来像一个常规的 Android 项目,但绝不会以结束。apk 文件本身。相反,一个库项目的代码和资源成为另一个项目的一部分,并被编译到主项目的中。apk 文件。由于库纯粹是一个编译时概念,每个开发工具可能会以不同的方式创建这个工具。
以下是关于这些图书馆项目的一些其他事实:
- 一个库项目可以有自己的不同于主应用的包名。
- 库项目可以使用其他 JAR 文件。
- Eclipse ADT 将把库 Java 源文件编译成一个 JAR 文件,然后用应用项目编译这个 JAR 文件。
- 除了 Java 文件(它变成了一个 jar 文件)之外,属于一个库项目的其余文件(比如资源)都保存在库项目中。为了编译包含库作为依赖项的应用项目,库项目是必需的。
- 从 SDK Tools 15.0 开始,为库项目在其各自的 R.java 文件中生成的资源 id 不是最终的。(这将在本章后面解释。)
- 库项目和主项目都可以通过各自的 R.java 文件访问库项目中的资源。这意味着 ID 名称是重复的,并且在两个 R.java 文件中都可用。
- 如果您想区分两个项目(library 和 main)之间的资源 id,您可以使用不同的资源前缀,例如库项目资源的 lib_ 。
- 主项目可以引用任意数量的库项目。
- 需要在目标主项目清单文件中定义库的组件,如活动。完成后,库包中的组件名必须用库包名完全限定。
- 没有必要在库清单文件中定义组件,尽管快速了解它支持哪些组件可能是一个好的做法。
- 创建一个库项目首先要创建一个常规的 Android 项目,然后在其属性窗口中选择 Is Library 标志。
- 您也可以通过项目属性屏幕设置主项目的从属库项目。
- 显然,作为一个库项目,任何数量的主项目都可以包含一个库项目。
- 从发行版(Android 4.4、API 19、SDK Tools 19、ADT 22.3)开始,一个库项目不能引用另一个库项目,尽管似乎希望在未来的发行版中能够这样做。
要创建一个库项目,首先要创建一个常规的 Android 项目。建立项目后,右键单击项目名称,然后单击属性上下文菜单,显示库项目的属性对话框。该对话框如图 13-2 中的所示。(此图中可用的构建目标可能因您的 Android SDK 版本而异。)只需从该对话框中选择 Is Library,将该项目设置为库项目。
图 13-2 。将项目指定为库项目
你可以使用下面的项目属性对话框(见图 13-3 )来表明一个主项目依赖于先前创建的库项目。
图 13-3 。声明库项目依赖关系
请注意对话框中的“添加”按钮。你可以用这个来添加图 13-3 中的库作为参考。你不需要做任何其他事情。
一旦库项目被设置为主应用项目的依赖项,库项目就会作为编译后的 JAR 文件出现在应用项目的节点 Android Dependencies 下。
Android 不会将库中的 R.class 文件打包到各自的 jar 文件中。相反,它依赖于源文件【R.java】,该文件被重新创建并在每个库的主应用项目中可用。这意味着在主项目的 gen 子目录中,每个库都有一个 R.java 文件。
为了避免库的编译源代码中出现硬编码的常量,Android 创建了库 R.java 文件,使得该文件中的所有常量都是非最终的。在主项目的最终编译过程中,会分配新的常数值,以便这些常数值在所有库和主项目中是唯一的。如果我们在库编译期间给定了最终的常量值,那么这些数字可能会在库之间发生冲突。给定名称集的唯一 id 分配必须一次性完成。一旦这些数字在主项目的编译期间被分配给 IDs,它们就可以成为该主项目中的最终数字。
库的 R.java 文件中的 id 不是最终的,这一事实暗示了这一点。通常使用开关语句来响应基于菜单项 ID 的菜单项。如果 id 不是最终的,那么在库代码中完成时,这个语言构造将在编译时失败。这是因为 switch 子句中的 case 语句必须是一个数字常量。
所以,清单 13-4 中的 switch 语句不会编译,除非 id(如 R.id.menu_item_1 )是实际的文字数字或静态的终结符。
清单 13-4 。示例 switch 语句演示非最终变量
switch(menuItem.getItemId()) {case R.id.menu_item_1:Statement1;break;case 0x7778888: // as an example for R.id.menu_item_2:statement;statement;break;default:statement;statement;
}
因为 id 被定义为非最终的库项目,我们被迫使用 if/else 语句,而不是 switch/case 子句。因为从库的 R.java 文件中重新创建的相同常量是最终的,所以你可以在你的最终项目中自由使用开关子句。
如您所见,库项目是编译时构造。显然,任何属于这个库的资源都会被吸收并合并到主项目中。不存在运行时共享的问题,因为只有一个包文件与主包同名。简而言之,库提供了一种在编译时在相关项目之间共享资源的方式。
了解组件和线程
我们从这一章开始,确定每个包在它自己的进程中运行。我们现在将解释这个过程中线程的组织。这将引导我们为什么我们需要处理器来卸载主线程的工作,并与主线程通信。
Android 应用中的大多数代码都运行在组件的上下文中,比如一个活动或一个服务。大多数时候,一个 Android 进程中只有一个线程在运行,称为主线程。我们将讨论在不同组件之间共享这个主线的含义。首先,这可能导致应用不响应(ANR)消息(“A”代表“应用”,而不是“烦人”)。我们将向您展示当需要长时间运行的操作时,如何使用处理器、消息和线程来打破对主线程的依赖。
Android 流程有四种主要的组件类型:活动、服务、内容提供者和广播接收者。您在 Android 应用中编写的大多数代码都是这些组件之一的一部分,或者由这些组件之一调用。在 Android 项目清单文件中的应用节点规范下,每个组件都有自己的 XML 节点。回想一下,清单 13-5 中的这些节点是:
清单 13-5 。如何在清单文件中声明组件
<manifest...><application><activity/><service/><receiver/><provider/></application>
</manifest>
除了一些例外(比如对内容提供者的外部进程调用),Android 使用相同的线程来处理(或运行)这些组件中的代码。这个线程被称为应用的主线程。当调用这些组件时,调用可以是同步调用,例如当您调用内容提供者获取数据时,也可以是通过消息队列的延迟调用,例如当您通过调用启动服务或显示对话框来调用功能时。
图 13-4 描述了螺纹和这四个部件之间的关系。这张图展示了线程是如何在 Android 框架及其组件中交织的。该图没有指出线可能穿过各种部件的顺序。该图仅仅示出了处理以连续的方式从一个组件继续到另一个组件。
图 13-4 。 Android 组件和线程框架
如图 13-4 所示,主螺纹做重提升。它通过使用消息队列贯穿所有组件。当您在设备屏幕上选择菜单或按钮时,设备会将这些操作转换为消息,并将其放在处于焦点的流程的主队列中。主线程位于一个循环中,处理每条消息。如果任何消息超过 5 秒左右,Android 就会抛出一条 ANR 消息。
同样,在响应菜单项时,如果您要调用广播消息,Android 会再次在包进程的主队列中删除一条消息,注册的接收者将从该包进程中被调用。主线程将在以后调用该消息来调用接收方。主线程也为广播接收器工作。如果主线程忙于响应菜单动作,广播接收器将不得不等待,直到主线程被释放。
服务也是如此。当您从一个菜单项用 activity . start service 启动一个本地服务时,一条消息被放到主队列中,主线程将通过服务代码来处理它。
对本地内容供应器的调用略有不同。对于本地调用,内容提供者仍然在主线程上运行,但是对它的调用是同步的,并且不使用消息队列。
你可能会问,“为什么 Android 应用中的大部分代码运行在主线程或其他线程上很重要?”这很重要,因为主线程有责任回到它的队列,以便 UI 事件得到响应。因此,您不应该阻塞主线程。如果有些事情要花五秒以上的时间,你应该在一个单独的线程中完成它,或者通过请求主线程在它从其他处理中释放出来时返回来推迟它。
当流程之外的外部客户端或组件调用内容提供者获取数据时,会从线程池中为该调用分配一个线程。连接到服务的外部客户端也是如此。
让我们在下一节看看什么是处理器以及它们是如何工作的。
了解处理器
我们简单地提到了如果需要的话,推迟一个主线程的工作的想法。这是通过处理器完成的。处理器在 Android 中被广泛使用,所以主 UI 线程不会被阻塞。它们还在与其他派生工作线程中的主线程进行通信时发挥作用。
处理器是一种在主队列(更准确地说,是附加到处理器在其上被实例化的线程的队列)上丢弃消息的机制,以便该消息可以在稍后的时间点被循环线程处理。被丢弃的消息有一个指向丢弃它的处理器的内部引用。
当主线程开始处理该消息时,它通过 handler 对象上的回调方法调用丢弃该消息的处理器。这个回调方法叫做 handleMessage 。图 13-5 展示了处理器、消息和主线程之间的关系。
图 13-5 。处理器、消息、消息队列关系
图 13-5 展示了当我们谈论处理器时,一起工作的关键角色:主线程、主线程队列、处理器和消息。在这四种方法中,我们没有直接暴露给主线程或队列。我们主要处理 h 和 l 对象和 m 消息对象。即使在这两者之间,h 和 ler 对象协调大部分工作。
虽然处理器允许我们将消息放到队列中,但实际上是消息对象保存了对处理器的引用。m 消息对象也保存了一个可以传递回处理器的数据结构。
通过一个例子可以更好地理解处理器和消息。例如,我们将有一个调用一个函数的菜单项,该函数依次以一秒的间隔执行一个动作五次,并且每次都向调用活动报告。
如果我们不介意阻塞主线程,我们可以像清单 13-6 中的伪代码一样编写这个场景。
清单 13-6 。 用睡眠法压着主线程
public class SomeActivity {....other methodsvoid respondToMenuItem() {//Prove that we are on the main threadUtils.logThreadSignature();//simulate an operation that takes longer than 5 secondsfor (int i=0;i<6;i++) {sleepFor(1000);// put main thread to sleep for 1 secdosomething();SomeTextView.setText("did something. Counter:" + Integer.toString(i));}}
}
这将满足用例的需求。然而,如果我们这样做了,我们就抓住了主线,我们保证会有一个 ANR。我们可以使用一个处理器来避免前面例子中的 ANR。通过一个处理器来做这件事的伪代码将看起来像清单 13-7 中的。
清单 13-7 。从主线程实例化一个处理器
void respondToMenuItem(){SomeHandlerDerivedFromHandler myHandler =new SomeHandlerDerivedFromHandler();myHandler.doDeferredWork(); //invoke a function in 1 sec intervals//note that doDeferredWork() is not part of the SDK//we will show you the code for this shortly
}
现在,调用 respontomenuitem()将允许主线程返回到它的循环中。实例化的处理器知道它是在主线程上被调用的,并将自己挂接到队列上。方法 doDeferredWork() 将调度工作,以便主线程一旦空闲就可以回到这个工作。
为了研究这个协议,让我们来看看正确的处理器的实际源代码。下一节清单 13-8 中的代码演示了这个处理器,它被称为 DeferWorkHandler 。在前面清单 13-7 的伪代码中,指示的处理器 SomeHandlerDerivedFromHandler 相当于这个 DeferWorkHandler 。类似地,在清单 13-8 中的 DeferWorkHandler 上实现了(清单 13-7 中的 doDeferredWork() )所示的方法。
清单 13-8 。DeferWorkHandler 源代码
public class DeferWorkHandler extends Handler {//Keep track of how many times we sent the messageprivate int count = 0;//A parent driver activity we can use to inform of status.private TestHandlersDriverActivity parentActivity = null;//During construction we take in the parent driver activity.public DeferWorkHandler(TestHandlersDriverActivity inParentActivity){parentActivity = inParentActivity;}//Callback method that gets called by the main thread@Overridepublic void handleMessage(Message msg) {//Use the message object to get to its dataString pm = new String("message called:" + count + ":" +msg.getData().getString("message"));//you can access the parent activity and invoke UI calls on it hereparentActivity.someControl.somemethod(); //example only//logic to invoke itself multiple times if neededif (count > 5) {return;}count++; //increment countsendTestMessage(1); //reinvoke again by sending a message}//method called by the clientpublic void doDeferredWork() {count = 0;sendTestMessage(1);}//Preparing and sending the messagepublic void sendTestMessage(long interval) {Message m = this.obtainMessage();prepareMessage(m);this.sendMessageDelayed(m, interval * 1000);}public void prepareMessage(Message m) {Bundle b = new Bundle();b.putString("message", "Hello World");m.setData(b);return ;}
}
让我们来看看这段源代码的关键方面。第一个是处理器是从基类 h 和 ler 派生的。在处理器的构造函数中,我们使用一个指向父活动的指针,这样我们就可以使用活动的 UI 控件来报告需要报告的内容或需要采取的行动。然后我们编写一个方法( doDeferredWork )来封装这个处理器要为我们做什么。请注意, doDeferredWork ()不是一个被覆盖的方法,您可以随意调用这个方法。正是在这种方法中,您使用消息来最终调用被覆盖的 handleMessage ()。此外,正是在这个 handleMessage ()中,您实际上放置了最初从主线程推迟的真正代码。
基本处理器提供了一系列方法来将消息发送到队列,以便稍后进行响应。这些方法在 doDeferredWork ()中使用。 sendMessage() 和 sendMessageDelayed() 是这些发送方法的两个例子。我们在示例中使用的 sendMessageDelayed() ,允许我们以给定的时间延迟在主队列中丢弃一条消息。相反,当主线程找到消息时,sendMessage() 会立即丢弃该消息进行处理。
当您调用 sendMessage() 或 sendMessageDelayed() 时,您将需要 mmessage 对象的一个实例。最好是你让处理器给你,因为当处理器返回 m 消息对象时,它把自己藏在 m 消息的肚子里。这样,当主线程出现时,它就知道只根据消息调用哪个处理器。在清单 13-8 中,使用以下代码获得消息:
Message m = this.obtainMessage();
这个引用的变量是 handler 对象实例。顾名思义,该方法不创建新消息,而是从全局消息池中获取一个消息。稍后,一旦该消息被处理,它将被回收。方法获得消息() 的变化如清单 13-9 所示。
清单 13-9 。通过处理器构造消息
obtainMessage();
obtainMessage(int what);
obtainMessage(int what, Object object);
obtainMessage(int what, int arg1, int arg2)
obtainMessage(int what, int arg1, int arg2, Object object);
每个方法变体都在消息对象上设置相应的字段。当消息跨越流程边界时,对 object 参数有一些限制。在这种情况下,需要打包。在这种情况下,在消息对象上显式使用 setData() 方法要安全得多,也更兼容,该方法需要一个包。在清单 13-8 中,我们使用了 setData() 。如果您打算传递的是可以用整数值容纳的简单指示符,鼓励您使用 arg1 或 arg2 来代替。
参数 what (在清单 13-9 中)允许您将消息出队或查询队列中是否有这种类型的消息。详见 h 和 ler 类的操作。
一旦我们从处理器获得消息,我们就可以有选择地修改该消息的数据内容。在我们的例子中,我们使用了 setData() 函数,将它传递给一个 Bundle 对象。在我们对消息的数据进行分类或识别后,我们可以通过 sendMessage() 或 sendMessageDelayed() 将消息发送到队列中。当调用这些方法时,主线程将返回到队列中。
一旦消息被发送到队列,处理器就会等待(形象地说)直到主线程检索到这些消息并调用处理器的 handleMessage() 。
如果您想更清楚地看到这个处理器和主线程的交互,您可以在发送消息时和在 handleMessage() 回调中编写一个 logcat 消息。您会注意到时间戳有所不同,因为主线程会多花几毫秒的时间返回到 handleMessage() 方法。
在我们的示例中,每个 handleMessage() 在处理完一条消息后,将另一条消息发送到队列,以便可以再次调用它。它这样做五次,当计数器达到 5 时,它停止向队列发送消息。这是将工作分成多个块的一种方法,尽管有更好的方法来完成这个任务,要么通过一个工人线程,要么通过一个类 AsyncTask 。基本的 异步任务将在下一章讨论。现在让我们简要地介绍一下显式工作线程选项。
使用工作线程
当我们使用上一节中的处理器时,代码仍然在主线程上执行。对 handleMessage() 的每个调用仍然应该在主线程规定的时间内返回(换句话说,每个消息调用应该在五秒钟内完成,以避免应用不响应)。如果您的目标是进一步延长执行时间,您将需要启动一个单独的线程,保持该线程运行,直到它完成工作,并允许该子线程向运行在主线程上的主活动报告。这种类型的子线程通常称为工作线程。
在响应菜单项时启动一个单独的线程是很容易的。然而,巧妙的方法是允许工作线程向主线程的队列发送一条消息,告知正在发生某件事情,并且主线程应该在到达该消息时查看该消息。在非 UI 线程上调用 UI 方法也是错误的。因此,您将需要这个绑定到主线程的处理器来从工作线程调用 UI 方法。
涉及工作线程的合理解决方案如下:
- 响应菜单项时,在主线程中创建一个处理器。放在一边。
- 创建一个单独的线程(工作线程)来完成实际的工作。将步骤 1 中的处理器传递给工作线程。该处理器允许工作线程与主线程通信。
- 工作线程代码现在可以在超过五秒的时间内完成实际工作,并且在完成工作的同时,可以调用处理器来发送状态消息,以便与主线程进行通信。
- 这些状态消息现在由主线程处理,因为处理器属于主线程。主线程可以在工作线程工作的同时处理这些消息。
您可以在本章的可下载项目中看到这个交互的示例代码。从工作线程与 UI 线程通信的另一种可能更直接的方式是获取活动指针并调用方法 activity . runonuithread(Runnable action)。当然,您需要创建一个可运行的对象来进行协调。
参考
以下是一些有用的链接,可以进一步加深你对本章的理解:
developer . Android . com/guide/publishing/app-signing . html
:签约必读。apk 文件。developer . Android . com/guide/developing/projects/projects-eclipse . html
:Android 库初级 SDK 参考。- 【http://developer.android.com/guide/topics/fundamentals.html】:SDK 参考 Android 组件生命周期。
- :浅显易懂地介绍数字签名的含义。
- :我们对安卓包的理解研究。你会看到如何签名。 apk 文件,更多关于如何在软件包间共享数据的链接,更多关于共享用户 id,以及安装和卸载软件包的说明。
- :我们对 Android 库支持各个方面的研究笔记,包括旧的截屏、新的截屏、有用的 URL、样本代码等等。
Android-developers . blogspot . com/2011/10/changes-to-library-projects-in-Android . html
:Android 4.0 的时候图书馆有什么变化,变化的原因是什么?这个博客还谈到了与图书馆合作的未来方向。- :对非最终变量的作用以及它们如何影响 switch 语句的精辟讨论。
- :SDK 工具和 ADT 版本中已知问题的 Android 文档。还要注意这个 URL 的域名;这个网站致力于 Android 工具的各个方面。
docs . Oracle . com/javase/7/docs/technotes/tools/windows/keytool . html
:关于 keytool 、 jarsigner 以及签名过程本身的优秀文档。- :与本书相关的可下载项目列表。对于这一章,寻找一个名为 pro Android 5 _ Ch13 _ testandroidlibraries . zip 的文件。这个 ZIP 文件包含两个项目:一个是库,另一个使用这个库。还可以看看一个名为 pro Android 5 _ Ch13 _ test handlers . zip 的项目,它包含了处理包括工作线程在内的处理器的代码。
摘要
本章简要介绍了 Android 应用中的包、进程、组件和线程是如何交互的。本章还记录了库对在多个应用之间共享资源的支持。本章还介绍了处理器,这是 Android SDK 中的一个关键概念。在下一章中,我们将详细介绍 AsyncTask ,它将工作线程和处理器组合成一个更简单的编程抽象来使用。