安卓 4 入门指南(三)
十七、显示弹出消息
有时候,你的活动(或者其他 Android 代码)需要大声说出来。
并不是每一次与 Android 用户的交互都是整洁的,都包含在由视图组成的片段或活动中。错误会突然出现。后台任务可能比预期花费更长时间。可能会发生一些不同步的情况,比如传入的消息。在这些和其他情况下,您可能需要在传统用户界面之外与用户进行交流。
当然,这并不是什么新鲜事。对话框形式的错误消息已经存在很长时间了。更微妙的指示器也存在,从任务托盘图标到跳跃的停靠图标到振动的手机。
Android 有很多系统可以让你在基于Activity的用户界面之外提醒你的用户。一个是通知,它与意图和服务紧密相关,因此包含在第三十七章中。在这一章中,你将学习两种弹出消息的方式:祝酒和提醒。
举杯
一个Toast是一个瞬时消息,意味着它在没有用户交互的情况下自己显示和消失。此外,它不会将焦点从当前活动的Activity上移开,所以如果用户正忙于编写下一个伟大的编程指南,按键不会被消息“吃掉”。
因为Toast是短暂的,你无法知道用户是否注意到了它。你得不到用户的确认,消息也不会停留很长时间来烦扰用户。因此,Toast主要用于咨询消息,比如指示一个长期运行的后台任务已经完成,电池电量已经下降到低水平,等等。
制作一个Toast相当容易。Toast类提供了一个静态的makeText()方法,该方法接受一个String(或字符串资源 ID)并返回一个Toast实例。makeText()方法也需要Activity(或其他Context)加上一个持续时间。持续时间以LENGTH_SHORT常量或LENGTH_LONG常量的形式表示,以相对基础表示消息保持可见的时间。
如果你希望你的Toast由其他的View组成,而不是一段无聊的旧文本,只需通过构造函数创建一个新的Toast实例(它需要一个Context,然后调用setView()来提供要使用的视图,调用setDuration()来设置持续时间。
一旦您的Toast被配置,调用它的show()方法,消息将被显示。在本章的后面,你将会看到一个这样的例子。
警惕!警惕!
如果你喜欢更经典的对话框风格,你想要的是一个AlertDialog。与任何其他模式对话框一样,会弹出一个AlertDialog,获取焦点,并停留在那里直到被用户关闭。您可以将它用于严重错误、无法在基本活动 UI 中有效显示的验证消息,或者您确定用户需要立即看到消息的其他情况。
构造AlertDialog最简单的方法是使用Builder类。遵循真正的构建器风格,Builder提供了一系列配置AlertDialog的方法,每个方法返回Builder以方便链接。最后,调用构建器上的show()来显示对话框。
Builder上常用的配置方法有以下几种:
setMessage():将对话框的“主体”设置为简单的文本消息,来自提供的String或提供的字符串资源 IDsetTitle()和setIcon():配置出现在对话框标题栏的文本和/或图标setPositiveButton()和setNegativeButton():指示哪个(些)按钮应该出现在对话框的底部,它们应该被放置在哪里(分别是左、中或右),它们的标题应该是什么,以及当按钮被点击时应该调用什么逻辑(除了关闭对话框之外)。
如果需要对AlertDialog进行超出构建器允许范围的配置,不要调用show(),而是调用create()来获得部分构建的AlertDialog实例,剩下的部分进行配置,然后在AlertDialog本身上调用show()的一种风格。一旦show()被调用,对话框将出现并等待用户输入。
请注意,按下任何按钮都将关闭对话框,即使您已经为该按钮注册了侦听器。因此,如果你需要一个按钮来关闭对话框,给它一个标题和一个null监听器。使用AlertDialog时,没有选项可以让底部的按钮调用监听器,但不关闭对话框。
结账
要了解这些在实践中是如何工作的,请看一下包含以下布局的Messages/Message:
<?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/alert" android:text="Raise an alert" android:layout_width="fill_parent" android:layout_height="fill_parent" android:onClick="showAlert" />
以下是 Java 代码:
`public void onCreate(Bundle icicle) { super.onCreate(icicle);
setContentView(R.layout.main); }
public void showAlert(View view) { new AlertDialog.Builder(this) .setTitle("MessageDemo") .setMessage("Let's raise a toast!") .setNeutralButton("Here, here!", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dlg, int sumthin) { Toast .makeText(MessageDemo.this, "<clink, clink>", Toast.LENGTH_SHORT) .show(); } }) .show(); } }`
布局并不起眼——只有一个很大的Button来显示AlertDialog。然而,冰激凌三明治为AlertDialog(context, int)形式的调用增加了两个新选项。这些选项通过THEME_DEVICE_DEFAULT_LIGHT和THEME_DEVICE_DEFAULT_DARK值支持设备范围的“亮”和“暗”背景报警。这些选项有助于在整个 Android 设备上推广无缝体验的概念。
当你点击Button时,在显示对话框之前,我们使用一个生成器(new Builder(this))来设置标题(setTitle("MessageDemo"))、消息(setMessage("Let's raise a toast!"))和中性按钮(setNeutralButton("Here, here!", new OnClickListener() ...)。当按钮被点击时,OnClickListener回调触发Toast类来制作一个基于文本的 toast ( makeText(this, "<clink, clink>", LENGTH_SHORT)),然后我们将其命名为show()。结果是一个典型的对话框,如图 Figure 17–1 所示。
图 17–1。??【message demo】示例应用,点击后引发一个警告按钮
当您通过按钮关闭对话框时,它会升起烤面包片,如 Figure 17–2 所示。
图 17–2。 同样的应用,点击制作敬酒按钮后
十八、处理活动生命周期事件
众所周知,Android 设备基本上都是手机。因此,有些活动比其他活动更重要——对用户来说,接电话可能比玩数独更重要。而且,因为它是一部手机,它的内存可能比你现在的台式机或笔记本要少。
由于手机内存有限,你的活动可能会被终止,因为其他活动正在进行,系统需要你的活动的内存。可以把它想象成生命循环的机器人版——你的活动结束了,其他人可能会活下来,以此类推。在您认为活动已经完成之前,甚至在用户认为活动已经完成之前,您都不能假设活动将会运行。这是一个例子,也许是最重要的例子,说明了活动的生命周期将如何影响您自己的应用逻辑。
本章涵盖了构成活动生命周期的各种状态和回调,以及如何恰当地挂钩它们。
薛定谔的活动
一般来说,活动在任何时间点都处于四种状态之一:
- 活动:活动由用户启动,正在运行,在前台。这是你习惯于考虑的活动运作方式。
- 暂停:活动由用户启动,正在运行,并且是可见的,但是通知或其他东西覆盖了屏幕的一部分。在此期间,用户可以看到您的活动,但可能无法与之互动。示例包括提示用户接受来电,或者警告用户电池电量低或电量极低。
- 停止:该活动由用户启动,正在运行,但被其他已经启动或切换到的活动隐藏。您的应用不能直接向用户呈现任何有意义的内容,但是可以通过通知的方式进行通信。
- Dead :要么该活动从未开始(例如,在电话复位之后),要么该活动被终止,可能是由于缺少可用存储器。
生命、死亡和你的活动
Android 使用本节描述的方法调用您的活动,因为活动在上一节列出的四种状态之间转换。一些转换可能会导致多次调用您的活动,有时 Android 会在不调用应用的情况下将其杀死。这整个领域是相当模糊的,可能会发生变化,所以在决定哪些事件值得关注,哪些可以安全忽略时,请密切关注 Android 官方文档以及本节。
请注意,对于所有这些方法,您应该向上链接并调用该方法的超类版本,否则 Android 可能会引发异常。
onCreate()和 onDestroy()
在所有的例子中,我们已经在所有的Activity子类中实现了onCreate()。在三种情况下会调用此方法:
- 当活动第一次启动时(例如,从系统重启开始),将使用
null参数调用onCreate()。 - 如果活动一直在运行,然后过一段时间被终止,
onCreate()将被调用,来自onSaveInstanceState()的Bundle作为参数(如下一节所述)。 - 如果 activity 一直在运行,并且您已经将 activity 设置为基于不同的设备状态(例如,横向与纵向)拥有不同的资源,那么您的 activity 将被重新创建,并调用
onCreate()。第二十三章中介绍了如何使用资源。
这里是您初始化 UI 和设置任何需要一次性完成的事情的地方,不管活动是如何使用的。
在生命周期的另一端,当活动关闭时,可能会调用onDestroy(),这可能是因为名为finish()的活动(它“结束”了该活动),也可能是因为 Android 需要 RAM 并且过早地关闭了该活动。请注意,如果对 RAM 的需求很紧急(例如,一个来电),可能不会调用onDestroy(),但是活动仍然会被关闭。因此,onDestroy()主要是为了干净地释放你在onCreate()获得的资源(如果有的话)。
在处理包含视图的活动时要小心,该视图由来自数据库(如 SQLite)的适配器填充。谨慎的做法是在数据库和/或适配器对象上调用close(),但是也要记住,如果您的关闭是突然的,那么您不能依赖在onDestroy()中调用这些对象。我们将在第三十二章中进一步讨论这个问题。
onStart()、onRestart()和 onStop()
一个活动可以出现在前台,因为它是第一次被启动,或者因为它在被隐藏(例如,被另一个活动或被一个呼入电话)之后被带回到前台。在这两种情况下都会调用onStart()方法。
在活动已经停止并且现在重新开始的情况下,调用onRestart()方法。
相反,当活动将要停止时,调用onStop()。
onPause()和 onResume()
在您的活动进入前台之前调用onResume()方法,无论是在最初启动之后、从停止状态重新启动之后,还是在弹出对话框(例如,来电)被清除之后。这是一个很好的地方,可以根据用户上次查看您的活动后发生的事情来刷新 UI。例如,如果您正在轮询某个服务的某些信息的更改(例如,提要的新条目),onResume()是刷新当前视图以及(如果适用)启动后台线程来更新视图(例如,通过Handler)的好时机。
相反,任何从您的活动中窃取用户的事情——通常是激活另一个活动——都会导致您的onPause()方法被调用。在这里,您应该撤销您在onResume()中所做的任何事情,比如停止后台线程、释放您可能已经获得的任何独占访问资源(例如,相机)等等。
一旦onPause()被调用,Android 保留在任何时候终止你的活动进程的权利。因此,您不应该依赖于接收任何进一步的事件。
国家的恩惠
大多数情况下,前面提到的方法是用于处理应用级的事情(例如,在onCreate()中把你的 UI 的最后部分连接在一起,或者在onPause()中关闭后台线程)。
然而,Android 的很大一部分目标是拥有无缝的铜绿。活动可能根据内存需求来来去去,但理想情况下,用户不会意识到这种情况正在发生。例如,如果一个用户正在使用一个计算器,午休后又回到那个计算器,他应该会看到他在午休前正在处理的数字,除非他采取了一些措施关闭计算器(例如,按下返回按钮退出)。
为了完成所有这些工作,活动需要能够保存它们的应用实例状态,并且要快速、廉价地保存。由于活动可能在任何时候被终止,活动可能需要比您预期的更频繁地保存它们的状态。然后,当活动重新启动时,活动应该恢复到以前的状态,这样它就可以将活动恢复到以前的状态。可以把它想象成建立一个书签,这样当用户返回到该书签时,您可以将应用恢复到用户离开时的状态。
保存实例状态由onSaveInstanceState()处理。这提供了一个Bundle,活动可以将它们需要的任何数据(例如,计算器显示屏上显示的数字)注入其中。这个方法的实现需要很快,所以不要太花哨——只需将数据放入Bundle中,然后退出该方法。
该实例状态在两个地方再次提供给您:在onCreate()和onRestoreInstanceState()中。当您希望将状态数据重新应用到活动时,这是您的选择——任一回调都是合理的选择。
onSaveInstanceState()的内置实现将保存部件子集的可能可变状态。例如,它会将文本保存在EditText中,但不会保存Button是启用还是禁用的状态。只要小部件通过它们的android:id属性被唯一识别,这就可以工作。
因此,如果您实现了onSaveInstanceState(),您可以向上链接并利用继承的实现,或者不向上链接并覆盖继承的实现。类似地,有些活动可能根本不需要onSaveInstanceState()来实现,因为内置的活动会处理所有需要的事情。
十九、处理旋转
一些 Android 设备提供滑出式键盘,可以触发屏幕从纵向旋转到横向。其他设备使用加速度计来确定屏幕何时旋转。因此,有理由假设从纵向到横向的切换可能是您的应用的用户想要做的事情。
正如本章所描述的,Android 有很多方法可以让你处理屏幕旋转,这样你的应用就可以正确地处理任何一个方向。但是请记住,这些工具只能帮助您检测和管理旋转过程——您仍然必须确保您的布局和片段在每个方向上都看起来不错。
毁灭的哲学
默认情况下,当设备配置发生可能影响资源选择的变化时,Android 将在下次查看时销毁并重新创建任何正在运行或暂停的活动。各种不同的配置更改都会发生这种情况,包括:
- 旋转屏幕(即方向改变)
- 在具有滑动键盘的设备上扩展或隐藏物理键盘
- 将设备放入汽车或桌面坞站,或从坞站中移除
- 更改区域设置,从而更改首选语言
屏幕旋转是最容易出错的变化,因为方向的变化会导致应用加载一组不同的资源(例如布局)。
这里的关键是,Android 的默认行为破坏并重新创建任何正在运行或暂停的活动,这可能是最适合您的大多数活动的行为。不过,您确实可以控制这件事,并且可以定制您的活动如何响应方向变化或类似的配置切换。
都一样,只是不同
由于默认情况下,Android 会在循环中销毁并重新创建您的活动,因此您可能只需要连接到相同的onSaveInstanceState(),如果您的活动因任何其他原因被销毁(例如,内存不足或我们在第十八章中讨论的其他原因)。在您的活动中实现该方法,并在提供的Bundle中填入足够的信息,让您回到当前状态。然后,在onCreate()(或者onRestoreInstanceState(),如果你喜欢的话),从Bundle中挑选数据,用它来恢复你的活动。
为了证明这一点,我们来看一下Rotation/RotationOne项目。本章中的这个和其他示例项目使用一对main.xml布局,一个在res/layout/中用于纵向模式,一个在res/layout-land/中用于横向模式。以下是纵向布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:id="@+id/pick" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="Pick" android:enabled="true" android:onClick="pickContact" /> <Button android:id="@+id/view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="View" android:enabled="false" android:onClick="viewContact" /> </LinearLayout>
这里是类似的景观布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:id="@+id/pick" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="Pick" android:enabled="true" android:onClick="pickContact" /> <Button android:id="@+id/view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="View" android:enabled="false" android:onClick="viewContact" /> </LinearLayout>
基本上,这两种布局都包含一对按钮,每个按钮占据半个屏幕。在纵向模式下,按钮是堆叠的;在横向模式下,它们是并排的。
如果您只是创建一个项目,放入这两个布局,然后编译它,应用看起来工作得很好——旋转(模拟器中的 Ctrl+F12)将导致布局改变。虽然按钮没有状态,但是如果你正在使用其他小部件(例如EditText),你甚至会发现 Android 为你保留了一些小部件的状态(例如在EditText中输入的文本)。
Android 不能自动帮助你的是小部件之外的任何东西。
选择并查看联系人
该应用允许用户选择一个联系人,然后通过单独的按钮查看该联系人。仅当用户通过挑选按钮挑选联系人后,查看按钮才被启用。让我们仔细看看这一壮举是如何完成的。
当用户点击 Pick 按钮时,我们调用startActivityForResult()。这是startActivity()的一个变体,设计用于返回某种结果的活动——用户选择的文件、联系人或其他。相对来说,很少有活动是这样安排的,所以你不能指望打电话给startActivityForResult()并从你选择的任何活动中得到答案。
在这种情况下,我们要选择一个联系人。Android 中有一个ACTION_PICKIntent动作就是为这种场景设计的。一个ACTION_PICKIntent向 Android 表示我们要挑选…某样东西。那个“某物”是由我们放入Intent的Uri决定的。
在我们的例子中,我们可以使用一个ACTION_PICKIntent作为某些系统定义的Uri值,让用户从设备的联系人列表中选择一个联系人。特别是在 Android 2.0 及更高版本上,我们可以使用android.provider.ContactsContract.Contacts.CONTENT_URI来实现这个目的:
`public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
startActivityForResult(i, PICK_REQUEST); }`
对于 Android 1.6 和更早的版本,我们可以使用一个单独的android.provider.Contacts.CONTENT_URI。
startActivityForResult()的第二个参数是一个标识号,帮助我们将这个对startActivityForResult()的调用与我们可能进行的任何其他调用区分开来。用ACTION_PICKIntent呼叫Contacts.CONTENT_URI的startActivityForResult()将会调出一个由 Android 提供的联系人选择活动。
当用户点击一个联系人时,picker 活动结束(例如,通过finish()),并且控制返回到我们的活动。此时,我们的活动用onActivityResult()调用。Android 为我们提供了三条信息:
- 我们提供给
startActivityForResult()的标识号,因此我们可以将这个结果与其原始请求进行匹配 - 结果状态
RESULT_OK或RESULT_CANCELED,指示用户是否做出了肯定的选择或放弃了选取器(例如,通过按下后退按钮) - 对于
RESULT_OK响应,代表结果数据本身的Intent
您调用的活动需要记录Intent中的详细内容。在Contacts.CONTENT_URI的ACTION_PICKIntent的情况下,返回的Intent有它自己的Uri(通过getData())来代表选择的联系人。在RotationOne示例中,我们将它放在活动的数据成员中,并启用 View 按钮:
@Override protected void **onActivityResult**(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data**.getData**(); viewButton**.setEnabled**(true); } } }
如果用户点击现在启用的视图按钮,我们在联系人的Uri上创建一个ACTION_VIEWIntent,并在那个Intent上调用startActivity():
public void **viewContact**(View v) { **startActivity**(new **Intent**(Intent.ACTION_VIEW, contact)); }
这将弹出一个 Android 提供的活动来查看该联系人的详细信息。
保存您的状态
假设我们已经使用了startActivityForResult()来选择一个联系人,现在我们需要在屏幕方向改变时保持这个联系人。在RotationOne示例中,我们通过onSaveInstanceState()来实现:
`package com.commonsware.android.rotation.one;
import android.app.Activity; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.view.View;
import android.widget.Button;
import android.util.Log;
public class RotationOneDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null;
@Override public void onCreate(Bundle savedInstanceState) { super**.onCreate**(savedInstanceState); setContentView(R.layout.main);
viewButton=(Button)findViewById(R.id.view); restoreMe(savedInstanceState);
viewButton**.setEnabled**(contact!=null); }
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data**.getData**(); viewButton**.setEnabled**(true); } } }
public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
startActivityForResult(i, PICK_REQUEST); }
public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); }
@Override protected void onSaveInstanceState(Bundle outState) { super**.onSaveInstanceState**(outState);
if (contact!=null) { outState**.putString**("contact", contact**.toString**()); } }
private void restoreMe(Bundle state) { contact=null;
if (state!=null) { String contactUri=state**.getString**("contact");
if (contactUri!=null) { contact=Uri**.parse**(contactUri); } } } }`
总的来说,这看起来像一个正常的活动…因为它是。最初,“模型”——一个名为contact的Uri——是null。它被设置为生成ACTION_PICK子活动的结果。它的字符串表示保存在onSaveInstanceState()中,并在restoreMe()(从onCreate()调用)中恢复。如果联系人不是null,查看按钮被激活,可用于查看所选联系人。
从视觉上看,它看起来和你想象的差不多,如图图 19–1 和 19–2 所示。
图 19–1。 纵向模式下的 RotationOne 应用
**图 19–2。**rotation one 应用,在风景模式下
这种实现的好处是,它处理许多系统事件,而不仅仅是旋转,比如由于内存不足而被 Android 关闭。
出于好玩,注释掉onCreate()中的restoreMe()调用,并尝试运行应用。当你旋转仿真器或设备时,你会看到应用“忘记”在一个方向选择的一个触点。
现在节省更多!
onSaveInstanceState()的问题在于,你被限制在一个Bundle之内。这是因为这个回调也用于整个进程可能被终止的情况(例如,内存不足),所以要保存的数据必须是可以序列化的,并且不依赖于正在运行的进程。
对于某些活动,这种限制不是问题。对其他人来说,就更烦了。以网上聊天为例。您无法在Bundle中存储套接字,因此默认情况下,您必须断开与聊天服务器的连接,然后重新建立连接。这不仅可能会影响性能,还可能会影响聊天本身,例如在聊天日志中显示您正在断开连接和重新连接。
解决这个问题的一个方法是使用onRetainNonConfigurationInstance()而不是onSaveInstanceState()来表示“光”的变化,比如旋转。您的活动的onRetainNonConfigurationInstance()回调可以返回一个Object,稍后您可以通过getLastNonConfigurationInstance()检索它。Object可以是你想要的任何东西。通常,它是某种保存活动状态的“上下文”对象,如运行线程、打开套接字等。你的活动的onCreate()可以调用getLastNonConfigurationInstance(),如果你得到一个非null的响应,你现在就有了你的套接字、线程等等。最大的限制是,您不希望在保存的上下文中放置任何可能引用将被换出的资源的内容,比如从资源中加载的Drawable。
让我们看看Rotation/RotationTwo示例项目,它使用这种方法来处理旋转。布局和视觉外观与Rotation/RotationOne相同。略有不同的是 Java 代码:
`package com.commonsware.android.rotation.two;
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.Contacts; import android.view.View; import android.widget.Button; import android.util.Log;
public class RotationTwoDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null;
@Override public void onCreate(Bundle savedInstanceState) { super**.onCreate**(savedInstanceState); setContentView(R.layout.main);
viewButton=(Button)findViewById(R.id.view); restoreMe();
viewButton**.setEnabled**(contact!=null); }
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data**.getData**(); viewButton**.setEnabled**(true); } } }
public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
startActivityForResult(i, PICK_REQUEST); }
public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); }
@Override
public Object onRetainNonConfigurationInstance() {
return(contact); }
private void restoreMe() { contact=null;
if (getLastNonConfigurationInstance()!=null) { contact=(Uri)getLastNonConfigurationInstance(); } } }`
在这种情况下,我们覆盖了onRetainNonConfigurationInstance(),返回联系人的实际Uri,而不是它的字符串表示。反过来,restoreMe()呼叫getLastNonConfigurationInstance(),如果不是null,我们将它作为联系人并启用查看按钮。
这里的优点是我们传递的是Uri而不是字符串表示。在这种情况下,这不是一个很大的节省。但是我们的状态可能要复杂得多,包括线程、套接字和其他我们无法打包到Bundle中的东西。
然而,即使是处理旋转的onRetainNonConfigurationInstance()方法也可能对您的应用造成太大的干扰。例如,假设您正在创建一个实时游戏,例如第一人称射击游戏。当你的活动被破坏和重新创建时,你的用户所经历的“打嗝”可能足以让他们中枪,但他们可能不欣赏。虽然这在 T-Mobile G1 上不是什么问题,但由于旋转需要滑动打开键盘,因此不太可能在游戏中途完成,其他设备可能会仅根据加速计确定的设备位置进行旋转。对于这样的应用,还有第三种处理旋转的可能性,那就是告诉 Android 你将自己处理它们,而不需要框架的任何帮助。
DIY 旋转
要在没有 Android 帮助的情况下处理旋转,请执行以下操作:
- 在您的
AndroidManifest.xml文件中放入一个android:configChanges条目,列出您想要自己处理的配置更改和让 Android 为您处理的配置更改。 - 在您的
Activity中实现onConfigurationChanged(),当您在android:configChanges中列出的配置变化之一发生时,将会调用它。
现在,对于您想要的任何配置更改,您可以绕过整个活动销毁过程,只需获得一个回调,让您知道更改。
要了解这一点,请转到Rotation/RotationThree示例应用。同样,我们的布局是相同的,因此应用看起来与前两个示例相同。然而,Java 代码有很大的不同,因为我们不再关心保存我们的状态,而是更新我们的 UI 来处理布局。
但是首先,我们需要对我们的清单做一个小小的改动:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.rotation.three" android:versionCode="1" android:versionName="1.0.0"> <uses-sdk android:minSdkVersion="5" android:targetSdkVersion="6"/> <application android:label="@string/app_name" android:icon="@drawable/cw"> <activity android:name=".RotationThreeDemo" android:label="@string/app_name" android:configChanges="keyboardHidden|orientation"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:anyDensity="true"/> </manifest>
这里,我们声明我们将自己处理keyboardHidden和orientation配置变更。这涵盖了任何原因的旋转,无论是滑动键盘还是物理旋转。请注意,这是在活动上设置的,而不是在应用上。如果你有几项活动,你将需要为每一项活动决定你希望使用本章概述的哪一种策略。
此外,我们需要向我们的LinearLayout容器添加一个android:id,如下所示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:id="@+id/pick" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="Pick" android:enabled="true" android:onClick="pickContact" /> <Button android:id="@+id/view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" android:text="View" android:enabled="false" android:onClick="viewContact" /> </LinearLayout>
这个项目的 Java 代码如下所示:
package com.commonsware.android.rotation.three; `import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
public class RotationThreeDemo extends Activity { static final int PICK_REQUEST=1337; Button viewButton=null; Uri contact=null;
@Override public void onCreate(Bundle savedInstanceState) { super**.onCreate**(savedInstanceState);
setContentView(R.layout.main); viewButton=(Button)findViewById(R.id.view); viewButton**.setEnabled**(contact!=null); }
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data**.getData**(); viewButton**.setEnabled**(true); } } }
public void pickContact(View v) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
startActivityForResult(i, PICK_REQUEST); }
public void viewContact(View v) { startActivity(new Intent(Intent.ACTION_VIEW, contact)); }
public void onConfigurationChanged(Configuration newConfig) { super**.onConfigurationChanged**(newConfig);
LinearLayout container=(LinearLayout)findViewById(R.id.container);
if (newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE) {
container**.setOrientation**(LinearLayout.HORIZONTAL);
}
else { container**.setOrientation**(LinearLayout.VERTICAL);
}
}
}`
我们的onConfigurationChanged()需要更新 UI 来反映方向的变化。在这里,我们找到我们的LinearLayout,并告诉它改变方向,以匹配设备的方向。Configuration对象上的orientation字段将告诉我们设备是如何定向的。
…但是谷歌不推荐这个
你可能认为onConfigurationChanged()和android:configChanges是处理旋转的最终解决方案。毕竟,随着旧活动被破坏,我们不再需要担心将数据杂乱地传递给新活动。onConfigurationChanged()的做法非常性感。
但是,谷歌并不推荐。
主要担心的是忘记资源。使用onConfigurationChanged()方法,您必须确保由于这种配置变化而可能发生变化的每个资源都得到更新。这包括字符串、布局、可绘制性、菜单、动画、偏好、尺寸、颜色和所有其他内容。如果你不能确保所有的东西都被完全更新,你的应用将会有一系列的小错误。
允许 Android 破坏和重新创建您的活动保证您将获得适当的资源。您需要做的就是安排将适当的数据从旧活动传递到新活动。
onConfigurationChanged()方法仅适用于用户会直接受到破坏-创建循环影响的情况。例如,想象一个正在播放视频流的视频播放器应用。销毁并重新创建活动必然会导致应用必须重新连接到流,并在此过程中丢失缓冲数据。如果意外的移动导致设备改变方向并中断他们的视频播放,用户会感到沮丧。在这种情况下,由于用户会察觉到破坏-创建循环的问题,onConfigurationChanged()是一个合适的选择。
强行提出问题
有些活动根本不是为了改变方向。例如,游戏、相机预览、视频播放器等可能仅在横向方向有意义。虽然大多数活动应该允许用户在任何期望的方向上工作,但是对于只有一个方向有意义的活动,您可以控制它。
要阻止 Android 旋转您的活动,您需要做的就是将android:screenOrientation = "portrait"(或"landscape",随您喜欢)添加到您的AndroidManifest.xml文件中,如下所示(来自Rotation/RotationFour示例项目):
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.rotation.four" android:versionCode="1" android:versionName="1.0.0"> <uses-sdk android:minSdkVersion="5" android:targetSdkVersion="6"/> <application android:label="@string/app_name" android:icon="@drawable/cw"> <activity android:name=".RotationFourDemo" android:screenOrientation= "portrait" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:anyDensity="true"/> </manifest>
由于这是在每个活动的基础上应用的,您将需要决定您的哪些活动可能需要打开它。
此时,无论您做什么,您的活动都被锁定在您指定的方向。图 19–3 和 19–4 显示了与前三节相同的活动,但是使用了前面的清单,并且模拟器设置为纵向和横向。请注意,UI 没有移动一点,而是保持在纵向模式。
图 19–3。 在纵向模式下旋转四个应用
图 19–4。??【rotation four】应用,在风景模式下
请注意,Android 仍然会破坏和重新创建您的活动,即使您将方向设置为特定值,如下所示。如果您希望避免这种情况,您还需要在清单中设置android:configChanges,如本章前面所述。或者,您仍然可以使用onSaveInstanceState()或onRetainNonConfigurationInstance()来保存活动的可变状态。
理解这一切
正如本章开头所提到的,带有滑出式键盘的设备(如 T-Mobile G1、摩托罗拉 DROID/Milestone 等。)在键盘暴露或隐藏时改变屏幕方向,而其他设备基于加速度计改变屏幕方向。如果你有一个基于加速度计改变方向的活动,即使这个设备有一个滑出式键盘,只需将android:screenOrientation = "sensor"添加到AndroidManifest.xml文件中,如下所示(来自Rotation/RotationFive示例项目):
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.rotation.five" android:versionCode="1" android:versionName="1.0.0"> <uses-sdk android:minSdkVersion="5" android:targetSdkVersion="6"/> <application android:label="@string/app_name" android:icon="@drawable/cw"> <activity android:name=".RotationFiveDemo" android:screenOrientation="sensor" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:anyDensity="true"/> </manifest>
在这种情况下,传感器告诉 Android 您希望加速度计控制屏幕方向,因此设备方向的物理移动控制了屏幕方向。
Android 2.3 为android:screenOrientation增加了许多其他可能的值:
reverseLandscape和reversePortrait:分别表示您希望屏幕处于横向或纵向,但与正常的横向和纵向相比,屏幕上下颠倒sensorLandscape和sensorPortrait:分别表示您希望屏幕锁定在横向或纵向,但传感器可用于确定哪一侧“向上”fullSensor:允许传感器将屏幕置于四个可能的方向(纵向、反向纵向、横向、反向横向),而sensor只能在纵向和横向之间切换
更高版本的 Android 增加了更多的可能性:
behind:匹配此活动背后的方向user:采用用户手机范围内的定向行为偏好(这显然依赖于使用提供全局设置选项的设备)
您的偏好和选项通过使用片段得到进一步扩展,这将在第二十八章中的专门章节中讨论。
二十、处理线程
用户喜欢简洁的应用。用户不喜欢感觉迟钝的应用。让你的应用让用户感觉爽快的方法是使用 Android 内置的标准线程功能。本章将带你了解 Android 中线程管理的相关问题,以及一些保持 UI 简洁和响应的选项。
主应用线程
你可能会认为,当你在一个TextView上调用setText()时,屏幕会立即更新你提供的文本。事情不是这样的。相反,所有修改基于小部件的 UI 的事情都要经过一个消息队列。对setText()的调用不更新屏幕;他们只是在队列中弹出一条消息,告诉操作系统更新屏幕。操作系统将这些消息从队列中弹出,并执行消息要求的操作。
队列由一个线程处理,分别称为主应用线程和 UI 线程。只要该线程能够继续处理消息,屏幕就会更新,用户输入就会被处理,等等。
然而,主应用线程也用于对活动的几乎所有回调。你的onCreate()、onClick()、onListItemClick()以及类似的方法都是在主应用线程上调用的。当您的代码在这些方法中执行时,Android 不会处理队列中的消息,这意味着屏幕不会更新,用户输入不会被处理,等等。
这当然是不好的。事实上,糟糕的是,如果你在主应用线程上花费超过几秒钟的时间,Android 可能会显示可怕的“应用没有响应”(ANR)错误,你的活动可能会被终止。因此,您希望确保您在主应用线程上的所有工作快速进行。这意味着任何缓慢的事情都应该在后台线程中完成,以免占用主应用线程。这包括以下活动:
- 互联网访问,如向 web 服务发送数据或下载图像
- 重要的文件操作,因为闪存存储有时会非常慢
- 任何复杂的计算
幸运的是,Android 支持使用来自 Java 的标准Thread类的线程,以及所有你能想到的包装器和控制结构,比如java.util.concurrent类包。
然而,有一个很大的限制:您不能从后台线程修改 UI。您只能从主应用线程修改 UI。因此,您需要将长期运行的工作转移到后台线程中,但是这些线程需要做一些事情来安排使用主应用线程更新 UI。Android 提供了大量的工具来做到这一点,这些工具是本章的主要焦点。
使用进度条取得进展
如果您打算派生后台线程来代表用户工作,您应该考虑让用户知道工作正在进行。如果用户实际上在等待后台工作完成,这一点尤其正确。
让用户了解进度的典型方法是某种形式的进度条,就像你在许多桌面操作系统中将一堆文件从一个地方复制到另一个地方时看到的那样。Android 通过ProgressBar小部件支持这一点。
一个ProgressBar跟踪进度,定义为一个整数,0表示没有取得任何进展。您可以通过setMax()定义范围的最大值——该值表示进度已完成。默认情况下,ProgressBar以进度0开始,尽管您可以通过setProgress()从其他位置开始。如果你希望你的进度条不确定,使用setIndeterminate()并将其设置为true。
在您的 Java 代码中,您可以积极地设置已经取得的进展量(通过setProgress())或者从当前量增加进展量(通过incrementProgressBy())。你可以通过getProgress()了解进展情况。
还有其他显示进度的方法——ProgressDialog,活动标题栏中的进度指示器,等等——但是ProgressBar是一个很好的起点。
通过处理程序
制作 Android 友好的后台线程最灵活的方法是创建一个Handler子类的实例。每个活动只需要一个Handler对象,不需要手工注册。仅仅创建实例就足以将它注册到 Android 线程子系统。
您的后台线程可以与Handler通信,它将在活动的 UI 线程上完成所有工作。这一点很重要,因为 UI 更改(比如更新小部件)应该只发生在活动的 UI 线程上。
与Handler通信有两种选择:消息和Runnable对象。
消息
要将一个Message发送给一个Handler,首先调用obtainMessage()将Message对象从池中取出。有几种风格的obtainMessage(),允许您创建空的Message对象或填充了消息标识符和参数的对象。您的Handler处理需要越复杂,您就越有可能需要将数据放入Message来帮助Handler区分不同的事件。
然后,通过消息队列将Message发送到Handler,使用sendMessage...()系列方法之一,如下所示:
sendMessage():立即将消息放入队列sendMessageAtFrontOfQueue():立即将消息放入队列,并将其放在消息队列的前面(而不是后面,这是默认设置),因此您的消息优先于所有其他消息sendMessageAtTime():在指定的时间将消息放入队列,根据系统正常运行时间以毫秒表示(SystemClock.uptimeMillis())sendMessageDelayed():延迟一段时间后将消息放入队列,以毫秒表示sendEmptyMessage():向队列发送一个空的Message对象,如果您打算让它为空,就可以跳过obtainMessage()步骤
为了处理这些消息,您的Handler需要实现handleMessage(),它将被出现在消息队列中的每条消息调用。在那里,Handler可以根据需要更新 UI。然而,它仍然应该很快完成这项工作,因为其他 UI 工作会暂停,直到Handler完成。
例如,让我们创建一个ProgressBar并通过一个Handler更新它。下面是来自Threads/Handler示例项目的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <ProgressBar android:id="@+id/progress" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
除了正常设置宽度和高度之外,ProgressBar还使用了style属性。这种特殊的风格表明ProgressBar应该被绘制成传统的水平条,显示已经完成的工作量。
这是 Java:
`package com.commonsware.android.threads;
import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.widget.ProgressBar; import java.util.concurrent.atomic.AtomicBoolean;
public class HandlerDemo extends Activity { ProgressBar bar; Handler handler=new Handler() { @Override public void handleMessage(Message msg) { bar.incrementProgressBy(5); } }; AtomicBoolean isRunning=new AtomicBoolean(false);
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); bar=(ProgressBar)findViewById(R.id.progress); }
public void onStart() { super.onStart(); bar.setProgress(0);
Thread background=new Thread(new Runnable() { public void run() { try { for (int i=0;i<20 && isRunning.get();i++) { Thread.sleep(1000); handler.sendMessage(handler.obtainMessage()); } } catch (Throwable t) { // just end the background thread } } });
isRunning.set(true);
background.start(); }
public void onStop() { super.onStop(); isRunning.set(false); } }`
作为构建Activity的一部分,我们用handleMessage()的实现创建了一个Handler的实例。基本上,对于收到的任何消息,我们通过5点更新ProgressBar,然后退出消息处理程序。
然后我们利用onStart()和onStop()。在onStart()中,我们设置了一个后台线程。在真实的系统中,这个线程会做一些有意义的事情。在这里,我们只需休眠 1 秒钟,向Handler发送一个Message,并重复总共20次。由于ProgressBar的默认最大值是100,这与ProgressBar位置的 5 点增加相结合,将使该条在屏幕上清晰行进。您可以通过setMax()调整最大值。例如,您可以将最大值设置为正在处理的数据库行数,并且每行更新一次。
注意,我们接着离开 onStart()。这一点至关重要。在活动 UI 线程上调用了onStart()方法,因此它可以更新小部件等。然而,这意味着我们需要离开onStart(),既要让Handler完成工作,又要告诉 Android 我们的活动没有停滞。
产生的活动只是一个水平进度条,如图 Figure 20–1 所示。
**图 20–1。**handler demo 样本应用
请注意,虽然像这样的ProgressBar示例显示了您的代码安排更新 UI 线程的进度,但对于这个特定的小部件,这不是必需的。至少从 Android 1.5 开始,ProgressBar现在是 UI 线程安全的,因为你可以从任何线程更新它,它将处理在 UI 线程上执行实际 UI 更新的细节。
不可执行
如果您不想对Message对象大惊小怪,您也可以将Runnable对象传递给Handler,它将在活动 UI 线程上运行这些Runnable对象。Handler提供了一组post...()方法来传递Runnable对象进行最终处理。
正如Handler支持post()和postDelayed()将Runnable对象添加到事件队列中一样,您可以在任何View(即任何小部件或容器)上使用相同的方法。这稍微简化了您的代码,因为您可以跳过Handler对象。
我的 UI 线程去哪里了?
有时,您可能不知道您当前是否正在应用的 UI 线程上执行。例如,如果您将一些代码打包在一个 JAR 中供其他人重用,您可能不知道您的代码是在 UI 线程上执行还是从后台线程执行。
为了帮助解决这个问题,Activity提供了runOnUiThread()。这类似于Handler和View上的post()方法,如果你现在不在 UI 线程上,它将一个Runnable排队在 UI 线程上运行。如果您已经在 UI 线程上,它会立即调用Runnable。这给了你两全其美的好处:如果你在 UI 线程上,没有延迟;如果你不在,也很安全。
令人激动的感觉
Android 1.5 引入了后台操作的新思路:AsyncTask。在一个(相当)方便的类中,Android 处理所有在 UI 线程和后台线程上工作的杂务。此外,Android 本身会分配和删除那个后台线程。而且,它保持了一个小的工作队列,进一步强调了“一劳永逸”的感觉。
理论
有一种说法在营销界很流行,“当一个人在五金店买 1/4 英寸的钻头时,他想要的不是 1/4 英寸的钻头,而是 1/4 英寸的孔。”五金店不能卖洞,所以他们卖退而求其次的东西:让打洞变得容易的设备(钻子和钻头)。
同样,一直纠结于后台线程管理的 Android 开发者,严格来说也不是想要后台线程。相反,他们希望工作在 UI 线程之外完成,这样用户就不会陷入等待,活动也不会出现可怕的 ANR 错误。虽然 Android 不能神奇地让工作不消耗 UI 线程时间,但它可以提供一些东西,使这种后台操作更容易、更透明。AsyncTask就是这样一个例子。
要使用AsyncTask,您必须执行以下操作:
- 创建
AsyncTask的子类,通常作为使用任务的私有内部类(例如,活动) - 覆盖一个或多个
AsyncTask方法来完成后台工作,以及与需要在 UI 线程上完成的任务相关的任何工作(例如,更新进度) - 需要时,创建一个
AsyncTask子类的实例并调用execute()让它开始工作
你需要做的是
- 创建自己的后台线程
- 在适当时候终止后台线程
- 调用各种方法来安排在 UI 线程上完成的处理
AsyncTask、泛型和 Varargs
创建AsyncTask的子类不像实现Runnable接口那么简单。AsyncTask使用泛型,因此需要指定三种数据类型:
- 处理任务所需的信息类型(例如,要下载的 URL)
- 在任务中传递以指示进度的信息类型
- 任务完成时传递给任务后代码的信息类型
更令人困惑的是,前两个数据类型实际上被用作 varargs,这意味着在您的AsyncTask子类中使用了这些类型的数组。
当我们朝着一个例子前进时,这一点会变得更加清楚。
异步任务的各个阶段
你可以在AsyncTask中忽略四种方法来实现你的目标。
为了使任务类有用,您必须覆盖的是doInBackground()。这将由AsyncTask在后台线程上调用。只要有必要,它就可以运行,以完成这个特定任务需要完成的任何工作。不过,请注意,任务是有限的;不建议对无限循环使用AsyncTask。
doInBackground()方法将接收一个 varargs 数组作为参数,该数组是上一节中列出的三种数据类型中的第一种——处理任务所需的数据。因此,如果您的任务是下载一组 URL,doInBackground()将接收这些 URL 进行处理。doInBackground()方法必须返回前一节中列出的第三种数据类型的值——后台工作的结果。
您可能希望覆盖onPreExecute()。在后台线程执行doInBackground()之前,从 UI 线程调用这个方法。在这里,您可以初始化一个ProgressBar,或者指示后台工作正在开始。
此外,您可能希望覆盖onPostExecute()。在doInBackground()完成之后,从 UI 线程调用这个方法。它接收由doInBackground()返回的值作为参数(例如,成功或失败标志)。在这里,您可以关闭ProgressBar并利用后台完成的工作,比如更新列表的内容。
此外,您可能希望覆盖onProgressUpdate()。如果doInBackground()调用任务的publishProgress()方法,传递给该方法的对象被提供给onProgressUpdate(),但是在 UI 线程中。这样,onProgressUpdate()可以提醒用户后台工作的进展,比如更新ProgressBar或者继续播放动画。onProgressUpdate()方法将从前面的列表中接收第二种数据类型的 varargs 由doInBackground()通过publishProgress()发布的数据。
一个示例任务
如前所述,实现一个AsyncTask不像实现一个Runnable那么容易。然而,一旦你过了泛型和 varargs 这一关,就不会太糟糕了。
例如,下面是一个来自Threads/Asyncer示例项目的ListActivity的实现,它使用了一个AsyncTask:
`package com.commonsware.android.async;
import android.app.ListActivity; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.widget.ArrayAdapter; import android.widget.Toast; import java.util.ArrayList;`
`public class AsyncDemo extends ListActivity { private static final String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, new ArrayList()));
new AddStringTask().execute(); }
class AddStringTask extends AsyncTask<Void, String, Void> { @Override protected Void doInBackground(Void... unused) { for (String item : items) { publishProgress(item); SystemClock.sleep(200); }
return(null); }
@Override protected void onProgressUpdate(String... item) { ((ArrayAdapter)getListAdapter()).add(item[0]); }
@Override protected void onPostExecute(Void unused) { Toast .makeText(AsyncDemo.this, "Done!", Toast.LENGTH_SHORT) .show(); } } }`
这是本书中频繁使用的词汇列表的另一个变体。这一次,我们不是简单地将单词列表交给一个ArrayAdapter,而是模拟使用我们的AsyncTask实现AddStringTask在后台创建这些单词。
让我们一段一段地检查这个项目的代码。
addstring task 声明
AddStringTask声明如下:
class AddStringTask extends AsyncTask<Void, String, Void> {
这里,我们使用泛型来设置我们将在AddStringTask中利用的特定数据类型:
- 在这种情况下,我们不需要任何配置信息,所以我们的第一个类型是
Void。 - 我们希望将我们的后台任务生成的每个字符串传递给
onProgressUpdate(),以允许我们将它添加到我们的列表中,所以我们的第二个类型是String。 - 严格地说,我们没有任何结果(除了更新),所以我们的第三种类型是
Void。
doInBackground()方法
代码中的下一步是doInBackground()方法:
`@Override protected Void doInBackground(Void... unused) { for (String item : items) { publishProgress(item); SystemClock.sleep(200); }
return(null); }`
在后台线程中调用doInBackground()方法。因此,我们喜欢多久就多久。在一个生产应用中,我们可能会做一些类似于遍历一个 URL 列表并下载每个 URL 的事情。在这里,我们迭代我们的静态列表 lorem ipsum 单词,为每个单词调用publishProgress(),然后休眠 200 毫秒来模拟正在完成的实际工作。
既然我们选择了没有配置信息,我们应该不需要参数来doInBackground()。然而,与AsyncTask的约定说我们必须接受第一个数据类型的 varargs,这就是为什么我们的方法参数是Void... unused。
既然我们选择了没有结果,我们应该不需要返回任何东西。尽管如此,与AsyncTask的契约说我们必须返回第三种数据类型的对象。由于数据类型是Void,我们返回的对象是null。
onProgressUpdate()方法
接下来是onProgressUpdate()方法:
@Override protected void **onProgressUpdate**(String... item) { ((ArrayAdapter)getListAdapter()).**add**(item[0]); }
在 UI 线程上调用了onProgressUpdate()方法,我们想做一些事情让用户知道我们在加载这些字符串方面取得了进展。在这种情况下,我们简单地将字符串添加到ArrayAdapter中,因此它被追加到列表的末尾。
onProgressUpdate()方法接收一个String... varargs,因为这是我们的类声明中的第二个数据类型。因为我们每次调用publishProgress()只传递一个字符串,所以我们只需要检查 varargs 数组中的第一个条目。
onPostExecute()方法
下一个方法是onPostExecute():
@Override protected void **onPostExecute**(Void unused) { Toast .**makeText**(AsyncDemo.this, "Done!", Toast.LENGTH_SHORT) .**show**(); }
在 UI 线程上调用了onPostExecute()方法,我们想做一些事情来表示后台工作已经完成。在一个真实的系统中,可能会有一些ProgressBar被解除或者一些动画被停止。在这里,我们简单地举一个Toast。
既然我们选择了没有结果,我们应该不需要任何参数。与AsyncTask的合同规定我们必须接受第三种数据类型的单个值。因为数据类型是Void,我们方法参数是Void unused。
活动
该活动如下:
new AddStringTask().**execute**();
要使用AddStringTask,我们只需创建一个实例并在其上调用execute()。这启动了一系列事件,最终导致后台线程完成其工作。
如果AddStringTask需要配置参数,我们将不会使用Void作为我们的第一个数据类型,构造函数将接受零个或多个已定义类型的参数。这些价值最终会传递给doInBackground()。
结果
如果您构建、安装并运行这个项目,您将会看到列表在几秒钟内被实时填充,然后是一个表示完成的Toast,如图 20–2 中的所示。
**图 20–2。**async demo,中途加载单词列表
螺纹和旋转
活动在方向改变时经历的默认破坏-创建循环的一个问题来自后台线程。如果活动已经开始了一些后台工作——例如,通过一个AsyncTask——然后活动被销毁并重新创建,那么AsyncTask需要以某种方式知道这一点。否则,AsyncTask很可能会将更新和最终结果发送给旧的活动,而新的活动对此一无所知。事实上,新活动可能会再次开始后台工作*,浪费资源。*
*处理这个问题的一种方法是通过接管配置更改来禁用销毁-创建循环,如前一节所述。另一个选择是进行更聪明的活动。您可以在Rotation/RotationAsync示例项目中看到这样的例子。如下所示,这个项目使用了一个ProgressBar,很像本章前面的Handler演示。它还有一个TextView来指示后台工作何时完成,最初是不可见的。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent" > <ProgressBar android:id="@+id/progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/completed" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Work completed!" android:visibility="invisible" /> </LinearLayout>
“业务逻辑”是让一个AsyncTask在后台做一些(假的)工作,一路更新ProgressBar,并在完成时使TextView可见。更重要的是,如果屏幕被旋转,它需要以这样的方式正确地运行。这意味着:
- 我们不能“失去”我们的
AsyncTask,让它继续工作并更新错误的活动。 - 我们不能开始第二个
AsyncTask,从而加倍我们的工作量。 - 我们需要让 UI 正确地反映我们工作的进度或完成情况。
手工活动协会
前面,本章展示了作为Activity类的常规内部类实现的AsyncTask的使用。当你不关心旋转时,这很有效。例如,如果AsyncTask不影响 UI——比如上传照片——旋转对你来说就不是问题。将AsyncTask作为Activity的内部类意味着您可以随时访问任何需要Context的地方的活动。
然而,对于旋转场景,常规的内部类将很难工作。AsyncTask会认为它知道应该与哪个Activity一起工作,但实际上它会保持对旧活动的隐式引用,而不是在方向改变后。
所以,在RotationAsync中,RotationAwareTask类是一个静态内部类。这意味着RotationAwareTask没有任何对任何RotationAsyncActivity(旧的或新的)的隐式引用:
import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.util.Log; import android.view.View; import android.widget.ProgressBar;
`public class RotationAsync extends Activity { private ProgressBar bar=null; private RotationAwareTask task=null;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
bar=(ProgressBar)findViewById(R.id.progress);
task=(RotationAwareTask)getLastNonConfigurationInstance();
if (task==null) { task=new RotationAwareTask(this); task.execute(); } else { task.attach(this); updateProgress(task.getProgress());
if (task.getProgress()>=100) { markAsDone(); } } }
@Override public Object onRetainNonConfigurationInstance() { task.detach();
return(task); }
void updateProgress(int progress) { bar.setProgress(progress); }
void markAsDone() { findViewById(R.id.completed).setVisibility(View.VISIBLE); }
static class RotationAwareTask extends AsyncTask<Void, Void, Void> { RotationAsync activity=null; int progress=0;
RotationAwareTask(RotationAsync activity) { attach(activity); }
@Override protected Void doInBackground(Void... unused) { for (int i=0;i<20;i++) { SystemClock.sleep(500); publishProgress(); }`
` return(null); }
@Override protected void onProgressUpdate(Void... unused) { if (activity==null) { Log.w("RotationAsync", "onProgressUpdate() skipped – no activity"); } else { progress+=5; activity.updateProgress(progress); } }
@Override protected void onPostExecute(Void unused) { if (activity==null) { Log.w("RotationAsync", "onPostExecute() skipped – no activity"); } else { activity.markAsDone(); } }
void detach() { activity=null; }
void attach(RotationAsync activity) { this.activity=activity; }
int getProgress() { return(progress); } } }`
因为我们希望RotationAwareTask更新当前的RotationAsyncActivity,所以我们在创建任务时通过构造函数提供了那个Activity。RotationAwareTask也有attach()和detach()方法来改变任务所知道的Activity,我们很快就会看到。
事件的流程
当RotationAsync第一次启动时,它创建一个RotationAwareTask类的新实例并执行它。在这一点上,任务有了对RotationAsyncActivity的引用,并且可以做它的(假的)工作,告诉RotationAsync在这个过程中更新进度。
现在,假设在doInBackground()处理过程中,用户旋转屏幕。我们的Activity将用onRetainNonConfigurationInstance()来称呼。在这里,我们想做两件事:
- 由于这个
Activity实例正在被销毁,我们需要确保任务不再持有对它的引用。因此,我们调用detach(),使任务将其RotationAsync数据成员(activity)设置为null。 - 我们返回
RotationAwareTask对象,这样我们的新RotationAsync实例就可以访问它。
最终,新的RotationAsync实例将被创建。在onCreate()中,我们试图通过getLastNonConfigurationInstance()访问任何当前的RotationAwareTask实例。如果那是null,那么我们知道这是一个新创建的活动,因此我们创建了一个新任务。然而,如果getLastNonConfigurationInstance()从旧的RotationAsync实例中返回了任务对象,我们就保留它并更新我们的 UI 以反映当前已经取得的进展。我们还将新的RotationAsync添加到RotationAwareTask中,这样随着进一步的进展,任务可以通知适当的活动。
最终结果是我们的ProgressBar平稳地从0进展到100,即使轮换正在进行。
为什么会这样
Android 中的大多数回调方法都是由主应用线程正在处理的消息队列中的消息驱动的。通常,只要主应用线程不忙,比如运行我们的代码,就会处理这个队列。然而,当配置发生变化时,比如屏幕旋转,这就不再成立了。在调用旧活动的onRetainNonConfigurationInstance()实例和完成新活动的onCreate()之间,消息队列保持不变。
所以,让我们假设,在onRetainNonConfigurationInstance()活动和随后的onCreate()之间,我们的AsyncTask的后台工作完成了。这将触发onPostExecute()被调用...最终。然而,由于onPostExecute()实际上是从消息队列中的一条消息启动的,所以在我们的onCreate()完成之前onPostExecute()不会被调用。因此,我们的AsyncTask可以在配置更改期间保持运行,只要我们做两件事:
- 在新活动实例的
onCreate()中,我们更新了AsyncTask,让它与我们的新活动一起工作,而不是与旧活动一起工作。 - 我们不尝试使用来自
doInBackground()的活动。
现在,注意事项
背景线程,虽然使用 Android Handler系统非常可能,但并不都是快乐和温暖的小狗。后台线程不仅增加了复杂性,而且在可用内存、CPU 和电池寿命方面也有实际成本。因此,你需要用你的后台线程考虑各种各样的场景,包括如下:
- 当后台线程运行时,用户可能会与您的活动的 UI 进行交互。如果后台线程正在做的工作被用户输入改变或无效,您需要将这一情况通知后台线程。Android 在
java.util.concurrent包中包含了许多类,可以帮助你安全地与后台线程通信。 - 当后台工作正在进行时,活动被取消的可能性。例如,在开始你的活动后,用户可能有一个电话进来,接着是一条短信,然后需要查找一个联系人——所有这些可能足以将你的活动踢出记忆。第十八章涵盖了 Android 将带你的活动经历的各种事件;挂钩到正确的线程,并确保在有机会时干净地关闭后台线程。
- 如果你浪费了大量的 CPU 时间和电池寿命却没有任何回报,用户可能会被激怒。从战术上来说,这意味着使用
ProgressBar或其他方式让用户知道有事情正在发生。从战略上来说,这意味着您仍然需要高效地工作——后台线程不是处理缓慢或无意义代码的灵丹妙药。 - 在后台处理过程中遇到错误的可能性。例如,如果您从互联网上收集信息,设备可能会失去连接。通过一个通知来提醒用户这个问题(在第三十七章中有介绍)并关闭后台线程可能是你最好的选择。*
二十一、创建意图过滤器
到目前为止,这本书的重点一直是用户从设备的启动器直接打开的活动。这是让您的活动开始运行并让用户可见的最明显的例子。而且,在许多情况下,这是用户开始使用您的应用的主要方式。
但是,请记住,Android 系统是基于许多松散耦合的组件的。你可能在桌面 GUI 中通过对话框、子窗口等完成的事情通常被认为是独立的活动。虽然一个活动是“特殊的”,因为它显示在启动器中,但是其他活动都需要被访问...不知何故。
“不知何故”是通过意图。
意图基本上是你传递给 Android 的一个信息,说“哟!我想做什么...呃...有事!耶!”“某事”的具体程度取决于具体情况——有时你确切地知道你想做什么(例如,打开你的一个其他活动),有时你不知道。
抽象地说,Android 是关于意图和那些意图的接收者的。所以,现在你已经精通了创建活动,让我们深入了解意图,这样我们就可以创建更复杂的应用,同时成为“好的 Android 公民”
你的意图是什么?
当蒂姆·伯纳斯·李爵士发明超文本传输协议(HTTP)时,他建立了一个动词加地址的 URL 形式的系统。该地址表示一种资源,如网页、图形或服务器端程序。动词指示应该做什么:GET 检索它,POST 将表单数据发送给它进行处理,等等。
意图是相似的,因为它们代表一个动作加上上下文。与 HTTP 动词和资源相比,Android 意图的上下文有更多的动作和组件,但概念仍然是相同的。正如 web 浏览器知道如何处理动词+URL 对一样,Android 知道如何找到处理给定意图的活动或其他应用逻辑。
件意图
意图的两个最重要的部分是动作和 Android 称为的数据。这些几乎完全类似于 HTTP 动词和 URL:动作是动词,数据是一个Uri,比如content://contacts/people/1,表示联系人数据库中的一个联系人。动作是常量,比如ACTION_VIEW(打开资源的查看器)、ACTION_EDIT(编辑资源),或者ACTION_PICK(选择一个可用的项目,给定一个代表集合的Uri,比如content://contacts/people。
如果你要创建一个意图,将ACTION_VIEW和content://contacts/people/1的内容Uri结合起来,并将这个意图传递给 Android,Android 会知道找到并打开一个能够查看该资源的活动。
除了动作和数据Uri之外,您还可以在意图(表示为Intent对象)中放置其他标准,例如:
- 类别:你的“主要”活动将在
LAUNCHER类别中,表明它应该出现在启动菜单上。其他活动可能属于DEFAULT类别或ALTERNATIVE类别。 - MIME 类型:如果你不知道一个集合
Uri,这表示你想要操作的资源的类型。 - 组件:这是应该接收这个意图的活动的类。以这种方式使用组件消除了对意图的其他属性的需要。然而,它确实使意图更加脆弱,因为它假设了特定的实现。
- 额外信息:这指的是你想要传递给接收者的其他信息的
Bundle,通常是接收者可能想要利用的信息。给定的接收者可以使用哪些信息取决于接收者,并且(希望)被很好地记录下来。
您将在 Android SDK 文档中找到Intent类的标准动作和类别的列表。
意图路由
如前一节所述,如果您在意图中指定目标组件,Android 毫无疑问会将意图路由到哪里,并且它会启动指定的活动。如果目标意图在您的应用中,这可能是好的。绝对不建议将意图发送到其他应用。总的来说,组件名称被认为是应用的私有名称,可能会更改。模板和 MIME 类型是识别您希望第三方代码提供的服务的首选方式。
如果您没有指定目标组件,那么 Android 必须找出哪些活动(或其他接收者)有资格接收该意图。请注意复数 activities 的使用,因为广义的书面意图可能会分解为几个活动。那是...嗯...意图(原谅这个双关语),你会在本章后面看到。这种路由方法被称为隐式路由。
基本上,有三个规则,对于给定的活动,所有这些规则都必须符合给定的意图:
- 活动必须支持指定的操作。
- 活动必须支持规定的 MIME 类型(如果提供)。
- 活动必须支持意向中指定的所有类别。
结果是,你要让你的意图足够具体,以找到正确的接收者,没有比这更具体。当我们在本章后面学习一些例子时,这一点会变得更加清楚。
陈述你的意图
所有希望通过意图得到通知的 Android 组件都必须声明意图过滤器,因此 Android 知道哪些意图应该发送给该组件。为此,您需要将intent-filter元素添加到您的AndroidManifest.xml文件中。
所有的示例项目都定义了意图过滤器,这要归功于 Android 应用构建脚本(android create project或 IDE 等价物)。它们看起来像这样:
<?xml version="1.0"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.skeleton"> <application> <activity android:name=".Now" android:label="Now"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
注意activity元素下的intent-filter元素。在此,我们声明这一活动:
- 是该应用的主要活动
- 在
LAUNCHER类别中,这意味着它在 Android 主菜单中有一个图标
因为这个活动是应用的主要活动,Android 知道当有人从主菜单中选择应用时,它应该启动这个组件。
欢迎您在意向过滤器中加入多个行动或多个类别。这表明相关联的组件(例如,活动)处理多个不同种类的意图。
很有可能,您还希望让您的次要(非MAIN)活动指定它们所处理的数据的 MIME 类型。然后,如果一个意图是针对那个 MIME 类型的——不管是直接的,还是通过引用那个类型的东西的Uri间接的——Android 将知道组件处理这样的数据。
例如,您可以像这样声明一个活动:
<activity android:name=".TourViewActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.commonsware.tour" /> </intent-filter> </activity>
该活动将由请求查看代表一条vnd.android.cursor.item/vnd.commonsware.tour内容的Uri的意图发起。该Intent可能来自同一应用中的另一个活动(例如,该应用的MAIN活动),或者来自另一个 Android 应用中的另一个活动,该活动碰巧知道该活动处理的Uri。
窄接收器
在前面的例子中,意图过滤器是在活动上设置的。有时,将意图与活动捆绑在一起并不完全是您想要的,如以下情况:
- 一些系统事件可能会导致您想要触发服务中的某个事件,而不是某个活动。
- 一些事件可能需要在不同的情况下启动不同的活动,其中标准不仅仅基于意图本身,而是基于一些其他状态(例如,如果我们获得意图 X,并且数据库具有 Y,则启动活动 M;如果数据库没有 Y,则启动活动 N)。
对于这些情况,Android 提供了接收器,定义为实现BroadcastReceiver接口的类。广播接收器是设计用来接收意图(具体地说,广播意图)并采取行动的一次性对象。
BroadcastReceiver接口只有一个方法:onReceive()。接收者实现那个方法,在那里他们做任何他们想要做的事情。要声明一个接收者,在您的AndroidManifest.xml文件中添加一个receiver元素:
<receiver android:name=".MyIntentReceiverClassName" />
接收器只在处理onReceive()的时间内有效——一旦该方法返回,接收器实例就会被垃圾收集,不会被重用。这意味着接收者在他们能做的事情上有些限制,主要是为了避免任何涉及回调的事情。例如,它们不能绑定到服务,也不能打开对话框。
例外情况是,如果BroadcastReceiver是在一些长期存在的组件上实现的,比如一个活动或服务。在这种情况下,接收方的寿命与其“宿主”的寿命一样长(例如,直到活动被冻结)。但是,在这种情况下,您不能通过AndroidManifest.xml声明接收者。相反,你需要在你的Activity的onResume()回调中调用registerReceiver()来声明对一个意向感兴趣,然后当你不再需要那些意向时,从你的Activity的onPause()中调用unregisterReceiver()。
各种场合的意图
随着 Android 每个新版本的推出,Intent类包含的动作数量稳步增长。随着冰淇淋三明治(ICS)4.0 版本的发布,Google 又增加了六个动作,并取消了三个不再需要的动作。Android SDK 文档涵盖了 ICS 中所有可用的 97 个意图动作,您可以在闲暇时阅读。以下是让你思考可能性的一些亮点:
ACTION_AIRPLANE_MODE_CHANGED:设备已进入或退出飞行模式。ACTION_CAMERA_BUTTON:相机按钮被按下。- 日期已经改变。这对于提醒列表、日历等应用来说很重要。
ACTION_HEADSET_PLUG:耳机被连接或移除。这对于音乐播放类应用和类似的应用来说是相当重要的。
你可以开始看到可能性和复杂性。
暂停警告
使用Intent对象传递任意消息有一个问题:它只有在接收者活动时才起作用。引用BroadcastReceiver的文档:
如果在您的
Activity.onResume()实现中注册了一个接收者,您应该在Activity.onPause()中注销它。(暂停时你不会收到意图,这将减少不必要的系统开销)。不要在Activity.onSaveInstanceState()中取消注册,因为如果用户在历史堆栈中返回,将不会调用这个函数。
因此,您只能在以下情况下使用Intent框架作为任意的消息总线:
- 你的接收器并不在乎是否因为没有激活而错过了信息。
- 您提供了一些方法来让接收者“抓住”它在不活动时错过的消息。
- 你的收货人在货单上登记了。
二十二、启动活动和子活动
Android UI 架构背后的理论是,开发人员应该将他们的应用分解成不同的活动。例如,日历应用可以具有用于查看日历、查看单个事件、编辑事件(包括添加新事件)、在同一屏幕上查看和编辑事件以进行更大显示等活动。这意味着您的一个活动有办法启动另一个活动。例如,如果用户从视图-日历活动中选择了一个事件,您可能希望显示该事件的视图-事件活动。这意味着您需要能够启动 view-event 活动并显示特定的事件(用户选择的事件)。
这可以进一步分为两种情况:
- 您知道要启动哪个活动,可能是因为它是您自己的应用中的另一个活动。
- 您有一个内容
Uri来做一些事情,并且您希望您的用户能够用它来做一些事情,但是您事先不知道选项是什么。
本章涵盖了第一种情况;第二个超出了本书的范围。
同级和下级
当你决定发起一项活动时,你需要回答的一个关键问题是:你的活动需要知道发起的活动何时结束吗?
例如,假设您想要生成一个活动来收集您正在连接的某个 web 服务的身份验证信息——为了使用 OAuth 服务,您可能需要使用 OpenID 进行身份验证。在这种情况下,您的主活动将需要知道身份验证何时完成,以便它可以开始使用 web 服务。
另一方面,想象一下 Android 中的电子邮件应用。当用户选择查看附件时,您和用户都不一定希望主活动知道用户何时完成了对附件的查看。
在第一个场景中,已启动的活动显然从属于启动活动。在这种情况下,您可能希望将子活动作为子活动启动,这意味着当子活动完成时,您的活动将得到通知。
在第二个场景中,启动的活动更像是您的活动的对等体,因此您可能希望像启动常规活动一样启动子活动。孩子做完了你的活动不会被通知,但是,话说回来,你的活动真的不需要知道。
启动它们
开始一项活动的两个要素是一个意图和你如何开始的选择。
制定一个意图
正如前一章所讨论的,intents 封装了对 Android 的请求,请求一些活动或其他接收者做一些事情。如果您想要启动的活动是您自己的,您可能会发现创建一个明确的意图是最简单的,命名您想要启动的组件。例如,在您的活动中,您可以创建如下意图:
new **Intent**(this, HelpActivity.class);
这规定了你要发射HelpActivity。这个活动需要在您的AndroidManifest.xml文件中命名,尽管不需要任何意图过滤器,因为您试图直接请求它。
或者,您可以为某个Uri组织一个意图,请求一个特定的操作:
Uri uri=Uri.**parse**("geo:"+lat.**toString**()+","+lon.**toString**()); Intent i=new **Intent**(Intent.ACTION_VIEW, uri);
这里,假设您有类型为Double的某个位置的纬度和经度(分别为lat和lon,您构建了一个geo方案Uri,并创建了一个请求查看这个Uri ( ACTION_VIEW)的意图。
打电话
一旦你有了你的意图,你需要把它传递给 Android 并启动子活动。您有两个主要选项(以及一些更高级/更专业的变体):
- 最简单的选择是用
Intent调用startActivity()。这将导致 Android 找到最匹配的活动,并将意图传递给它进行处理。当子活动完成时,您的活动不会得到通知。 - 您可以调用
startActivityForResult(),向其传递Intent和一个数字(对于调用活动是唯一的)。Android 将找到最匹配的活动,并将意图传递给它进行处理。当子活动完成时,您的活动将通过onActivityResult()回调得到通知。 - 在某些情况下,您可能希望或需要条件启动、批量启动等。的活动。像
startActivities()、startActivityFromFragment()和startActivityIfNeeded()这样的附加方法可以帮助处理这些情况。
如上所述,使用startActivityForResult(),您可以实现onActivityResult()回调,以便在子活动完成其工作时得到通知。回调接收提供给startActivityForResult()的唯一编号,因此您可以确定哪个子活动已经完成。您还会得到以下内容:
- 一个结果代码,来自调用
setResult()的子活动。通常,这是RESULT_OK或RESULT_CANCELED,尽管您可以创建自己的返回代码(选择一个以RESULT_FIRST_USER开头的数字)。 - 可选的
String包含一些结果数据,可能是一些内部或外部资源的 URL。例如,ACTION_PICK意图通常通过这个数据字符串返回内容的选定位。 - 可选的
Bundle包含结果代码和数据字符串之外的附加信息。
为了演示如何启动一个 peer 活动,请看一下Activities/Launch示例应用。XML 布局相当简单:两个纬度和经度字段,外加一个按钮。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TableLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:stretchColumns="1,2" > <TableRow> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="2dip" android:paddingRight="4dip" android:text="Location:" /> <EditText android:id="@+id/lat" android:layout_width="fill_parent" android:layout_height="wrap_content" android:cursorVisible="true" android:editable="true" android:singleLine="true" android:layout_weight="1" /> <EditText android:id="@+id/lon" android:layout_width="fill_parent" android:layout_height="wrap_content" android:cursorVisible="true" android:editable="true" android:singleLine="true" android:layout_weight="1" /> </TableRow> </TableLayout> <Button android:id="@+id/map" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Show Me!" android:onClick="showMe" /> </LinearLayout>
按钮的showMe()回调方法简单地获取纬度和经度,将它们倒入一个geo方案Uri,然后开始活动:
`packagecom.commonsware.android.activities;
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.EditText;
public class LaunchDemo extends Activity { private EditText lat; private EditText lon;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);
lat=(EditText)findViewById(R.id.lat); lon=(EditText)findViewById(R.id.lon); }
public void showMe(View v) { String _lat=lat.getText().toString(); String _lon=lon.getText().toString(); Uri uri=Uri.parse("geo:"+_lat+","+_lon);
startActivity(new Intent(Intent.ACTION_VIEW, uri)); } }`
我们保持了非常基本的活动,以便将重点放在处理地理意图的主题上。我们开始如图 22–1 所示。
**图 22–1。**launch demo 示例应用,位置填写为
如果你填写一个位置(如纬度 38.8891,经度-77.0492)并点击按钮,得到的地图会更有趣,如图图 22–2 所示。请注意,这是内置的 Android 地图活动——我们没有创建自己的活动来显示该地图。
**图 22–2。**launch demo 推出的地图,显示了 DC 的林肯纪念堂
在第四十章中,你将看到如何在自己的活动中创建地图,以防你需要更好地控制地图的显示方式。
**注意:**这个geo:Intent只能在安装了谷歌地图的设备或模拟器上运行,或者在安装了其他支持geo: URL 的地图应用的设备上运行。
类似于标签式浏览
现代桌面网络浏览器的主要特征之一是选项卡式浏览,其中单个浏览器窗口可以显示跨越一系列选项卡的几个页面。在移动设备上,这可能没有太大意义,因为你失去了选项卡本身的屏幕空间。然而,在本书中,我们不会让感性这样的小事阻止我们,所以这一节使用TabActivity和Intent对象演示了一个选项卡式浏览器。
您可能还记得第十四章的“将它放在我的选项卡上”一节,一个选项卡可以有一个View或一个Activity作为它的内容。如果您想使用一个Activity作为选项卡的内容,您提供一个Intent,它将启动所需的Activity;Android 的标签管理框架会将Activity的用户界面注入到标签中。
你的本能可能是使用http: Uri,就像我们在前面的例子中使用geo: Uri一样:
Intent i=new **Intent**(Intent.ACTION_VIEW); i.**setData**(Uri.**parse**("http://commonsware.com"));
这样,您可以使用内置的浏览器应用,并获得它提供的所有功能。唉,这不管用。您不能在选项卡中主持其他应用的活动;出于安全原因,只允许您自己的活动。所以,我们从第十五章中掸掉我们的WebView演示,并使用它们,重新包装成Activities/IntentTab。
下面是主活动的源代码,它托管了TabView:
`package com.commonsware.android.intenttab;
import android.app.Activity; import android.app.TabActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.webkit.WebView; import android.widget.TabHost;
public class IntentTabDemo extends TabActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
TabHost host=getTabHost(); Intent i=new Intent(this, CWBrowser.class);
i.putExtra(CWBrowser.URL, "commonsware.com"); host.addTab(host.newTabSpec("one") .setIndicator("CW") .setContent(i));
i=new Intent(i); i.putExtra(CWBrowser.URL, "www.android.com"); host.addTab(host.newTabSpec("two") .setIndicator("Android") .setContent(i)); } }`
如您所见,我们使用TabActivity作为基类,因此我们不需要自己的布局 XML— TabActivity为我们提供了它。我们所做的就是访问TabHost并添加两个选项卡,每个选项卡指定一个直接引用另一个类的Intent。在这种情况下,我们的两个选项卡将各自拥有一个CWBrowser,通过一个额外的Intent提供一个 URL 来加载。
CWBrowser活动是对早期浏览器演示的简单修改:
`package com.commonsware.android.intenttab;
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.webkit.WebView;
public class CWBrowser extends Activity { public static final String URL="com.commonsware.android.intenttab.URL"; private WebView browser;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle);
browser=new WebView(this); setContentView(browser); browser.loadUrl(getIntent().getStringExtra(URL)); } }`
他们只需在浏览器中加载不同的 URL:一个是 CommonsWare 主页,另一个是 Android 主页。
由此产生的用户界面显示了标签式浏览在 Android 上的样子,如图图 22–3 和图 22–4 所示。
图 22–3。??【IntentTabDemo 示例应用,显示第一个选项卡
**图 22–4。**IntentTabDemo 示例应用,显示第二个选项卡
然而,这种方法相当浪费。创建一个活动有相当大的开销,您不需要仅仅填充TabHost中的选项卡。特别是,它增加了你的应用所需的堆栈空间,堆栈空间的耗尽是 Android 中的一个重大问题,这将在后面的章节中描述。
二十三、使用资源
资源是保存在 Java 源代码之外的静态信息。你已经在本书的例子中经常看到一种类型的资源——布局。还有许多其他类型的资源,比如图像和字符串,您可以在 Android 应用中加以利用。
资源阵容
资源作为文件存储在 Android 项目布局中的res/目录下。除了原始资源(res/raw/),所有其他类型的资源都由 Android 打包系统或设备或仿真器上的 Android 系统为您解析。因此,例如,当您通过布局资源(res/layout/)布局一个活动的 UI 时,您不必自己解析布局 XML,因为 Android 会为您处理。
除了布局资源(在第八章的中介绍),还有其他几种类型的资源可供您使用,包括:
- 图像(
res/drawable-mdpi/、res/drawable-ldpi等)。),用于在用户界面中放置静态图标、图像、照片或其他图片 - Raw (
res/raw/),用于对您的应用有意义但对 Android 框架不一定有意义的任意文件 - 字符串、颜色、数组和维度(
res/values/),用于给这些类型的常量赋予符号名称,并使它们与代码的其余部分分开(例如,用于国际化和本地化) - XML (
res/xml/),用于包含您自己的数据和结构的静态 XML 文件
弦论
将标签和其他文本放在应用的主要源代码之外通常被认为是一个非常好的主意。特别是,它有助于国际化和本地化,这将在本章后面的“不同人使用不同的笔画”一节中讨论。即使你不打算把你的字符串翻译成其他语言,如果所有的字符串都在一个地方而不是分散在你的源代码中,修改起来会更容易。
Android 支持常规的外部化字符串,以及字符串格式,其中字符串有动态插入信息的占位符。最重要的是,Android 支持简单的文本格式,称为风格的文本,因此你可以将你的文字加粗或斜体与普通文本混合在一起。
普通字符串
一般来说,对于普通字符串,您所需要的只是一个位于res/values目录中的 XML 文件(通常命名为res/values/strings.xml),带有一个resources根元素,以及一个针对您希望编码为资源的每个字符串的子string元素。string元素采用了一个name属性(该属性是该字符串的唯一名称)和一个包含该字符串文本的文本元素,如下例所示:
<resources> <string name="quick">The quick brown fox...</string> <string name="laughs">He who laughs last...</string> </resources>
唯一棘手的部分是字符串值是否包含引号(")或撇号(')。在这种情况下,您可能希望通过在这些值前面加一个反斜杠来对它们进行转义(例如,These are the times that try men\'s souls.)。或者,如果它只是一个撇号,您可以用引号将值括起来(例如,"These are the times that try men's souls.")。
然后,您可以从布局文件中引用该字符串(如@string/...,其中省略号是唯一的名称,如@string/laughs)。或者您可以通过使用字符串资源的资源 ID 调用getString()从您的 Java 代码中获取字符串,该资源 ID 是以R.string.为前缀的唯一名称(例如getString(R.string.quick))。
字符串格式
与 Java 语言的其他实现一样,Android 的 Dalvik 虚拟机支持字符串格式。这里,字符串包含占位符,表示在运行时将被变量信息替换的数据(例如,My name is %1$s)。存储为资源的普通字符串可以用作字符串格式:
String strFormat=**getString**(R.string.my_name); String strResult=String.**format**(strFormat, "Tim"); ((TextView)**findViewById**(R.id.some_label)).**setText**(strResult);
还有一种getString()的味道,那就是String.format()在召唤你:
String strResult=**getString**(R.string.my_name, "Tim"); ((TextView)**findViewById**(R.id.some_label)).**setText**(strResult);
使用带索引的占位符版本— %1$s而不仅仅是%s是非常重要的。从策略上讲,字符串资源的翻译可能会导致您以不同于原始翻译的顺序应用变量数据,并且使用无索引占位符会将您锁定在特定的顺序。从战术上讲,你的项目将无法编译,因为现在 Android 构建工具拒绝无索引占位符。
样式化文本
如果您想要真正丰富的文本,您应该拥有包含 HTML 的原始资源,然后将这些资源注入一个 WebKit 小部件。然而,对于轻量级的 HTML 格式,使用诸如<b>、<i>和<u>之类的行内元素,您可以在字符串资源中使用它们:
<resources> <string name="b">This has <b>bold</b> in it.</string> <string name="i">Whereas this has <i>italics</i>!</string> </resources>
您可以通过getText()来访问它们,这给了您一个支持android.text.Spanned接口的对象,因此应用了所有的格式:
((TextView)**findViewById**(R.id.another_label)) **.setText(getText**(R.string.b));
样式化的文本和格式
样式化的文本变得棘手的地方是样式化的字符串格式,因为String.format()作用于String对象,而不是带有格式指令的Spanned对象。如果您真的想拥有样式化的字符串格式,以下是解决方法:
- 实体-转义字符串资源中的尖括号(如
this is <b>%1$s</b>)。 - 正常检索字符串资源,尽管此时不会对其进行样式化(例如,
getString(R.string.funky_format))。 - 生成格式结果,确保转义您替换的任何字符串值,以防它们包含尖括号或&符号:
String.**format(getString**(R.string.funky_format), TextUtils.**htmlEncode**(strName)); - 通过
Html.fromHtml():someTextView.setText(Html **.fromHtml**(resultFromStringFormat));将实体转义的 HTML 转换成Spanned对象
要了解这一点,我们来看一下Resources/Strings演示。以下是布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > <Button android:id="@+id/format" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_name" android:onClick="applyFormat" /> <EditText android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> <TextView android:id="@+id/result" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
如您所见,它只是一个按钮、一个字段和一个标签。这个想法是让用户在字段中输入他们的名字,然后单击按钮,用包含他们名字的格式化消息更新标签。
布局文件中的Button引用了一个字符串资源(@string/btn_name),所以我们需要一个字符串资源文件(res/values/strings.xml):
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">StringsDemo</string> <string name="btn_name">Name:</string> <string name="funky_format">My name is <b>%1$s</b></string> </resources>
app_name资源由android create project命令自动创建。btn_name字符串是Button的标题,而我们样式化的字符串格式在funky_format中。
最后,为了将所有这些联系在一起,我们需要一些 Java:
`package com.commonsware.android.strings;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.Html;
import android.view.View;
import android.widget.EditText; import android.widget.TextView;
public class StringsDemo extends Activity { EditText name; TextView result;
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);
name=(EditText)findViewById(R.id.name); result=(TextView)findViewById(R.id.result); }
public void applyFormat(View v) { String format=getString(R.string.funky_format); String simpleResult=String.format(format, TextUtils.htmlEncode(name.getText().toString())); result.setText(Html.fromHtml(simpleResult)); } }`
字符串资源操作可以在applyFormat()中找到,点击按钮时调用。首先,我们通过getString()获得我们的格式——为了效率,我们可以在onCreate()时间完成。接下来,我们使用这种格式格式化字段中的值,返回一个String,因为字符串资源是实体编码的 HTML。注意使用TextUtils.htmlEncode()对输入的名字进行实体编码,以防有人决定使用“与”号或其他符号。最后,我们通过Html.fromHtml()将简单的 HTML 转换成样式化的文本对象,并更新我们的标签。
当活动首次启动时,我们有一个空标签,如 Figure 23–1 所示。
**图 23–1。**strings demo 示例应用,如同最初启动的
如果我们填写一个名称并点击按钮,我们会得到如图图 23–2 所示的结果。
图 23–2。 同样的申请,在填写一些英雄人物的名字后
拿到图了?
Android 支持 PNG、JPEG、BMP、WEBP 和 GIF 格式的图像。然而,官方不鼓励使用 GIF。PNG 是最常见的格式,因为它在 Android 的早期版本中更受欢迎,而且在网络上也越来越受欢迎。冰淇淋三明治新支持 WEBP。这是一种基于 VP8 技术的编解码器,谷歌在 2010 年收购了 On2 Technologies。对于相同的图像质量,WEBP(通常读作“weppy”)提供了比 JPEG 好大约 40%的压缩率。图像可以用在任何需要Drawable的地方,比如ImageView的图像和背景。
使用图像只需将图像文件放在res/drawable/中,然后作为资源引用它们。在布局文件中,图像被引用为@drawable/...,其中省略号是文件的基本名称(例如,对于res/drawable/foo.png,资源名称是@drawable/foo)。在 Java 中,当您需要一个图像资源 ID 时,使用R.drawable.加上基本名称(例如R.drawable.foo)。
因此,让我们更新前面的例子,使用按钮的图标代替字符串资源。这个可以找到Resources/Images。我们稍微调整了布局文件,使用了一个ImageButton并引用了一个名为@drawable/icon的 drawable,该 drawable 引用了res/drawable中一个基本名为icon的图像文件。在这种情况下,我们使用 Nuvola 图标集中的 32×32 像素 PNG 文件。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > <ImageButton android:id="@+id/format" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon" android:onClick="applyFormat" /> <EditText android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> <TextView android:id="@+id/result" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
现在,我们的按钮有了想要的图标,如图 23–3 所示。
图 23–3。??【图片】Demo 示例应用
XML:资源之道
如果希望将静态 XML 打包到应用中,可以使用 XML 资源。只需将 XML 文件放在res/xml/中,就可以通过Resources对象上的getXml()来访问它,为它提供一个资源 IDR.xml.和 XML 文件的基本名称。例如,在一个活动中,对于一个 XML 文件words.xml,您可以调用getResources().getXml(R.xml.words)。这将返回一个在org.xmlpull.v1 Java 名称空间中找到的XmlPullParser的实例。
XML 拉解析器是事件驱动的:您不断调用解析器上的next()来获取下一个事件,可能是START_TAG、END_TAG、END_DOCUMENT等等。在一个START_TAG事件中,你可以访问标签的名称和属性;单个TEXT事件表示作为该元素直接子元素的所有文本节点的连接。通过循环、测试和调用每个元素的逻辑,您可以解析文件。
为了看到这一点,让我们为示例项目Files/Static重写 Java 代码,以使用 XML 资源。这个新项目Resources/XML要求您将Static中的words.xml文件放在res/xml/而不是res/raw/中。布局保持不变,因此需要替换的只是 Java 源代码:
`package com.commonsware.android.resources;
import android.app.Activity;
import android.os.Bundle;
import android.app.ListActivity;
import android.view.View;
import android.widget.AdapterView; import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.InputStream;
import java.util.ArrayList;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class XMLResourceDemo extends ListActivity { TextView selection; ArrayList items=new ArrayList();
@Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); selection=(TextView)findViewById(R.id.selection);
try { XmlPullParser xpp=getResources().getXml(R.xml.words);
while (xpp.getEventType()!=XmlPullParser.END_DOCUMENT) { if (xpp.getEventType()==XmlPullParser.START_TAG) { if (xpp.getName().equals("word")) { items.add(xpp.getAttributeValue(0)); } }
xpp.next(); } } catch (Throwable t) { Toast .makeText(this, "Request failed: "+t.toString(), Toast.LENGTH_LONG) .show(); }
setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items)); }
public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items.get(position).toString()); } }`
现在,在我们的try...catch块中,我们得到了我们的XmlPullParser,并循环直到文档结束。如果当前事件是START_TAG,元素的名称是word ( xpp.getName().equals("word")),那么我们获得唯一的属性,并将其放入选择小部件的项目列表中。因为我们对 XML 文件有完全的控制权,所以假设只有一个属性是足够安全的。在其他情况下,如果您不确定 XML 是否被正确定义,您可能会考虑检查属性计数(getAttributeCount())和属性名称(getAttributeName()),而不是假设0 -index 属性就是您所认为的那样。
结果看起来和以前一样,只是标题栏中的名称不同,如图 Figure 23–4 所示。
**图 23–4。**XML resource demo 示例应用
杂项值
在res/values/目录中,除了字符串资源,还可以放置一个或多个描述其他简单资源的 XML 文件,比如维度、颜色和数组。在前面的例子中,您已经看到了尺寸和颜色的使用,它们作为简单的字符串(例如,"10dip")作为参数传递给调用。您可以将它们设置为 Java static final 对象,并使用它们的符号名称,但是这只能在 Java 源代码中使用,而不能在布局 XML 文件中使用。通过将这些值放在资源 XML 文件中,您可以从 Java 和 layouts 中引用它们,并且将它们放在中心位置以便于编辑。
资源 XML 文件的根元素为resources;其他一切都是这个根的孩子。
尺寸
Android 中有几个地方使用维度来描述距离,比如小部件的填充。您可以使用几种不同的测量单位:
in和mm分别为英寸和毫米。这些是基于屏幕的实际尺寸。pt为积分。在出版术语中,一个点是 1/72 英寸(同样,基于屏幕的实际物理尺寸)dip和sp分别用于与设备无关的像素和与比例无关的像素。对于 160 dpi 分辨率的屏幕,一个像素等于一个dip,比例缩放基于实际的屏幕像素密度。与比例无关的像素也考虑了用户偏好的字体大小。
要将一个维度编码为一个资源,添加一个dimen元素,用一个name属性表示该资源的唯一名称,并用一个子文本元素表示值:
<resources> <dimen name="thin">10px</dimen> <dimen name="fat">1in</dimen> </resources>
在布局中,可以将维度引用为@dimen/...,其中省略号是资源的唯一名称的占位符(例如,前面示例中的thin和fat)。在 Java 中,通过以R.dimen.为前缀的惟一名称来引用维度资源(例如Resources.getDimen(R.dimen.thin))。
颜色
Android 中的颜色是十六进制的 RGB 值,还可以选择指定一个 alpha 通道。您可以选择单字符十六进制值或双字符十六进制值,提供四种样式:
#RGB#ARGB#RRGGBB#AARRGGBB
它们的工作方式与级联样式表(CSS)中的类似。
当然,您可以将这些 RGB 值作为字符串放在 Java 源代码或布局资源中。但是,如果您希望将它们转换成资源,您所需要做的就是将color元素添加到资源文件中,用一个name属性作为该颜色的唯一名称,以及一个包含 RGB 值本身的文本元素:
<resources> <color name="yellow_orange">#FFD555</color> <color name="forest_green">#005500</color> <color name="burnt_umber">#8A3324</color> </resources>
在布局中,您可以将颜色引用为@color/...,将省略号替换为该颜色的唯一名称(例如burnt_umber)。在 Java 中,通过以R.color.为前缀的惟一名称来引用颜色资源(例如Resources.getColor(R.color.forest_green))。
数组
数组资源被设计用来保存简单字符串的列表,比如一个尊称列表(先生、夫人、女士、博士等)。).
在资源文件中,每个数组需要一个string-array元素,其中一个name属性代表您赋予数组的惟一名称。然后,添加一个或多个子item元素,每个子元素都有一个文本元素,包含数组中该条目的值:
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="cities"> <item>Philadelphia</item> <item>Pittsburgh</item> <item>Allentown/Bethlehem</item> <item>Erie</item> <item>Reading</item> <item>Scranton</item> <item>Lancaster</item> <item>Altoona</item> <item>Harrisburg</item> </string-array> <string-array name="airport_codes"> <item>PHL</item> <item>PIT</item> <item>ABE</item> <item>ERI</item> <item>RDG</item> <item>AVP</item> <item>LNS</item> <item>AOO</item> <item>MDT</item> </string-array> </resources>
从您的 Java 代码中,您可以使用Resources.getStringArray()来获得列表中项目的String[]。getStringArray()的参数是数组的唯一名称,以R.array.为前缀(例如Resources.getStringArray(R.array.honorifics))。
因人而异
一组资源可能不适合应用可能使用的所有情况。一个明显的领域是字符串资源和处理国际化(I18N)和本地化(L10N)。将所有字符串放在一种语言中很好——至少对开发人员来说是这样——但是只涵盖一种语言。
然而,这并不是资源可能需要不同的唯一场景。以下是其他内容:
- 屏幕方向:屏幕是纵向还是横向?或者屏幕是方形的,因此没有方向?
- 屏幕尺寸:屏幕有多少像素,这样你就可以相应地调整你的资源(例如,大图标还是小图标)?
- 触摸屏:设备有触摸屏吗?如果是,触摸屏是设置为使用手写笔还是手指?
- 键盘:用户有哪种键盘(QWERTY,数字,两者都没有),现在有还是作为一个选项?
- 其他输入:设备是否有其他形式的输入,比如 D-pad 或点击轮?
Android 目前处理这一问题的方式是拥有多个资源目录,每个目录的标准都嵌入在其名称中。
例如,假设您希望同时支持英语和西班牙语的字符串。通常,对于单语言设置,您应该将字符串放在一个名为res/values/strings.xml的文件中。为了同时支持英语和西班牙语,您将创建两个文件夹,res/values-en/和res/values-es/,其中连字符后的值是该语言的 ISO 639-1 双字母代码。你的英语琴弦会放在res/values-en/strings.xml里,西班牙语琴弦会放在res/values-es/strings.xml里。Android 会根据用户的设备设置选择合适的文件。
更好的方法是将某种语言作为默认语言,并将这些字符串放入res/values/strings.xml中。然后,为您的翻译创建其他资源目录(例如,res/values-es/strings.xml代表西班牙语)。Android 会尝试匹配特定语言的资源集;如果做不到这一点,它将退回到res/values/strings.xml的违约状态。
看起来很简单,对吧?
当您需要对您的资源使用多个不同的标准时,事情就变得复杂了。例如,假设您想为以下设备开发:
- HTC Nexus 1,拥有正常尺寸的高密度屏幕,没有硬件键盘
- 三星 Galaxy Tab,它有一个大尺寸、高密度的屏幕,没有硬件键盘
- 摩托罗拉的魅力,它有一个小尺寸、中等密度的屏幕和一个硬件键盘
您可能希望这些设备的布局有所不同,以利用不同的屏幕空间和不同的输入选项。具体来说,您可能需要以下内容:
- 每种大小、方向和键盘组合的不同布局
- 每种密度都有不同的图案
然而,一旦你进入这种情况,各种各样的规则就开始起作用了,比如下面这些:
- 配置选项(如
-en)有特定的优先顺序,它们必须以该顺序出现在目录名中。Android 文档概述了这些选项出现的具体顺序。就本例而言,屏幕尺寸比屏幕方向更重要,屏幕方向比屏幕密度更重要,屏幕密度比设备是否有键盘更重要。 - 每个目录的每个配置选项类别只能有一个值。
- 选项区分大小写。
因此,对于示例场景,理论上,我们需要以下目录,代表可能的组合:
res/layout-large-port-mdpi-qwertyres/layout-large-port-mdpi-nokeysres/layout-large-port-hdpi-qwertyres/layout-large-port-hdpi-nokeysres/layout-large-land-mdpi-qwertyres/layout-large-land-mdpi-nokeysres/layout-large-land-hdpi-qwertyres/layout-large-land-hdpi-nokeysres/layout-normal-port-mdpi-qwertyres/layout-normal-port-mdpi-nokeysres/layout-normal-port-finger-qwertyres/layout-normal-port-hdpi-nokeysres/layout-normal-land-mdpi-qwertyres/layout-normal-land-mdpi-nokeysres/layout-normal-land-hdpi-qwertyres/layout-normal-land-hdpi-nokeysres/drawable-large-port-mdpi-qwertyres/drawable-large-port-mdpi-nokeysres/drawable-large-port-hdpi-qwertyres/drawable-large-port-hdpi-nokeysres/drawable-large-land-mdpi-qwertyres/drawable-large-land-mdpi-nokeysres/drawable-large-land-hdpi-qwertyres/drawable-large-land-hdpi-nokeysres/drawable-normal-port-mdpi-qwertyres/drawable-normal-port-mdpi-nokeysres/drawable-normal-port-finger-qwertyres/drawable-normal-port-hdpi-nokeysres/drawable-normal-land-mdpi-qwertyres/drawable-normal-land-mdpi-nokeysres/drawable-normal-land-hdpi-qwertyres/drawable-normal-land-hdpi-nokeys
别慌!我们将很快缩短此列表!
请注意,没有什么可以阻止您使用不带修饰的基本名称(res/layout)的目录。事实上,这确实是一个好主意,以防 Android 运行时的未来版本引入您没有考虑到的其他配置选项——拥有默认布局可能会影响您的应用在新设备上的工作或失败。
正如承诺的那样,我们可以大幅削减所需目录的数量。我们通过解码 Android 用于确定一组候选目录中哪个是正确的资源目录的规则来做到这一点:
- Android 会丢弃特别无效的目录。因此,举例来说,如果设备的屏幕尺寸是
normal,Android 会放弃-large目录作为候选目录,因为它们需要其他尺寸。 - Android 统计每个文件夹的匹配数,只关注匹配数最多的文件夹。
- Android 按照选项的优先顺序进行;换句话说,它在目录名中是从左到右的。
此外,我们的 drawables 只随密度而变化,而我们的布局不会随密度而变化,因此我们可以通过只关注相关平台的差异来清除许多组合。
因此,我们可以只用以下配置滑行:
res/layout-large-land-qwertyres/layout-large-qwertyres/layout-large-landres/layout-largeres/layout-normal-land-qwertyres/layout-normal-qwertyres/layout-normal-landres/layoutres/drawable-hdpires/drawable
这里,我们利用了特定匹配优先于未指定值的事实。因此,带有 QWERTY 键盘的设备将选择目录中带有qwerty的资源,而不是没有指定其键盘类型的资源。
我们可以进一步细化,仅涵盖我们针对的特定设备(例如,没有带qwerty的large设备):
res/layout-large-landres/layout-largeres/layout-land-qwertyres/layout-qwertyres/layout-landres/layoutres/drawable-hdpires/drawable
如果我们不在乎根据设备是否有硬件键盘而有不同的布局,我们可以删除两个-qwerty资源集。
我们将在第二十五章中再次看到这些资源集,其中描述了如何支持多种屏幕尺寸。
RTL 语言:双向发展
Android 2.3 增加了对更多语言的支持,超过了之前版本的平台。因此,您现在有更多的机会在需要的地方本地化您的应用。
特别是,Android 2.3 增加了对从右向左(RTL)语言的支持,尤其是希伯来语和阿拉伯语。以前 Android 只支持从左到右水平书写的语言,比如英语。这意味着你可以为 RTL 语言创建本地化版本,但是首先你需要考虑你的 UI 是否能为 RTL 语言正常工作。例如:
- 你的
TextView小部件是否在左侧与其他小部件或容器对齐?如果是,这是适合您的 RTL 用户的配置吗? - 当用户开始输入 RTL 文本时,你的
EditText窗口小部件会有什么问题吗,比如因为你没有适当地限制EditText窗口小部件的宽度而导致不适当的滚动? - 如果你在
EditText和输入法框架之外创建了自己的文本输入形式(例如,自定义屏幕虚拟键盘),它们会支持 RTL 语言吗?