安卓-4-入门指南-六-

662 阅读1小时+

安卓 4 入门指南(六)

原文:Beginning Android 4

协议:CC BY-NC-SA 4.0

三十七、通过通知提醒用户

弹出消息、托盘图标及其相关的“气泡”消息、跳跃的停靠图标……毫无疑问,你已经习惯于程序试图引起你的注意,有时是有充分理由的。除了来电,你的手机可能还会发出啁啾声:低电量、闹钟、约会通知、收到短信和电子邮件等等。

毫不奇怪,Android 有一个完整的框架来处理这类事情,统称为通知,如本章所述。

通知配置

在后台运行的服务需要一种方式来让用户知道发生了感兴趣的事情,比如何时收到了电子邮件。此外,该服务可能需要某种方式将用户引导到他们可以对事件采取行动的活动,比如阅读接收到的消息。为此,Android 提供了状态栏图标、闪光灯和其他指示器,统称为通知。

您当前的手机可能已经有这样的图标,以指示电池寿命、信号强度、蓝牙是否启用等。使用 Android,应用可以添加自己的状态栏图标,只在需要的时候才会出现(例如,有消息到达)。

在 Android 中,你可以通过系统服务NotificationManager发出通知。要使用它,您需要通过getSystemService(NOTIFICATION_SERVICE)从活动中获取服务对象。NotificationManager给你三个方法:一个养一个Notification ( notify(),两个去掉一个现有的Notification ( cancel()cancelAll())。

notify()方法接受一个Notification,这是一个数据结构,说明你的纠缠应该采取什么形式。以下部分描述了该对象的功能。

硬件通知

您可以通过将lights设置为true来闪烁设备上的 led,还可以指定颜色(作为ledARGB中的#ARGB值)和灯应该闪烁的模式(通过ledOnMSledOffMS提供灯的关闭/打开持续时间,单位为毫秒)。然而,请注意,Android 设备将尽最大努力满足您的颜色请求,这意味着不同的设备可能会给你不同的颜色,或者可能根本无法控制颜色。例如,摩托罗拉 CLIQ 只有一个白色的 LED,所以你可以要求任何你想要的颜色,你会得到白色。请注意,您需要将Notification.FLAG_SHOW_LIGHTS值或(|)到Notification对象上的公共flags域中,以使 LED 闪烁工作。

你可以使用一个Uri来播放一段内容,这段内容可能是由ContentManager ( sound)保存的。请将此视为您的应用的铃声。

您可以通过long[]控制振动设备,指示振动(vibrate)的开/关模式(毫秒)。你可以默认这样做,或者当情况需要比铃声更微妙的通知时,你可以让用户选择这个选项。不过,要使用这个,你需要请求VIBRATE许可(许可在第三十八章的中讨论)。

默认情况下,所有这些选项都会出现一次(例如,一次 LED 闪烁或一次声音回放)。如果您想让它们一直持续到Notification被取消,您需要在您的Notification中设置flags公共字段来包含FLAG_INSISTENT

除了手动指定硬件选项,您还可以使用Notification中的defaults字段,将其设置为DEFAULT_LIGHTSDEFAULT_SOUNDDEFAULT_VIBRATEDEFAULT_ALL,这将使用所有硬件选项的平台默认值。

图标

虽然闪烁的灯光、声音和振动旨在让人们看到设备,但图标旨在让他们采取下一步行动,并告诉他们什么是如此重要。

要为一个Notification设置一个图标,您需要设置两个公共字段:icon,在这里您提供一个代表图标的Drawable资源的标识符,以及contentIntent,在这里您提供一个当图标被点击时将被引发的PendingIntent。一个PendingIntent是一个常规Intent的包装器,它允许Intent稍后被另一个进程调用,启动一个活动或其他什么。通常,一个Notification将触发一个活动,在这种情况下,您将通过静态的getActivity()方法创建PendingIntent,并给它一个Intent来标识您的一个活动。也就是说,你可以通过使用PendingIntentgetBroadcast()版本,让Notification发送广播Intent。Android 4.0 扩展了PendingIntent可用的 send()方法的种类,提供了大多数可以想象的情况。

您也可以提供一个文本格式回复,当图标被放在状态栏上时出现(tickerText)。

如果您想要所有这三个,更简单的方法是调用setLatestEventInfo(),它将所有这三个封装在一个调用中。

您还可以在您的Notificationnumber公共字段中设置一个值。这将导致你提供的数字被画在一角的icon的上方。例如,这用于显示未读电子邮件的数量,这样您就不需要有一堆不同的图标,每个图标代表未读邮件的可能数量。默认情况下,number字段将被忽略且不使用。

请注意,在 Android 2.3 中,Notification图标的大小发生了变化。在那个版本之前,25 像素的正方形是理想的尺寸。现在,更矩形的每密度图标是首选:

  • 24 像素正方形(在 24 像素宽、38 像素高的边界框内),用于高密度和超高密度屏幕
  • 适用于中等密度屏幕的 16 像素正方形(在 16×25 像素边框内)
  • 用于低密度屏幕的 12 像素正方形(在 12×19 像素边界框内)

遵循这些规则的应用需要为新图标使用特定的资源集:

  • res/drawable-xhdpi-v9/:超高密度 Android 2.3 及以后版本
  • res/drawable-hdpi-v9/:针对高密度 Android 2.3 及以后版本
  • res/drawable-mdpi-v9/:适用于中密度 Android 2.3 及以后版本
  • res/drawable-ldpi-v9/:低密度 Android 2.3 及以后版本
  • res/drawable/:用于 Android 2.2 及更早版本的图标

关于包括状态栏图标在内的所有图标指南的更多细节可以在 Android 开发者文档中找到。

通知在行动

现在让我们看一下Notifications/Notify1示例项目,特别是NotifyDemo类:

`packagecom.commonsware.android.notify;

importandroid.app.Activity; importandroid.app.Notification; importandroid.app.NotificationManager; importandroid.app.PendingIntent; importandroid.content.Intent; importandroid.os.Bundle; importandroid.view.View;

public class NotifyDemo extends Activity {   private static final int NOTIFY_ME_ID=1337;   privateint count=0;   private NotificationManager mgr=null;

  @Override   public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.main);

    mgr=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);   }

  public void notifyMe(View v) {     Notification note=new Notification(R.drawable.stat_notify_chat,                                        "Status message!",                                         System.currentTimeMillis());     PendingIntenti=PendingIntent.getActivity(this, 0,                           newIntent(this, NotifyMessage.class),                                           0);

    note.setLatestEventInfo(this, "Notification Title",                            "This is the notification message", i);     note.number=++count;     note.vibrate=new long[] {500L, 200L, 200L, 500L};     note.flags|=Notification.FLAG_AUTO_CANCEL;

    mgr.notify(NOTIFY_ME_ID, note);   }

  public void clearNotification(View v) {     mgr.cancel(NOTIFY_ME_ID);   } }`

如图 Figure 37–1 所示,该活动有两个大按钮,一个用于在 5 秒钟延迟后启动通知,另一个用于取消通知(如果它处于活动状态)。

images

图 37–1。??【notify demo】活动主视图

notifyMe()中,创建通知分七个步骤完成:

  1. 创建一个带有我们的图标的Notification对象,一个当通知出现时在状态栏上闪烁的消息,以及与该事件相关的时间。
  2. 创建一个PendingIntent,它将触发另一个活动(NotifyMessage)的显示。
  3. 使用setLatestEventInfo()来指定,当通知被点击时,我们将显示某个标题和消息,如果被点击,我们将启动PendingIntent
  4. 更新与通知相关的号码。
  5. 指定振动模式:500 毫秒开启,200 毫秒关闭,200 毫秒开启,500 毫秒关闭。
  6. FLAG_AUTO_CANCEL包含在Notification对象的flags字段中。
  7. 告诉NotificationManager(在onCreate()中获得)显示通知。

因此,如果我们点击顶部的按钮,我们的图标将会出现在状态栏中,同时会简短显示我们的状态信息,如图 Figure 37–2 所示。

images

图 37–2。 我们的通知出现在状态栏上,上面有我们的状态消息

状态信息消失后,图标的右下角会叠加我们的编号(最初为 1),如图 Figure 37–3 所示。例如,您可以用它来表示未读邮件的数量。

images

图 37–3。 我们的通知加上了叠加号

如果您向下拖动图标,状态栏下方会出现一个抽屉。将该抽屉一直拖到屏幕底部,以显示未完成的通知,包括我们自己的通知,如 Figure 37–4 所示。

images

图 37–4。 的通知抽屉,完全展开,里面有我们的通知

如果您单击抽屉中的通知条目,您将被带到一个显示消息的小活动。在实际的应用中,这个活动会根据发生的事件做一些有用的事情(例如,将用户带到新到达的邮件消息)。

单击取消按钮、单击抽屉中的清除按钮或单击抽屉中的通知条目将从状态栏中移除图标。后者之所以发生,是因为我们在Notification中包含了FLAG_AUTO_CANCEL,这表明在抽屉条目上点击一下应该会取消Notification本身。Android 4.0 的用户还可以选择“滑动清除”,他们可以简单地将单个通知滑动到屏幕的任意一侧,以消除或处理它们。这有助于用户处理多个活动通知,并希望在处理完一个特定通知后返回到剩余的通知。

待在前台

通知还有另一个用途:保留选定的服务。

服务不会永远存在。Android 可能会在紧急情况下终止应用的进程来释放内存,或者只是因为它似乎已经在内存中逗留了太久。理想情况下,您设计您的服务来处理它们可能不会无限期运行的事实。

然而,如果一些服务神秘消失,用户将会错过它们。例如,Android 自带的默认音乐播放器应用使用一个服务来播放音乐。这样,用户可以一边听音乐,一边继续将手机用于其他目的。只有当用户按下音乐播放器活动中的停止按钮时,服务才会停止。如果该服务意外关闭,用户会想知道出了什么问题。

像这样的服务可以宣称自己是前台的一部分。这将导致它们的优先级上升,并使它们不太可能被挤出内存。代价是服务必须维护一个Notification,所以用户知道这个服务要求前台的一部分。而且,理想情况下,Notification应该提供一个简单的路径返回到某个用户可以停止服务的活动。

要做到这一点,在您的服务的onCreate()(或者在服务生命周期中任何有意义的地方),调用startForeground()。这需要一个Notification和一个本地唯一的整数,就像NotificationManager上的notify()方法一样。它使Notification出现,并将服务移动到前台优先级。稍后,您可以调用stopForeground()返回正常优先级。

注意,这个方法是在 Android 2.0 (API level 5)中添加的。有一个更早的方法setForeground(),在 Android 的早期版本中执行类似的功能。

fakeplicael,Redux

前一章介绍了服务模式,给出了一个假的音乐播放器,用一个Activity ( FakePlayer)和一个Service ( PlayerService)实现。实际上,PlayerService是播放音乐的地方,所以即使FakePlayer活动没有打开,音乐也可以播放。

然而,Android 可能不认为PlayerService是用户体验的一部分,因为服务通常很少直接与用户互动。这意味着 Android 可能会以限制 CPU 使用的方式运行PlayerService(不一定是坏的),如果它认为自己已经运行了太长时间(可能是坏的),可能会选择关闭服务。

答案是用startForeground()stopForeground()。当我们用play()方法开始播放音乐时,我们可以调用startForeground():

`private void play(String playlist, booleanuseShuffle) {   if (!isPlaying) {     Log.w(getClass().getName(), "Got to play()!");     isPlaying=true;

    Notification note=new Notification(R.drawable.stat_notify_chat,                                        "Can you hear the music?",                                         System.currentTimeMillis());     Intent i=new Intent(this, FakePlayer.class);

    i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|               Intent.FLAG_ACTIVITY_SINGLE_TOP);

    PendingIntent pi=PendingIntent.getActivity(this, 0,                                               i, 0);

    note.setLatestEventInfo(this, "Fake Player",                            "Now Playing: "Ummmm, Nothing"", pi);     note.flags|=Notification.FLAG_NO_CLEAR;

    startForeground(1337, note);   } }`

有利的一面是,如果需要的话,我们的服务将会有更多的 CPU 可用,并且被 Android 杀死的可能性会小得多。用户将在状态栏中看到一个图标。如果他们滑下通知抽屉并点击我们的Notification条目,他们将被带回到FakePlayer——现有的实例,如果有的话,或者一个新的实例,否则,由于我们的Intent标志(Intent.FLAG_ACTIVITY_CLEAR_TOP| Intent.FLAG_ACTIVITY_SINGLE_TOP)。对于一个音乐播放器来说,这种 UI 模式可以让用户在需要的时候快速返回停止播放音乐。

停止音乐,通过我们的stop()方法,将调用stopForeground():

private void **stop**() {   if (isPlaying) {     Log.**w**(**getClass**().**getName**(), "Got to stop()!");     isPlaying=false;     **stopForeground(true);**   } }

传递给stopForeground()true值告诉 Android 删除Notification,这将是这种模式的典型方法。

冰淇淋三明治和蜂巢中的通知

Android 3.0 中引入的蜂巢 UI,以及它在 Android 4.0 中的继任者冰激凌三明治,都支持通知,就像之前所有版本的 Android 一样。然而,由于平板电脑的比喻及其额外的屏幕空间,用户体验略有不同。

figure 37–5 显示了未修改的Notifications/Notify1项目,就像在平板电脑大小的模拟器中看到的一样。

images

图 37–5。 在安卓 3.0 平板大小的模拟器上看到的 notify 1

除了新样式的状态栏和超大按钮之外,这与你在蜂窝电话之前看到的没有什么不同。

如果我们单击顶部的按钮,我们的Notification会出现,这次是在右下角,带有图标和滚动条文本,如图 Figure 37–6 所示。

images

图 37–6。??【notify 1】增加了一个通知

请注意,如果用户点击滚动条,就会触发我们的PendingIntent,就像他们点击了电话上的通知抽屉条目一样。

当滚动条被移除时,我们的图标依然存在...无编号,如图图 37–7 所示。

images

图 37–7。 Notify1 带有一个无编号的通知图标

如果用户点击该图标,附近会出现一个通知抽屉样式的弹出窗口,如 Figure 37–8 所示。

images

图 37–8。 通知 1,通知内容出现

点击图标或文本触发PendingIntent,点击右边的×取消此Notification

三十八、请求和要求权限

在 20 世纪 90 年代末,一波病毒通过互联网传播,通过电子邮件传递,使用从微软 Outlook 中收集的联系信息。病毒会将自身的副本通过电子邮件发送给每个有电子邮件地址的 Outlook 联系人。这是可能的,因为当时 Outlook 没有采取任何措施来保护使用 Outlook API 的程序的数据,因为该 API 是为普通开发人员而不是病毒作者设计的。

如今,许多保存联系人数据的应用通过要求用户明确授予其他程序访问联系人信息的权限来保护这些数据。这些权限可以根据具体情况授予,也可以在安装时一次性授予。

Android 也不例外,因为它要求应用读取或写入联系人数据的权限。Android 的许可系统不仅对联系人数据有用,而且对 Android 框架提供的内容供应器和服务也有用。

作为一名 Android 开发人员,您经常需要确保您的应用有适当的权限来处理其他应用的数据。如果您允许其他 Android 组件使用您的数据或服务,您也可以选择要求其他应用使用您的数据或服务的权限。本章涵盖了如何实现这两个目标。

妈妈,我可以吗?

请求使用其他应用的数据或服务需要将uses-permission元素添加到您的AndroidManifest.xml文件中。您的清单可能有零个或多个uses-permission元素,都是根manifest元素的直接子元素。

uses-permission元素接受一个属性android:name,它是您的应用需要的权限的名称:

<uses-permission   android:name="android.permission.ACCESS_LOCATION" />

所有股票系统权限都以android.permission开头,并在 Android SDK 文档中列出Manifest.permission。第三方应用可能有自己的权限,希望他们已经为您记录了这些权限。以下是一些更有用的权限:

  • INTERNET,如果您的应用希望通过任何方式访问互联网,从原始 Java 套接字通过WebView小部件
  • WRITE_EXTERNAL_STORAGE,用于将数据写入 SD 卡(或设备指定的任何外部存储器)
  • NFC,用于在较新的设备上通过近场通信(NFC)无线电执行 I/O
  • ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION,用于确定设备的位置
  • CALL_PHONE,允许应用直接拨打电话,无需用户干预

权限在安装应用时得到确认。系统将提示用户确认您的应用是否可以执行权限所要求的操作。因此,要求尽可能少的权限并证明你所要求的权限是合理的是很重要的,这样用户就不会因为你要求太多不必要的权限而选择跳过安装你的应用。通过 USB 加载应用时,例如在开发过程中,此提示不会出现。

如果你没有想要的许可,并试图做一些需要它的事情,你应该得到一个SecurityException通知你缺少许可。请注意,只有当您忘记请求权限时,您才会在权限检查中失败——您的应用不可能运行,并且而不是已经被授予您所请求的权限。

立定!谁

硬币的另一面是保护您自己的应用。如果您的应用主要是活动,安全可能只是一个“出站”的东西,在那里您请求使用其他应用的资源的权利。另一方面,如果您将内容供应器或服务放在应用中,您将希望实现“入站”安全性来控制哪些应用可以对数据做什么。

请注意,这里的问题不是其他应用是否会弄乱您的数据,而是用户信息的隐私或可能会产生费用的服务的使用。这就是内置 Android 应用的股票权限所关注的地方:你是否可以阅读或修改联系人,发送短信,等等。如果你的应用不存储被认为是隐私的信息,安全性就不是问题。另一方面,如果您的应用存储私人数据,如医疗信息,安全性就更加重要了。

使用权限保护您自己的应用的第一步是再次在AndroidManifest.xml文件中声明所述权限。在这种情况下,您添加了permission元素,而不是uses-permission。同样,您可以拥有零个或更多的permission元素,全部作为根manifest元素的直接子元素。

声明权限比使用权限稍微复杂一些。您需要提供三条信息:

  • 权限的符号名:为了防止您的权限与其他应用的权限冲突,您应该使用应用的 Java 名称空间作为前缀。
  • 权限标签:选择用户能够理解的简短内容。
  • 对权限的描述:选择用户能理解的稍微长一点的内容。

下面是一个例子:

<permission   android:name="vnd.tlagency.sekrits.SEE_SEKRITS"   android:label="@string/see_sekrits_label"   android:description="@string/see_sekrits_description" />

这不会强制权限。相反,它表明这是一个可能的权限;当安全违规发生时,您的应用仍然必须标记它们。

您的应用有两种方法来实施权限,规定在什么地方和什么情况下需要权限。更简单的方法是在清单中指出哪里需要权限。更困难的选择是在代码中强制权限。接下来将讨论这两种选择。

通过清单实施权限

活动、服务和接收者都可以声明一个名为android:permission的属性,其值是访问这些项目所需的权限的名称:

<activity   android:name=".SekritApp"   android:label="Top Sekrit"   **android:permission="vnd.tlagency.sekrits.SEE_SEKRITS">**   <intent-filter>     <action android:name="android.intent.action.MAIN" />     <category       android:name="android.intent.category.LAUNCHER" />   </intent-filter> </activity>

只有已经请求您指定许可的应用才能访问安全组件。在这种情况下,“访问”的含义如下:

  • 未经许可,不能开始活动。
  • 未经许可,不能启动、停止服务或将服务绑定到活动。
  • 意图接收者忽略通过sendBroadcast()发送的消息,除非发送者允许。
在其他地方实施权限

在您的代码中,您有两种额外的方法来实施权限。

首先,您的服务可以通过checkCallingPermission()在每次调用的基础上检查权限。根据调用者是否拥有您指定的权限,这将返回PERMISSION_GRANTEDPERMISSION_DENIED。例如,如果您的服务实现了单独的读写方法,您可以通过检查这些方法是否具有您需要的 Java 权限,来要求代码中有单独的读写权限。

第二,调用sendBroadcast()时可以包含一个权限。这意味着合格的广播接收机必须持有该许可;没有许可的人没有资格得到它。我们将在本书的其他地方更详细地研究sendBroadcast()

我可以看看你的文件吗?

编译时不会自动发现权限;所有权限失败都发生在运行时。因此,记录公共 API 所需的权限非常重要,包括内容提供者、服务和从其他活动启动的活动。否则,试图与您的应用交互的程序员将不得不通过反复试验来找出权限规则。

此外,您应该预料到您的应用的用户将被提示确认您的应用说它需要的任何权限。因此,您需要为您的用户记录他们应该期望什么,以免他们被设备提出的问题弄糊涂,选择不安装或使用您的应用。您可能希望为此使用字符串资源,这样您就可以像国际化应用中所有其他消息和提示一样国际化您的权限细节。你还应该注意,你的用户会在手机或平板电脑屏幕上阅读你的许可信息,所以你的理由要简明扼要。

旧应用中的新权限

有时,Android 引入了新的权限来管理以前不需要权限的行为。WRITE_EXTERNAL_STORAGE就是一个例子。最初,应用可以在没有任何许可的情况下写入外部存储。Android 1.6 引入了WRITE_EXTERNAL_STORAGE,这是你写外存之前必须的。然而,在 Android 1.6 之前编写的应用不可能请求这种权限,因为当时并不存在这种权限。打破这些应用似乎是进步的沉重代价。

Android 所做的是在支持早期 SDK 版本的应用的某些权限中祖父。特别是,如果您的清单中有<uses-sdkandroid:minSdkVersion="3">,表示您支持 Android 1.5,您的应用将自动请求WRITE_EXTERNAL_STORAGEREAD_PHONE_STATE,即使您没有明确请求这些权限。在 Android 1.5 设备上安装您的应用的人会看到这些请求。

随着手机采用新功能以及用户对底层功能态度的成熟,引入的新权限数量也在增加,这就需要更细粒度的方法来管理访问。Android 4.0 中的新权限包括以下内容:

  • ADD_VOICEMAIL:允许应用将语音邮件注入系统
  • BIND_TEXT_SERVICE:您编写的任何从TextService派生的服务都需要
  • BIND_VPN_SERVICE:您编写的任何从VpnService派生的服务都需要
  • READ_PROFILE:提供读取用户个人资料的权限
  • WRITE_PROFILE:允许应用更新或更改用户的个人资料

最终,当你放弃对旧版本权限的支持(例如,切换到<uses-sdkandroid:minSdkVersion="4">)时,Android 将不再自动请求那些权限。因此,如果你的代码真的需要那些权限,你将需要自己申请。

*### 权限:预先或根本没有

Android 中的权限系统并不是特别灵活。值得注意的是,你必须事先申请你可能需要的所有权限,用户必须全部同意或者放弃安装你的应用。

这意味着您不能执行以下操作:

  • 创建可选的权限,用户可以对这些权限说“不,谢谢”,应用可以对这些权限做出动态反应
  • 安装后请求新的权限,这意味着即使只是一些很少使用的功能需要权限,你也必须请求

因此,在确定应用的功能列表时,牢记权限非常重要。你要求的每一个额外的许可都是一个过滤器,会让你失去一部分潜在的观众。某些组合——比如INTERNETREAD_CONTACTS——会有更强的效果,因为用户可能会担心这种组合会产生什么效果。您需要自己决定通过提供该功能来吸引更多用户是否值得为该功能的运行要求权限。*

三十九、访问基于位置的服务

当前移动设备上的一个流行功能是 GPS 功能,因此该设备可以告诉您在任何时间点的位置。虽然 GPS 服务最常见的用途是用于绘制地图和获取方向,但如果您知道自己的位置,还可以用它做其他事情。例如,您可以设置一个基于物理位置的动态聊天应用,这样用户就可以与离他们最近的人聊天。或者,你可以自动对 Twitter 或类似服务的帖子进行地理标记。

GPS 不是移动设备识别您位置的唯一方式。替代品包括以下几种:

  • 相当于 GPS 的欧洲版本,称为 Galileo,在撰写本文时仍在开发中
  • 手机信号塔三角测量,根据附近手机信号塔的信号强度来确定你的位置
  • 靠近已知地理位置的公共 Wi-Fi 热点

Android 设备可能拥有一项或多项这些服务。作为开发人员,您可以向设备询问您的位置,以及哪些供应器可用的详细信息。您甚至可以在模拟器中模拟您的位置,用于测试启用位置功能的应用。

位置提供者:他们知道你藏在哪里

Android 设备可以通过几种不同的方式来确定你的位置。有些会比其他的更准确。有些可能是免费的,而有些可能需要付费。有些可能不仅仅能告诉你你现在的位置,比如你的海拔高度或者你现在的速度。

Android 将所有这些抽象成了一组LocationProvider对象。您的 Android 环境将有零个或多个LocationProvider实例,每个实例用于设备上可用的不同定位服务。供应器不仅知道你的位置,还知道他们自己在准确性、成本等方面的特点。

作为一名开发人员,您将使用一个保存有LocationProvider集合的LocationManager,来计算哪个LocationProvider适合您的特定环境。您还需要应用中的权限,否则各种位置 API 将会由于安全违规而失败。根据您希望使用的位置供应器,您可能需要ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION,或者两者都需要(参见第三十八章)。

发现自我

对于定位服务来说,最明显的事情就是找出你现在的位置。为此,您首先需要获得一个LocationManager,因此从您的活动或服务中调用getSystemService(LOCATION_SERVICE),并将其转换为一个LocationManager。下一步是获得您想要使用的LocationProvider的名称。这里,您有两个主要选项:

  • 请用户选择一个供应器
  • 基于一组标准查找最匹配的供应器

如果你想让用户选择一个提供者,调用LocationManager上的getProviders()会给你一个提供者的List,然后你可以把它呈现给用户选择。

如果您想根据一组标准找到最匹配的提供者,创建并填充一个Criteria对象,说明您想从LocationProvider中得到什么的细节。以下是一些可用于指定标准的方法:

  • setAltitudeRequired():表示是否需要当前高度
  • setAccuracy():设置位置的最低精度等级,单位为米
  • setCostAllowed():控制供应器是否必须是免费的或者可以代表设备用户产生费用

给定一个已填充的Criteria对象,在你的LocationManager上调用getBestProvider(),Android 将筛选标准并给你最佳答案。请注意,并非您的所有标准都能得到满足;如果没有匹配,除了货币成本标准之外的所有标准都可以放宽。

您也可以使用一个LocationProvider名称(例如GPS_PROVIDER)进行硬连接,也许只是为了测试的目的。

一旦你知道了LocationProvider的名字,你就可以呼叫getLastKnownPosition()来找出你最近在哪里。然而,除非有其他原因导致期望的供应器收集定位(例如,除非 GPS 无线电打开),getLastKnownPosition()将返回null,指示没有已知位置。另一方面,getLastKnownPosition()不会产生金钱或电力成本,因为提供者不需要被激活来获取价值。

这些方法返回一个Location对象,它可以以 Java double的形式给出设备的纬度和经度,单位为度。如果特定位置供应器提供其他数据,您也可以获得:

  • 对于海拔高度,hasAltitude()将告诉您是否有海拔高度值,getAltitude()将以米为单位返回海拔高度。
  • 对于方位(即罗盘式的方向),hasBearing()将告诉您是否有可用的方位,getBearing()将返回正北以东的度数。
  • 对于速度,hasSpeed()会告诉你速度是否已知,getSpeed()会返回以米每秒为单位的速度。

不过,从LocationProvider获取Location的一个更可能的方法是注册更新,如下一节所述。

在移动中

不是所有的位置供应器都必须立即响应。例如,全球定位系统需要激活无线电,并从卫星获得定位,然后才能确定位置。这就是为什么 Android 不提供getMeMyCurrentLocationNow()方法。再加上你的用户可能不希望他们的移动反映在你的应用中,你最好注册位置更新,并以此作为获取当前位置的手段。

Internet/WeatherService/WeatherAPI示例应用展示了如何注册更新——在您的LocationManager实例上调用requestLocationUpdates()。这个方法有四个参数:

  • 您希望使用的位置提供者的名称
  • 在我们可能获得位置更新之前,应该过去多长时间(以毫秒为单位)
  • 在我们获得位置更新之前,设备必须移动多远(以米为单位)
  • 一个LocationListener,它将被通知关键的位置相关事件,如下例所示:

`LocationListener onLocationChange=new LocationListener() {   public void onLocationChanged(Location location) {     if (state.weather!=null) {       state.weather**.getForecast**(location, state);     }     else {       Log**.w(getClass**().getName(), "Unable to fetch forecast – no WeatherBinder");     }   }

  public void onProviderDisabled(String provider) {     // required for interface, not used   }

  public void onProviderEnabled(String provider) {     // required for interface, not used   }

  public void onStatusChanged(String provider, int status,                                Bundle extras) {     // required for interface, not used   } }`

这里,我们所做的就是用提供给onLocationChanged()回调方法的Location触发一个FetchForecastTask

请记住,时间参数只是从功耗角度帮助控制 Android 的一个指南。你可能会得到比这更多的位置更新。要获得最大数量的位置更新,请为时间和距离约束提供0

当您不再需要更新时,用您注册的LocationListener呼叫removeUpdates()。如果你没有做到这一点,你的应用将继续接收位置更新,即使在所有的活动和类似的关闭,这也将阻止 Android 回收你的应用的内存。

还有另一个版本的requestLocationUpdates()采用了一个PendingIntent而不是一个LocationListener。如果您想在您的位置发生变化时得到通知,即使您的代码没有运行,这也很有用。例如,如果您正在记录移动,您可以使用一个PendingIntent来触发一个BroadcastReceiver ( getBroadcast())并让BroadcastReceiver将条目添加到日志中。这样,只有当位置改变时,代码才在内存中,所以当设备不移动时,不会占用系统资源。

我们到了吗?我们到了吗?

有时候,你对你现在在哪里不感兴趣,甚至对你什么时候搬家不感兴趣,而是想知道你什么时候到达你要去的地方。这可能是一个最终目的地,也可能是到达一组方向上的下一步,所以你可以给用户下一个指令。

为了实现这一点,LocationManager提供了addProximityAlert()。这记录了一个PendingIntent,当设备到达某个位置的某个距离内时,它将被触发。addProximityAlert()方法采用以下参数:

  • 感兴趣位置的纬度和经度。
  • 一个半径,指定您应该离Intent升起的位置有多近。
  • 注册的持续时间,以毫秒为单位。过了这段时间,注册自动失效。值-1表示注册持续到您通过removeProximityAlert()手动删除它。
  • 当设备在由位置和半径表示的目标区域内时要升高的PendingIntent

请注意,这并不保证您真的会收到一个Intent。定位服务可能会中断,或者在接近警报激活期间,设备可能不在目标区域内。例如,如果位置偏离一点,并且半径过小,设备可能只会绕过目标区域的边缘,或者它可能会快速通过目标区域,以至于在此期间设备的位置没有被采样。

您可以安排一个活动或接收者来响应您注册的Intent接近警报。当Intent到来时,你做什么取决于你自己。例如,您可以设置一个通知(例如,振动设备),将信息记录到内容供应器,或将消息发布到网站。请注意,无论何时对头寸进行采样并且您在目标区域内,您都会收到Intent,而不仅仅是在进入区域时。因此,根据目标区域的大小和设备移动的速度,您可能会得到几次Intent,也许是相当多次。

测试...测试...

Android 模拟器无法通过 GPS 定位,通过手机信号塔对你的位置进行三角测量,或者通过附近的 Wi-Fi 信号识别你的位置。因此,如果您想要模拟一个移动的设备,您将需要一些向模拟器提供模拟位置数据的方法。

不管出于什么原因,随着 Android 自身的发展,这一特定领域已经发生了重大变化。过去,您可以在应用中提供模拟位置数据,这对于演示非常方便。唉,这些选项在 Android 1.0 中都被删除了。

提供模拟位置数据的一个选项是 Dalvik 调试监控服务(DDMS)。这是一个独立于仿真器的外部程序,它可以以几种不同的格式为仿真器提供单个位置点或要遍历的完整路径。在您的清单文件ACCESS_MOCK_LOCATION中包含一个特定的权限,以允许访问数据。您还需要确保在模拟器的开发人员设置下启用了“允许模拟位置”选项。第二章中的“步骤 6:设置仪器”部分解释了如何访问这些设置。DDMS 本身在第四十三章中有更详细的描述。

四十、使用 MapView 和 MapActivity 进行制图

谷歌最受欢迎的服务之一——当然是在搜索之后——是谷歌地图,它使你能够绘制从最近的比萨饼店的位置到从纽约市到旧金山的方向的所有信息(只有 2571 英里,或公制的 4135 公里),包括街景和卫星图像。

毫不奇怪,大多数 Android 设备都集成了谷歌地图。对于那些这样做的人,有一个直接从主 Android 启动器向用户提供的映射活动。作为开发人员,与您更相关的是MapViewMapActivity,它们允许您将地图集成到自己的应用中。你不仅可以显示地图,控制缩放级别,允许人们四处平移,还可以结合 Android 的基于位置的服务来显示设备的位置和去向。

幸运的是,将基本的地图功能集成到您的 Android 项目中相当容易。再努力一点,你就可以集成更复杂的地图功能。

术语,非爱称

将谷歌地图集成到第三方应用中需要同意一套相当长的法律条款。这些条款包括你可能会觉得不愉快的条款。

如果您正在考虑谷歌地图,请仔细阅读这些条款,以确定您的预期用途是否会与任何条款相冲突。如果有任何潜在的冲突,强烈建议您寻求专业的法律顾问。

此外,请留意基于其他地图数据源的其他地图选项,比如 OpenStreetMap ( [www.openstreetmap.org/](http://www.openstreetmap.org/))。

打桩

从 Android 1.5 开始,谷歌地图并不是 Android SDK 的一部分。相反,它是 Google APIs 插件的一部分,是股票 SDK 的扩展。Android 附加系统为其他子系统提供挂钩,这些子系统可能是某些设备的一部分,但不是其他设备的一部分。

**注意:**由于谷歌地图不是 Android 开源项目的一部分,一些设备由于许可问题而缺少谷歌地图。例如,在撰写本文时,Archos 5 Android 平板电脑没有谷歌地图。

总的来说,谷歌地图是一个附加软件的事实不会影响你的日常开发。但是,请记住以下几点:

  • 您需要创建具有适当目标的项目,以确保 Google Maps APIs 可用。
  • 为了测试您的 Google Maps 集成,您还需要一个使用适当目标的 AVD。

这一切的关键

如果您下载这本书的源代码,编译Maps/NooYawk项目,将其安装在您的模拟器中,并运行它,您可能会看到一个带有网格和几个图钉的屏幕,但没有实际的地图。那是因为源代码中的 API 键对于你的开发机器是无效的。相反,您将需要生成您自己的 API 密钥,以供您的应用使用。这也适用于您自己从头开始创建的任何启用地图的项目。

可以在 Android 网站上找到用于开发和生产的生成 API 密钥的完整说明。为了简洁起见,让我们把重点放在让NooYawk在您的仿真器中运行这个狭窄的例子上。为此,需要执行以下步骤:

  1. 请访问 API 密钥注册页面并查看服务条款。
  2. 重读这些服务条款,并确保你真的想同意他们。
  3. 找到用于对调试模式应用进行签名的证书的 MD5 摘要(在下面的列表中有详细描述)。
  4. 在 API 密钥注册页面上,粘贴 MD5 签名并提交表单。
  5. 在生成的页面上,复制 API 键并将其作为值粘贴到您的MapView -using 布局中。

最棘手的部分是找到用于签署调试模式应用的证书的 MD5 签名。大部分的复杂性仅仅在于理解这个概念。

所有 Android 应用都使用证书生成的数字签名进行签名。当您设置 SDK 时,系统会自动为您提供一个调试证书,并且有一个单独的过程来创建用于生产应用的自签名证书。这个签名过程涉及到 Java keytooljarsigner实用程序的使用。为了得到你的 API 密匙,你只需要担心keytool

要获取调试证书的 MD5 摘要,如果您使用的是 Mac OS X 或 Linux,请使用以下命令:

keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore![Images](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06a41891341e40b297a04d55ee0b92f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1773066454&x-signature=IAgFfXtMTlxlYTToGbGyx%2BNWLQI%3D)  -storepass android -keypass android

在其他开发平台上,比如 Windows,您需要用您的平台和用户帐户的位置替换-keystore开关的值(其中<user>是您的帐户名称):

  • 在 Windows XP 上,使用C:\Documents and Settings\<user>\.android\debug.keystore
  • 在 Windows Vista 或 Windows 7 上,使用C:\Users\<user>\.android\debug.keystore

输出的第二行包含您的 MD5 摘要,是由冒号分隔的一系列十六进制数字对。

光秃秃的骨头

要将地图放入您的应用,您需要创建自己的子类MapActivity。就像ListActivity总结了由ListView主导的活动背后的一些智慧一样,MapActivity处理了设置由MapView主导的活动的一些细微差别。A MapView只能被 a MapActivity使用,不能被任何其他类型的Activity使用。

MapActivity子类的布局中,您需要添加一个名为com.google.android.maps.MapView的元素。这是拼写小部件类名的“手写”方式,包括完整的包名和类名。这是必要的,因为MapView不在android.widget名称空间中。你可以给MapView小部件任何你想要的android:id属性值,并处理所有的布局细节,让它和你的其他小部件一起正确呈现。

但是,您需要具备以下物品:

  • android:apiKey,您的谷歌地图 API 密钥
  • 如果您希望用户能够点击并平移您的地图

例如,在Maps/NooYawk示例应用中,主要布局如下:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent">   <com.google.android.maps.MapView android:id="@+id/map"     android:layout_width="fill_parent"     android:layout_height="fill_parent"     android:apiKey="00yHj0k7_7vxbuQ9zwyXI4bNMJrAjYrJ9KKHgbQ"     android:clickable="true" /> </RelativeLayout>

此外,在您的AndroidManifest.xml文件中还需要一些额外的东西:

  • INTERNETACCESS_FINE_LOCATION权限(后者与MyLocationOverlay类一起使用,将在本章后面描述)
  • 在您的<application>中,一个带有android:name = "com.google.android.maps"<uses-library>元素,表示您正在使用一个可选的 Android APIs

以下是NooYawkAndroidManifest.xml文件:

` <manifest xmlns:android="schemas.android.com/apk/res/and…](p9-xtjj-sign.byteimg.com/tos-cn-i-73…)  package="com.commonsware.android.maps">      

                                                       <supports-screens android:largeScreens="true" android:normalScreens="true"Images  android:smallScreens="true" android:anyDensity="true"/> `

除了从MapActivity开始对活动进行子类化之外,对于初学者来说,这几乎就是你所需要的全部了。如果你什么都不做,构建这个项目并把它扔进模拟器,你会得到一个漂亮的世界地图。然而,请注意,MapActivity是抽象的——您需要实现isRouteDisplayed()来表明您是否在提供某种驾驶方向。由于当前版本的服务条款不支持显示行驶方向,您应该让isRouteDisplayed()返回false

可选地图

虽然大多数主流 Android 设备都有谷歌地图,但有一小部分没有,因为他们的制造商没有选择从谷歌获得许可。因此,您需要决定 Google Maps 对于您的应用的运行是否至关重要。

如果谷歌地图是必不可少的,那么在你的应用中包含<uses-library>元素,如前所示,因为这将要求任何运行你的应用的设备都要有谷歌地图。

如果谷歌地图不是必不可少的,你可以通过<uses-library>上的android:required属性使其成为可选的。设置为false,如果谷歌地图可用的话,它会被加载到你的应用中,但是不管怎样,你的应用都会运行良好。然后,您将需要使用类似Class.forName("com.google.android.maps.MapView")的东西来查看您的应用是否可以使用 Google Maps。如果不是,你可以禁用它的菜单项,或者任何可以引导用户到你的MapActivity的东西。

**注意:**在旧的文档中,android:required属性是“未记录的”,它的使用和支持是有问题的。谷歌现在已经正式记录了它,它在 Android 4.0 冰淇淋三明治和未来的版本中可用。

运用你的控制力

你可以通过findViewById()找到你的MapView部件,就像其他部件一样。小部件本身提供了一个getController()方法。在MapViewMapController之间,你有相当多的能力来决定地图显示什么以及它如何表现。缩放和居中是您可能想要使用的两个功能,因此它们将在接下来讨论。

缩放

你开始时的世界地图相当广阔。通常,在手机上看地图的人会期待范围稍微窄一点的东西,比如几个城市街区。

您可以通过MapController上的setZoom()方法直接控制缩放级别。这需要一个代表缩放级别的整数,其中1是世界视图,21是你能得到的最紧密的缩放。每一级都是有效分辨率的两倍:1的赤道宽度为 256 像素,而21的赤道宽度为 268,435,456 像素。由于手机的显示屏可能在两个维度上都没有 268,435,456 像素,用户看到的是聚焦在地球一个微小角落的小地图。一个17的关卡会在每个维度显示几个城市街区,这可能是你尝试的一个合理起点。

如果您希望允许用户更改缩放级别,调用setBuiltInZoomControls(true);,用户将能够通过地图底部中心的缩放控件放大和缩小地图。

居中

通常,除了缩放级别之外,您还需要控制地图显示的内容,例如用户的当前位置或与活动中的一些数据一起保存的位置。要改变地图的位置,调用MapController上的setCenter()

setCenter()方法将一个GeoPoint作为参数。一个GeoPoint通过纬度和经度代表一个位置。问题是GeoPoint将纬度和经度存储为整数,表示微度数(度数乘以 1E6)中的实际纬度和经度。与存储一个floatdouble相比,这节省了一点内存,并且大大加快了 Android 将GeoPoint转换成地图位置所需的一些内部计算。但是,这确实意味着您必须记住将真实世界的纬度和经度乘以 1E6。

层层叠叠

如果你曾经使用过全尺寸版的谷歌地图,你可能会习惯于看到覆盖在地图上的东西,比如图钉指示被搜索位置附近的企业。在地图术语中(就此而言,在许多严肃的图形编辑器中),图钉位于与地图本身不同的图层上,您看到的是地图图层上图钉图层的组合。

Android 的地图也允许你创建图层,所以你可以根据用户输入和你的应用的目的来标记你需要的地图。例如,NooYawk使用一个层来显示选择的建筑物在曼哈顿岛上的位置。

叠加类

任何想要添加到地图上的覆盖图都需要作为Overlay的子类来实现。如果你想添加图钉之类的东西,可以使用一个ItemizedOverlay子类;ItemizedOverlay简化了这个过程。

要给你的地图添加一个覆盖类,只需调用你的MapView上的getOverlays()和你的Overlay实例上的add(),就像我们在这里使用自定义的SitesOverlay一样:

`marker.setBounds(0, 0, marker.getIntrinsicWidth(),                        marker.getIntrinsicHeight());

map.getOverlays().add(new SitesOverlay(marker));`

我们将在下一节中讨论marker

绘制明细 Overlay

顾名思义,ItemizedOverlay允许您提供要在地图上显示的兴趣点列表——特别是OverlayItem的实例。然后,覆盖层会为您处理大部分绘图逻辑。以下是实现这一目标的最基本步骤:

  1. 覆盖ItemizedOverlay<OverlayItem>作为自己的子类(在这个例子中是SitesOverlay)。
  2. 在构造器中,构建您的OverlayItem实例花名册,当它们准备好供覆盖使用时,调用populate()
  3. 实现size()以返回覆盖要处理的项目数。
  4. 重写createItem()以返回给定索引的OverlayItem实例。
  5. 当你实例化你的ItemizedOverlay子类时,给它提供一个Drawable来表示为每个项目显示的默认图标(例如,图钉),在这个图标上你调用boundCenterBottom()来启用投影效果。

来自NooYawk构造函数的marker是用于步骤 5 的Drawable,它显示了一个图钉。

比如这里是SitesOverlay:

`private class SitesOverlay extends ItemizedOverlay {   private List items=new ArrayList();   private Drawable marker=null;

  public SitesOverlay(Drawable marker) {     super(marker);     this.marker=marker;

    boundCenterBottom(marker);

    items.add(new OverlayItem(getPoint(40.748963847316034,                                        -73.96807193756104),                              "UN", "United Nations"));     items.add(new OverlayItem(getPoint(40.76866299974387,                                        -73.98268461227417),                              "Lincoln Center",                              "Home of Jazz at Lincoln Center"));     items.add(new OverlayItem(getPoint(40.765136435316755,                                        -73.97989511489868),                              "Carnegie Hall",             "Where you go with practice, practice, practice"));     items.add(new OverlayItem(getPoint(40.70686417491799,                                        -74.01572942733765),                              "The Downtown Club",                      "Original home of the Heisman Trophy"));

    populate();   }

  @Override   protected OverlayItem createItem(int i) {     return(items.get(i));   }

  @Override   protected boolean onTap(int i) {     Toast.makeText(NooYawk.this,                     items.get(i).getSnippet(),                     Toast.LENGTH_SHORT).show();

    return(true);   }

  @Override   public int size() {     return(items.size());   } }`

处理屏幕点击

一个Overlay子类也可以实现onTap(),当用户点击地图时得到通知,这样覆盖图可以调整它所绘制的内容。例如,在全尺寸的谷歌地图中,点击一个图钉会弹出一个气泡,显示图钉所在位置的企业信息。有了onTap(),你可以在 Android 上做同样的事情。

用于ItemizedOverlayonTap()方法接收被点击的OverlayItem的索引。这取决于你做一些有意义的事情。

SitesOverlay的情况下,如前一节所示,onTap()看起来像这样:

`@Override protected boolean onTap(int i) {   Toast.makeText(NooYawk.this,                   items.get(i).getSnippet(),                   Toast.LENGTH_SHORT).show();

  return(true); }`

这里,我们只是用来自OverlayItem的片段抛出一个简短的Toast,返回true来表示我们处理了点击。

我的、我自己的和我的位置覆盖

Android 有一个内置的覆盖来处理两种常见的情况:

  • 基于 GPS 或其他位置提供逻辑,显示您在地图上的位置
  • 根据内建的指南针传感器显示您被指向的位置(如果有)

你所需要做的就是创建一个MyLocationOverlay实例,将其添加到你的MapView的覆盖列表中,并在适当的时候启用和禁用所需的功能。

“在适当的时候”的概念是为了最大限度地延长电池寿命。当活动暂停时,更新位置或方向没有意义,因此建议您在onResume()中启用这些功能,在onPause()中禁用它们。

例如,NooYawk将使用MyLocationOverlay显示罗盘。为此,我们首先需要创建覆盖图,并将其添加到覆盖图列表中(其中me是作为私有数据成员的MyLocationOverlay实例):

me=new **MyLocationOverlay**(this, map); map.**getOverlays**().**add**(me);

然后,我们根据需要启用和禁用罗盘:

`@Override public void onResume() {   super.onResume();

  me.enableCompass(); }

@Override public void onPause() {   super.onPause();

  me.disableCompass(); }`

当活动在屏幕上时,这给了我们一个罗盘,如图 Figure 40–1 所示。

images

图 40–1。 诺亚克地图,显示一个罗盘和两个重叠的物品

崎岖的地形

正如你在全尺寸电脑上使用的谷歌地图可以显示卫星图像一样,安卓地图也可以。

MapView提供toggleSatellite(),顾名思义,它可以打开和关闭正在查看的区域的卫星视角。您可以允许用户通过选项菜单来触发,或者在使用NooYawk的情况下,通过按键来触发:

` @Override   public boolean onKeyDown(int keyCode, KeyEvent event) {     if (keyCode == KeyEvent.KEYCODE_S) {       map.setSatellite(!map.isSatellite());       return(true);     }     else if (keyCode == KeyEvent.KEYCODE_Z) {       map.displayZoomControls(true);       return(true);     }

    return(super.onKeyDown(keyCode, event));   }`

图 40–2 显示了点击S键后NooYawk中的卫星视图。

images

**图 40–2。**NooYawk 地图,显示一个罗盘和两个重叠的项目,重叠在卫星视图上

地图和片段

您可能认为地图是使用片段的理想场所。毕竟,在一个大的平板电脑屏幕上,你可以将大部分空间分配给地图,但旁边仍然有其他东西。唉,在 Android 的最后两个主要版本中,地图和片段仍然是两种很好的口味,但合在一起就不那么好了。

首先,MapView要求你继承MapActivity。这有几个后果:

  • 不能使用 Android 兼容库(ACL),因为那需要你从FragmentActivity继承,而 Java 不支持多重继承。因此,你只能在 Android 3.0 和更高版本上使用分段地图,这需要你在旧版本的 Android 上使用一些替代实现。
  • 任何可能在片段中托管地图的活动都必须从MapActivity继承,即使在某些情况下它可能不在片段中托管地图。

另外,MapView对各种事件的时间做了一些假设,这使得建立一个基于地图的片段变得更加复杂。

完全有可能有一天这些问题会得到解决,通过一个更新的 Google APIs Android 插件与片段支持的结合,可能还有一个更新的 ACL。与此同时,这是让地图尽可能地分段工作的方法。

限制自己使用最新的安卓版本

在清单中,确保将您的android:minSdkVersionandroid:targetSdkVersion都设置为至少11,这样您的应用只能在 Android 3.0 和更新版本上运行。例如,下面是来自Maps/NooYawkFragments示例项目的清单:

` <manifest xmlns:android="schemas.android.com/apk/res/and…"           package="com.commonsware.android.maps">      

  <application android:label="@string/app_name"               android:icon="@drawable/cw"               android:hardwareAccelerated="true">                                                         `

使用 onCreateView()和 onActivityCreated()

基于地图的片段只是一个显示一个MapViewFragment。总的来说,这段代码看起来和工作起来很像一个MapActivity,配置MapView,设置ItemizedOverlay,等等。

然而,有一个时间问题:您不能可靠地从onCreateView()返回一个MapView小部件,或者包含这样一个小部件的膨胀布局。不管出于什么原因,第一次运行正常,但是在配置改变时(例如屏幕旋转),它会失败。

解决方案是从onCreateView()返回一个容器,比如一个FrameLayout,如这里的NooYawkFragmentsMapFragment类所示:

@Override public View **onCreateView**(LayoutInflater inflater, ViewGroup container,                          Bundle savedInstanceState) {   return(new **FrameLayout**(**getActivity**())); }

然后,在onActivityCreated()——一旦onCreate()在托管MapActivity中完成——您可以向该容器添加一个MapView,并继续您的正常设置:

`@Override public void onActivityCreated(Bundle savedInstanceState) {   super.onActivityCreated(savedInstanceState);

  map=new MapView(getActivity(), "00yHj0k7_7vxbuQ9zwyXI4bNMJrAjYrJ9KKHgbQ");   map.setClickable(true);

  map.getController().setCenter(getPoint(40.76793169992044,                                         -73.98180484771729));   map.getController().setZoom(17);   map.setBuiltInZoomControls(true);

  Drawable marker=getResources().getDrawable(R.drawable.marker);

  marker.setBounds(0, 0, marker.getIntrinsicWidth(),                          marker.getIntrinsicHeight());

  map.getOverlays().add(new SitesOverlay(marker));

  me=new MyLocationOverlay(getActivity(), map);   map.getOverlays().add(me);

  ((ViewGroup)getView()).addView(map); }`

注意,我们在 Java 代码中创建了一个MapView,这意味着我们的 Maps API 键驻留在 Java 代码中(或者可以从 Java 代码中获得的东西,比如一个字符串资源)。如果您愿意,您可以在这里展开包含一个MapView的布局——MapFragment的变化只是为了说明从 Java 代码创建一个MapView

在 MapActivity 中托管片段

您必须确保托管支持地图的片段的任何活动都是一个MapActivity。因此,即使NooYawk活动不再与映射有太大关系,它仍然是一个MapActivity:

`package com.commonsware.android.maps;

import android.os.Bundle; import com.google.android.maps.MapActivity;

public class NooYawk extends MapActivity {   @Override   public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.main);   }

  @Override   protected boolean isRouteDisplayed() {     return(false);   } }`

布局现在指向一个<fragment>而不是一个MapView:

<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android"   class="com.commonsware.android.maps.MapFragment"   android:id="@+id/map_fragment"   android:layout_width="fill_parent"   android:layout_height="fill_parent" />

最终的应用,如 Figure 40–3 所示,看起来像大屏幕上的原始NooYawk活动,因为我们没有对片段系统做任何其他事情(例如,在风景布局中有其他片段)。

images

图 40–3。 摩托罗拉 XOOM 上渲染的 NooYawkFragments 地图

地图和片段的定制替代方案

可以理解,Android 开发者社区对地图在片段中的限制感到沮丧,并努力应对MapView需要在MapActivity中的限制。你可以在 StackOverflow Android 开发者论坛上看到实验和讨论的历史,这导致了贡献者之一 Pete Doyle 发布了android-support-v4-googlemaps自定义兼容性库。

Doyle 的自定义兼容性库是一个使FragmentActivity扩展MapActivity的工作解决方案。这反过来允许你在一个片段中使用一个MapView对象。

你可以在[github.com/petedoyle/android-support-v4-googlemaps](https://github.com/petedoyle/android-support-v4-googlemaps)从 GitHub 下载support-v4-googlemaps自定义兼容性库。

四十一、处理电话

许多,如果不是大多数,Android 设备将是手机。因此,不仅用户会期待使用 Android 拨打和接听电话,而且如果你愿意,你将有机会帮助他们拨打电话。

为什么你会想呢?

  • 也许你正在为一个销售管理应用(a laSalesforce.com)编写一个 Android 界面,你想让用户只需点击一个按钮就能给潜在客户打电话,而不必让他们在你的应用和手机的联系人应用中都保留这些联系人。
  • 也许您正在编写一个社交网络应用,您可以访问的电话号码列表会不断变化,因此与其尝试将社交网络联系人与手机的联系人数据库同步,不如让人们直接从您的应用拨打电话。
  • 也许你正在创建一个现有联系人系统的替代界面,也许是为运动控制能力下降的用户(例如,老年人),运动大按钮等,使他们更容易拨打电话。

不管是什么原因,Android 有办法让你像操作 Android 系统的任何其他部分一样操作手机。

向经理汇报

为了获得更多的电话 API,可以使用TelephonyManager类。该类允许您执行如下操作:

  • 通过getCallState()确定电话是否在使用中,返回值为CALL_STATE_IDLE(电话未使用)CALL_STATE_RINGING(已请求通话但仍在连接中)CALL_STATE_OFFHOOK(通话进行中)
  • 通过getSubscriberId()找到 SIM ID (IMSI)
  • 通过getPhoneType()查找电话类型(如 GSM)或通过getNetworkType()查找数据连接类型(如 GPRS 或 EDGE)

你打电话吧!

您还可以从您的应用发起呼叫,例如从您通过自己的 web 服务获得的电话号码。要做到这一点,只需制作一个带有形式为tel:NNNNNUriACTION_DIALIntent(其中NNNNN是要拨打的电话号码)并使用带有startActivity()Intent。这实际上不会拨打电话;相反,它激活了拨号器活动,用户可以点击一个按钮发出呼叫。

例如,让我们看一下Phone/Dialer示例应用。以下是简单但有效的布局:

<?xml version="1.0" encoding="utf-8"?> <LinearLayoutxmlns: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"     >     <TextView       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:text="Number to dial:"       />     <EditTextandroid:id="@+id/number"       android:layout_width="fill_parent"       android:layout_height="wrap_content"       android:cursorVisible="true"       android:editable="true"       android:singleLine="true"     />   </LinearLayout>   <Button android:id="@+id/dial"     android:layout_width="fill_parent"     android:layout_height="wrap_content"     android:layout_weight="1"     android:text="Dial It!"     android:onClick="dial"   /> </LinearLayout>

我们有一个用于输入电话号码的标签栏,还有一个用于拨打该号码的按钮。

Java 代码只是使用字段中的电话号码启动拨号器:

packagecom.commonsware.android.dialer; `importandroid.app.Activity; importandroid.content.Intent; importandroid.net.Uri; importandroid.os.Bundle; importandroid.view.View; importandroid.widget.EditText;

public class DialerDemo extends Activity {   @Override   public void onCreate(Bundle icicle) {     super**.onCreate**(icicle);     setContentView(R.layout.main);   }

  public void dial(View v) {     EditText number=(EditText)findViewById(R.id.number);     String toDial="tel:"+number.getText().toString();

    startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(toDial)));   } }`

活动本身的 UI 并没有那么令人印象深刻,如图 Figure 41–1 所示。

images

**图 41–1。**dialer demo 示例应用,最初启动时

不过你点击拨号键得到的拨号器更好,显示你要拨的号码,如图 Figure 41–2 所示。

images

图 41–2。 安卓拨号器活动,从拨号器发起

不,真的,你来打电话!

好消息是ACTION_DIAL不需要任何特殊权限就可以工作。坏消息是它只带用户到拨号器。用户仍然需要采取行动(按绿色的呼叫按钮)来实际拨打电话。

另一种方法是使用ACTION_CALL而不是ACTION_DIAL。在一个ACTION_CALLIntent上调用startActivity()将立即发出电话呼叫,不需要任何其他的 UI 步骤。然而,你需要得到CALL_PHONE的许可才能使用ACTION_CALL(见第三十八章)。

四十二、字体

在开发任何类型的应用时,你都会不可避免地遇到这样的问题:“嘿,我们能改变这种字体吗?”答案取决于平台自带的字体,是否可以添加其他字体,以及字体如何应用到小部件或任何需要改变字体的地方。安卓也不例外。它附带了一些字体,以及添加新字体的方法。然而,如同任何新环境一样,有一些特质需要处理,如本章所述。

爱和你在一起的人

Android 本身就知道三种字体,简写为 sans、serif 和 monospace。这些字体实际上是 Droid 系列字体,由 Monotype Imaging 的一个部门 Ascender Corp. ( [www.ascendercorp.com/](http://www.ascendercorp.com/))为开放手机联盟创建。要使用这三种字体,您可以在您的布局 XML 中引用它们,例如下面来自Fonts/FontSampler示例项目的布局:

<?xml version="1.0" encoding="utf-8"?> <TableLayout   xmlns:android="http://schemas.android.com/apk/res/android"   android:layout_width="fill_parent"   android:layout_height="fill_parent"   android:stretchColumns="1">   <TableRow>     <TextView       android:text="sans:"       android:layout_marginRight="4dip"       android:textSize="20sp"     />     <TextView       android:id="@+id/sans"       android:text="Hello, world!"       android:typeface="sans"       android:textSize="20sp"     />   </TableRow>   <TableRow>     <TextView       android:text="serif:"       android:layout_marginRight="4dip"       android:textSize="20sp"     />     <TextView       android:id="@+id/serif"       android:text="Hello, world!"       android:typeface="serif"       android:textSize="20sp"     />   </TableRow>   <TableRow>     <TextView       android:text="monospace:"       android:layout_marginRight="4dip"       android:textSize="20sp"     />   <TextView       android:id="@+id/monospace"       android:text="Hello, world!"       android:typeface="monospace"       android:textSize="20sp"     />   </TableRow>   <TableRow>     <TextView       android:text="Custom:"       android:layout_marginRight="4dip"       android:textSize="20sp"     />     <TextView       android:id="@+id/custom"       android:text="Hello, world!"       android:textSize="20sp"     />   </TableRow>   <TableRowandroid:id="@+id/filerow">     <TextView       android:text="Custom from File:"       android:layout_marginRight="4dip"       android:textSize="20sp"     />     <TextView       android:id="@+id/file"       android:text="Hello, world!"       android:textSize="20sp"     />   </TableRow> </TableLayout>

此布局构建了一个显示五种字体的简短示例的表格。注意前三个有android:typeface属性,它的值是三个内置字体之一(例如“sans"”)。

附加字体

三个内置字体都很好看。然而,设计师、经理或客户可能想要不同的字体。或者,您可能希望使用一种用于特殊目的的字体,例如,用丁巴特字体代替一系列 PNG 图形。实现这一点最简单的方法是将所需的字体与应用打包在一起。为此,只需在项目根目录下创建一个assets/文件夹,并将您的 TrueType (TTF)字体放入该文件夹。例如,您可以创建assets/fonts /并将您的 TTF 文件放在那里。

然后,您需要告诉您的小部件使用该字体。不幸的是,您不能再为此使用布局 XML,因为 XML 不知道您可能已经作为应用素材隐藏起来的任何字体。相反,您需要在 Java 代码中进行更改:

`importandroid.widget.TextView; importjava.io.File;

public class FontSampler extends Activity {   @Override   public void onCreate(Bundle icicle) {     super**.onCreate**(icicle);     setContentView(R.layout.main);

    TextViewtv=(TextView)findViewById(R.id.custom);     Typeface face=Typeface**.createFromAsset**(getAssets(),                                           "fonts/HandmadeTypewriter.ttf");

    tv**.setTypeface**(face);

    File font=new File(Environment**.getExternalStorageDirectory**(),                      "MgOpenCosmeticaBold.ttf");

    if (font**.exists**()) {       tv=(TextView)findViewById(R.id.file);       face=Typeface**.createFromFile**(font);

      tv**.setTypeface**(face);     }     else {       findViewById(R.id.filerow).setVisibility(View.GONE);     }   } }`

这里,我们为我们的定制样本获取了TextView,然后通过静态的createFromAsset()构建器方法创建了一个Typeface对象。这需要应用的AssetManager(来自getAssets())和你的assets/目录中的一个路径到你想要的字体。

然后,只需要告诉TextViewsetTypeface(),提供我们刚刚创建的Typeface。在这种情况下,我们使用手工打字机字体。您也可以从本地文件中加载字体并使用它。好处是你可以在你的应用发布后定制你的字体。另一方面,你必须想办法把字体放到设备上。但是,正如你可以通过createFromAsset()得到一个Typeface,你也可以通过createFromFile()得到一个Typeface。在我们的FontSampler中,我们在“外部存储器”(通常是 SD 卡)的根目录中寻找MgOpenCosmeticaBold TrueType 字体文件,如果找到了,我们将它用于表的第五行。否则,我们隐藏该行。

图 42–1 显示了结果。

images

图 42–1。??【font sampler】应用

我们将在接下来的章节中详细介绍素材和本地文件。

注意,Android 似乎并不喜欢所有的 TrueType 字体。当 Android 不喜欢某个自定义字体时,它似乎会悄悄地用 Droid Sans ( "sans")代替,而不是升起一个Exception。因此,如果你试图使用不同的字体,但它似乎没有工作,它可能与 Android 不兼容,无论出于什么原因。

这里一字形,那里一字形

TrueType 字体可能会很笨重,尤其是当它们支持大量可用的 Unicode 字符时。上一节用的手工打字机字体运行 70KB 以上;DejaVu 免费字体每个可以运行 500KB 以上。即使是压缩的,也会增加你的应用的体积,所以注意不要过度使用自定义字体,以免你的应用在用户的手机上占据太多空间。

相反,请记住,字体可能没有您需要的所有字形。举个例子,我们来说说省略号。

Android 的TextView类具有“省略”文本的内置功能,如果文本长度超过可用空间,它会截断文本并添加省略号。例如,您可以通过android:ellipsize属性来使用它。至少对于单行文本来说,这样做效果很好。

安卓用的省略号不是三个句号。相反,它使用了一个实际的省略号字符,其中三个点包含在一个字形中。因此,您使用的任何也使用“椭圆化”功能的字体都需要省略号标志符号。

然而,除此之外,Android 填充了呈现在屏幕上的字符串,使得长度(以字符为单位)在椭圆化前后是相同的。为了做到这一点,Android 用省略号替换一个字符,用 Unicode 字符ZERO WIDTH NO-BREAK SPACE (U+FEFF)替换所有其他被删除的字符。因此,省略号后面的额外字符不会占用屏幕上的任何可见空间,但它们可以是字符串的一部分。然而,这意味着任何用于与android:ellipsize一起使用的TextView部件的定制字体也必须支持这种特殊的 Unicode 字符。并不是所有的字体都是这样,如果你的字体缺少这个字符(例如,流氓 X s 出现在行尾),你会在屏幕上看到你缩短的字符串。这种方法的另一个副作用是,预测任何字符串长度算法(比如一个简单的length() Java 调用)是否会返回您期望的值是有风险的。这几乎是反直觉的,但 Android 试图给你一个一致的答案,而你以前使用 Java 和字体的经验可能会让你期望不同的结果。

当然,Android 的国际部署意味着你的字体必须处理用户可能希望输入的任何语言,也许是通过特定语言的输入法编辑器。

因此,虽然在 Android 中使用自定义字体是非常可能的,但也有许多潜在的问题,所以你必须仔细权衡自定义字体的好处和潜在的成本。

四十三、更多开发工具

Android SDK 不仅仅是一个 Java 类和 API 调用库。它还包括许多有助于应用开发的工具。当然,Eclipse 倾向于主导讨论。然而,这并不是您可以使用的唯一工具,所以让我们快速浏览一下您还可以使用哪些工具。

层次查看器:你的代码有多深?

Android 附带了一个层次查看器工具,旨在帮助您可视化您的布局,就像在运行的仿真器中的运行活动中看到的一样。因此,举例来说,您可以确定某个小部件占用了多少空间,或者尝试找到没有出现在屏幕上的小部件隐藏在哪里。

要使用 Hierarchy Viewer,首先需要启动仿真器,安装应用,启动“活动”,并导航到要检查的位置。请注意,您不能在生产 Android 设备上使用层次查看器。

您可以通过 Android SDK 安装中的tools/目录下的hierarchyviewer程序启动层次结构查看器,或者从 Eclipse 内部启动。主窗口如图 43–1 所示。

images

图 43–1。 层级查看器主窗口

该表的根显示了当前在您的开发机器上运行的模拟器实例。树叶代表在特定仿真器上运行的应用。您的活动将通过应用包和类别(如com.commonsware.android.files/...)来识别。

当你选择一个窗口并点击 Load View Hierarchy 时,事情变得有趣了。几秒钟后,细节映入眼帘,如图 43–2 所示。

images

图 43–2。 层级查看器布局视图

布局视图的主要区域显示了一个由各种小部件和组成您的活动的东西组成的树,从整个系统窗口开始,一直到用户将与之交互的各个 UI 小部件。这包括您的应用定义的小部件和容器,以及系统提供的其他小部件和容器,包括标题栏。

单击其中一个视图可为该透视图添加更多信息,如图 Figure 43–3 所示。

images

图 43–3。 层次查看器查看属性

现在,在层次查看器的中右区域,您可以看到所选小部件或容器的属性,以及渲染该容器及其子容器所用时间的计时细节。

此外,小部件在活动的线框中以红色突出显示,显示在属性下面(默认情况下,视图显示为黑色背景上的白色轮廓)。这可以帮助您确保选择了正确的小部件,比如说,如果您有几个按钮,并且不能从树中轻易地判断出哪个按钮是哪个按钮。

您还可以在层次结构查看器主窗口中执行以下操作:

  • 将树形图保存为 PNG 文件
  • 将 UI 保存为 Photoshop PSD 文件,不同的小部件和容器使用不同的图层
  • 如果您对数据库或应用的内容进行了更改,并且需要一个新的图表,请强制 UI 在模拟器中重新绘制或重新加载层次结构

您可以单击检查屏幕截图,而不是在主窗口中单击加载视图层次。这将层次查看器置于一个全新的视角,称为像素完美视图,如图 Figure 43–4 所示。

images

图 43–4。 层次浏览器像素完美视图

在左侧,您会看到一个树,表示您活动中的小部件和其他View。在中间,您可以看到您的活动的放大视图,它以正常大小显示在右侧。

覆盖在活动上的十字光标显示被缩放的位置。只需点击一个新的区域来改变你所看到的。有一个控制缩放级别的滑块。单击像素还会指示该像素的位置和颜色。

如果您选中工具栏中的“自动刷新”复选框,层次查看器将定期轮询并从您的活动中重新加载 UI,频率由另一个滑块控制。

DDMS:在安卓的引擎盖下

Android 开发者的另一个工具是 Dalvik 调试监控服务(DDMS)。这就像一把瑞士军刀,允许您做任何事情,从浏览日志文件,更新模拟器提供的 GPS 位置,模拟来电和消息,以及浏览模拟器上的存储以推送和拉取文件。

要启动 DDMS,运行 Android SDK 发行版中tools/目录下的ddms程序,或者在 Eclipse 中打开 DDMS 透视图。它最初会在左边显示一个模拟器和运行程序的树,如图 Figure 43–5 所示。

images

图 43–5。 DDMS 初始视图

单击仿真器允许您浏览底部的事件日志,并通过右侧的选项卡操纵仿真器,如 Figure 43–6 所示。

images

图 43–6。 DDMS,用模拟器选中

伐木

DDMS 让你在一个可滚动的表格中查看你的日志信息,而不是使用adb logcat。只需突出显示您想要监控的仿真器或设备,屏幕的下半部分就会显示日志。

此外,您可以执行以下操作:

  • 按五个日志记录级别中的任何一个筛选日志选项卡,显示为工具栏按钮 V 到 E。
  • 创建一个自定义过滤器,这样您就可以通过点击+工具栏按钮并完成表格(如图 Figure 43–7 所示)来查看那些带有您的应用标签的条目。您在表单中输入的名称将被用作 DDMS 主窗口底部的另一个日志输出选项卡的名称。
  • 将日志信息保存到一个文本文件中,以便以后阅读或搜索。

images

图 43–7。??【DDMS】测井过滤器

文件推拉

虽然你可以使用adb pulladb push从模拟器或设备获取文件,但 DDMS 可以让你直观地做到这一点。只需突出显示您希望使用的仿真器或设备,然后从主菜单中选择Device images images File Explorer。这将打开典型的目录浏览器,如图 Figure 43–8 所示。

images

图 43–8。 DDMS 文件浏览器

只需浏览到所需的文件,然后单击工具栏上的“拉”(最左边)或“推”(中间)按钮,将文件传输到开发机器或从开发机器传输文件。要删除文件,请单击删除(最右边)工具栏按钮。

使用文件资源管理器有一些注意事项:

  • 您不能通过此工具创建目录。您将需要使用adb shell或者从您的应用中创建它们。
  • 虽然你可以在模拟器上浏览大部分文件,但由于 Android 的安全限制,你只能在实际设备上访问/sdcard之外的很少内容。
截图

要获取 Android 模拟器或设备的屏幕截图,请切换到 Eclipse 中的 DDMS 透视图,并按下屏幕截图工具栏按钮(显示为相机)。这将弹出一个包含当前屏幕图像的对话框,如 Figure 43–9 所示。

images

图 43–9。 DDMS 截屏

在这里,您可以单击“保存”将图像作为 PNG 文件保存在开发计算机上的某个位置,单击“刷新”根据模拟器或设备的当前状态更新图像,或者单击“完成”关闭对话框。

位置更新

要使用 DDMS 为您的应用提供位置更新,您必须做的第一件事就是让您的应用使用gpsLocationProvider,因为这是 DDMS 将要更新的。然后,单击“模拟器控件”选项卡,向下滚动到“位置控件”部分。在这里,您会发现一个更小的选项卡窗格,其中有三个选项用于指定位置:手动、GPX 和 KML,如图 Figure 43–10 所示。

images

图 43–10。 DDMS 位置控件

要使用 Manual 选项卡,请提供纬度和经度,然后单击 Send 按钮将该位置提交给模拟器。仿真器将依次通知任何位置监听器新的位置。

拨打电话和发送信息

如果你想在 Android 模拟器上模拟来电或短信,DDMS 也可以处理。在仿真器控制选项卡上,位置控制组上方是电话操作组,如图 Figure 43–11 所示。

images

图 43–11。??【DDMS】电话控制

要模拟来电,请填写电话号码,选择语音单选按钮,然后单击呼叫。此时,模拟器将显示来电,允许您接听或拒绝,如图 Figure 43–12 所示。

images

图 43–12。 模拟来电

要模拟收到的文本消息,请填写电话号码,选择 SMS 单选按钮,在提供的文本区域中输入消息,然后单击发送。文本消息将作为通知出现,如图 Figure 43–13 所示。

images

图 43–13。 模拟短信

当然,您可以点击通知,在成熟的消息应用中查看消息,如图 Figure 43–14 所示。

images

图 43–14。 模拟短信,在短信应用中

内存管理

DDMS 还可以帮助您诊断与应用如何使用内存(尤其是堆空间)相关的问题。

在 Sysinfo 选项卡上,您可以看到仿真器整体内存分配的饼图,如图 Figure 43–15 所示。

images

**图 43–15。**DDMS 内存使用图表

在 Allocation Tracker 选项卡上,您可以记录您的代码(或您在 Android 内部调用的代码)每次分配内存的时间。只需在树表中突出显示您的应用的进程,然后单击 Start Tracking 按钮。当您想查看自单击开始跟踪以来您已经分配了什么时,单击获取分配按钮,这将填写一个表格,显示每次分配、分配了多少内存以及内存在代码中的分配位置,如图 Figure 43–16 所示。

images

图 43–16。 DDMS 分配追踪器

此外,您甚至可以通过 Dump HPROF 选项为您的应用转储整个堆,这是一个工具栏按钮,看起来像一个半空的罐子,右边有一个红色的向下箭头。产生的 HPROF 文件可以与 MAT(Eclipse 的一个插件)一起使用,以查看哪些对象仍然在堆上,以及是谁导致它们停留在堆上。

在转储 HPROF 文件之前,您可能希望在您的进程上强制运行垃圾收集。您可以通过单击看起来像经典金属垃圾桶的工具栏按钮来实现。

亚行:像 DDMS,打字多

Android 调试桥或adb实用程序有两个作用:

  • 在幕后,它充当模拟器/设备和其他工具之间的桥梁。例如,ADT、层次结构查看器和 DDMS 都通过adb桥与仿真器通信。这个桥以守护进程的形式出现,在您上次重启后第一次尝试使用这些工具时产生。
  • 它为其他工具的许多功能提供了命令行等价物,特别是 DDMS。

您可以使用adb做的一些事情包括:

  • 启动(adb start-server)或停止(adb kill-server)上述守护进程
  • 列出目前可见的所有已识别的 Android 设备和模拟器(adb devices)
  • 在您的设备或模拟器中访问 Linux 外壳(adb shell)
  • 在您的设备或模拟器上安装或卸载 Android 应用(adb install)
  • 将文件复制到模拟器中(adb push)或从模拟器中(adb pull),很像 DDMS 的文件浏览器
  • 检查 LogCat ( adb logcat)

获取图形

与 Android 4.0 (ADT 版本 14 和 15)一起发布的最新版本的 Android 开发者工具引入了新的 Android Asset Studio。这个配套工具旨在让您快速轻松地创建图形素材,如启动器图标、操作栏和选项卡图标等,并处理创建一组连贯图像的许多繁琐方面,这些图像在每个可能的设备大小、分辨率等方面都很好看。

至少有两种方法可以访问和使用 Android Asset Studio。对于铁杆开发者来说,这些代码是开源的,可以在 Google Code 的[code.google.com/p/android-ui-utils/](http://code.google.com/p/android-ui-utils/)上获得。您可以下载代码,并构建您的解决方案,为您提供在您的环境中运行的 Android Asset Studio 实例。这种方法的缺点是 Android Asset Studio 仍然被标记为测试版,您很可能会发现自己遇到的问题仍然在开发中得到纠正。

**注意:**作为一个尚未纠正的问题的例子,作者可以通过试图在 Firefox 下的启动器图标上添加文本来不断地发送他的图形卡混乱。

另一种方法是从您最喜欢的浏览器中使用 Android Asset Studio 的托管版本。Android Asset Studio 的开发者建议你使用 Chrome 作为浏览器,但是你也可以使用其他浏览器。

Android Asset Studio 的托管版本目前在[android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html](http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html)。在您的浏览器中打开该页面,您会看到 Android Asset Studio 当前可用的选项,如图 Figure 43–17 所示。

images

图 43–17。 安卓素材工作室首页

例如,选择创建一个启动器图标会将你带到一个非常简单的调色板,在这里你可以看到 Android 大小的图标,如ldpimdpihdpixhdpi,当你向新图标添加图形和文本时,它们会变得栩栩如生。图 43–18 展示了一些立竿见影的效果(以及你的作者并非下一个毕加索的事实)。

images

图 43–18。 在安卓素材工作室创建启动器图标集

当你完成杰作的创作后,你可以下载这些资源并把它们放在项目的相关res/文件夹中。

四十四、替代环境的作用

你可能会认为 Android 全是 Java。官方的 Android 软件开发工具包(SDK)是用于 Java 开发的,构建工具是用于 Java 开发的,Android 讨论组和博客帖子都是关于 Java 的,是的,大多数 Android 书籍都是用于 Java 开发的。见鬼,这本书的大部分都是关于 Java 的。

然而(向威廉姆·高德曼道歉),Android 只是大部分是 Java。大部分 Java 和全 Java 有很大的区别。大多 Java 稍微不是 Java。

因此,虽然 Android 的“最佳点”在短期内仍将是基于 Java 的应用,但您仍然可以使用其他技术创建应用。本章和接下来的三章将介绍一些替代技术。

本章首先分析了 Android 以 Java 为中心的策略的利弊。然后,它列举了一些原因,为什么你可能想使用其他东西的 Android 应用。还讨论了替代 Android 应用环境的缺点——缺乏支持和技术挑战。

起初,有 Java...

Android 核心团队在选择 Java 的时候,做了一个相当合理的语言选择。它是一种非常流行的语言,在移动社区中,它的前身是 Java 2 Platform,Micro Edition (J2ME)。由于缺乏对内存地址(所谓的指针)的直接访问,基于 Java 的应用不太容易出现开发人员的错误,这些错误可能会导致缓冲区溢出,并使应用暴露在可能的攻击之下。围绕 Java 有一个相当健壮的生态系统,包括教育材料、现有的代码库、集成开发环境(ide)等等。

然而,虽然你可以用 Java 语言编写 Android 程序,但 Android 设备不能运行 Java 应用。相反,您的 Java 代码被转换成在 Dalvik 虚拟机上运行的东西。这类似于用于常规 Java 应用的技术,但 Dalvik 是专门为 Android 环境调整的。此外,它将 Android 对 Java 本身的依赖限制在少数编程工具上,这一点很重要,因为 Java 的管理权从 Sun 转移到 Oracle,再到 anywhere。

Dalvik 虚拟机还能够运行来自其他编程语言的代码,这一特性使得本书涵盖的大部分内容成为可能。

...这很好

没有一个移动开发环境是完美的,Java 和 Android 的结合也不例外。

一开始,Java 是为 Dalvik 虚拟机实现的,它是被解释的,没有任何常规 Java 用来提高性能的实时(JIT)编译器技巧。这在移动领域是一个更大的问题,因为运行 Android 的设备往往不如普通的台式机、笔记本电脑或网络服务器强大。Android 2.3 增加了一个 JIT 编译器,帮助很大,但是相比原生编译代码还是比较慢。因此,有些事情你不能在装有 Java 的 Android 上做,因为它太慢了。

Java 使用垃圾收集使人们不必跟踪所有的内存分配。这在很大程度上是可行的,通常有利于开发人员的生产力。然而,它不是解决所有内存和资源分配问题的灵丹妙药。即使这些泄漏的精确机制不同于 C、C++ 和其他语言中的经典泄漏,Java 中仍然可能存在所谓的“内存泄漏”。

然而,最重要的是,并不是每个人都喜欢 Java。这可能是因为他们缺乏这方面的经验,或者他们有过这方面的经验,但并不喜欢这种经验。当然,Java 通常被视为大型企业系统的语言,因此不一定“酷”其他语言的拥护者对 Java 也有自己的烦恼(例如,对于 Ruby 开发人员来说,Java 实在是太冗长了)。

因此,尽管 Java 对 Android 来说是个不错的选择,但它也不是完美的。

逆势而为

仅仅因为 Java 是为 Android 构建应用的主要方式,这并不意味着它是唯一的方式,对你来说,它甚至可能不是最好的方式。

也许 Java 不在你现有的技能范围内。您可能是一名 web 开发人员,更熟悉 HTML、CSS 和 JavaScript。有一些框架可以帮助你。或者,也许你已经开始接触服务器端脚本语言,比如 Perl 或 Python——也有办法将这些代码移植到 Android 上。或者也许你已经有了一堆 C/C++ 代码,比如游戏物理算法,用 Java 重写会很痛苦。您也应该能够重用这些代码。

即使你愿意学习 Java,你对 Java 和 Android APIs 的缺乏经验也可能会拖你的后腿。您也许能够用另一个框架更快地构建一些东西,即使将来您最终用一个基于 Java 的实现来替换它。快速开发和原型制作通常是很重要的,以便用最少的时间投入获得早期反馈。

当然,你可能会觉得 Java 编程很烦人。你不会是第一个,也不会是最后一个有这种想法的人。如果你把 Android 当成一种爱好,而不是你“日常工作”的一部分,享受乐趣将对你特别重要,你可能不会觉得 Java 很有趣。

幸运的是,与一些移动平台不同,Android 对构建应用的替代方式很友好。

支架,结构

但是,“友好”和“全力支持”是两回事。基于 Java 的开发的一些替代方案得到了核心 Android 团队的官方支持,例如通过原生开发工具包(NDK)的 C/C++ 开发和通过 HTML5 的 web 风格开发。一些公司支持基于 Java 的开发的替代方案。Adobe 支持 Adobe Integrated Runtime (AIR),最近还收购了以支持 PhoneGap 而闻名的 Nitobi(详见第四十六章),Rhomobile 支持 Rhodes,等等。标准机构支持其他替代方案。例如,万维网联盟(W3C)支持 HTML5。还有一些只是由几个开发商支持的小项目。

您需要自己决定这些支持级别中的哪一个能够满足您的要求。对于许多开发活动来说,支持不是大问题,但是在某些情况下,支持可能是最重要的(例如,企业应用开发)。

警告开发者

当然,在传统的 Java 环境之外进行 Android 开发也有它的问题,不仅仅是可以获得多少支持。

就处理器时间、内存或电池寿命而言,有些环境可能不如 Java 有效。C/C++ 总体来说可能比 Java 好,但是比如 HTML5 可能就差一些。根据你写的内容和它的使用量将决定这种低效率有多严重。

有些环境可能无法在所有设备上使用。现在,Flash 就是最好的例子;一些设备提供一定量的闪存支持,而另一些设备根本没有闪存支持。类似地,HTML5 支持仅从 Android 2.0 开始添加到 Android,因此运行旧版本 Android 的设备没有 HTML5 作为内置选项。

当新版本的 Android 出现时,你和官方支持的环境之间的每一层都使你更难确保与新版本的 Android 兼容。例如,如果您使用 PhoneGap 创建一个应用,并且一个新的 Android 版本变得可用,那么可能会出现不兼容的问题,只有 PhoneGap 团队能够解决。虽然他们可能会很快解决这些问题——并且他们可能会为您提供一些隔离这些不兼容性的措施——但响应时间是您无法控制的。在某些情况下,这不是问题,但在其他情况下,这可能对您的项目不利。

因此,仅仅因为你在 Java 之外开发并不意味着一切都是完美的。您只需在这些问题和基于 Java 的开发可能给您带来的问题之间进行权衡。平衡点在哪里取决于每个开发商或公司。

四十五、HTML5

在当前对移动应用的兴趣浪潮之前,当前的技术是 web 应用。很多注意力都放在了 AJAX、Ruby on Rails 和其他技巧和技术上,这些技巧和技术使得使用 web 应用的体验接近,有时甚至优于使用桌面应用的体验。

web 应用的爆炸最终推动了 web 标准的下一轮增强,统称为 HTML5。Android 2.0 是第一个支持这些 HTML5 增强功能的版本。值得注意的是,Android 支持离线应用和网络存储,这意味着 HTML5 成为创建 Android 应用的相关技术,而无需处理 Java。

离线应用

在 Android 或其他地方使用 HTML5 进行离线应用的关键是,当没有互联网连接时,可以在客户端(例如,在没有 Wi-Fi 的飞机上)或服务器端(例如,由于 web 服务器维护)使用这些应用。

这是什么意思?

从历史上看,web 应用有这种讨厌的趋势,需要 web 服务器。这导致了离线使用的各种变通方法,包括运送 web 服务器并将其部署到桌面。

HTML5 通过允许网页指定自己的缓存规则解决了这个问题。web 应用可以发布一个缓存清单,描述哪些资源

  • 可以安全地缓存,这样,如果 web 服务器不可用,浏览器可以使用缓存的副本。
  • 不能被安全地缓存,这样,如果 web 服务器不可用,浏览器就会像往常一样失败。
  • 拥有一个“后备”资源,这样,如果 web 服务器不可用,应该使用缓存的后备资源。

对于移动设备来说,这意味着一个完全支持 HTML5 的浏览器应该能够预先加载所有的资源并保持缓存。如果用户失去连接,应用仍将运行。在这方面,web 应用的行为几乎与常规应用相同。

你如何使用它?

这一章,我们将使用亚历克斯·吉普森创造的Checklist“迷你应用”。虽然可以在 MiniApps 网站([miniapps.co.uk/](http://miniapps.co.uk/))上找到该应用的最新版本,但本章将回顾在 Apress 网站([www.apress.com](http://www.apress.com))的源代码/下载区中找到的HTML5/Checklist副本。这份拷贝也在线托管在 CommonsWare 网站上,你可以很容易地通过简短的网址[bit.ly/cw-html5](http://bit.ly/cw-html5)直接找到它。

关于示例应用

Checklist顾名思义,是一个简单的清单应用。当您第一次启动它时,列表将是空的,如图 Figure 45–1 所示。

images

图 45–1。 清单 app,初始启动

您可以在顶部字段中输入一些文本,然后点击添加按钮将其添加到列表中,如图 Figure 45–2 所示。

images

图 45–2。 检查表,增加一项

您可以“勾选”个别项目,然后以删除线显示,如图 Figure 45–3 所示。

images

图 45–3。 检查表,其中一项标记为已完成

您也可以删除选中的条目(通过删除选中的按钮)或所有条目(通过删除全部按钮),在继续之前会弹出一个确认对话框,如 Figure 45–4 所示。

images

图 45–4。 清单删除确认对话框

在您的 Android 设备上“安装”清单

要在您的 Android 设备上访问Checklist,请访问位于[bit.ly/cw-html5](http://bit.ly/cw-html5)的托管版。然后你可以为它添加一个书签(在浏览器的选项菜单中选择更多images添加书签),以便以后返回。

如果你愿意,你甚至可以在主屏幕上设置书签的快捷方式——只需长按背景,选择书签,然后选择你之前设置的Checklist书签。

检查 HTML

Checklist应用中的所有功能都是通过几行 HTML 代码实现的:

`

Checklist ` `    Mail  

Checklist

                  Add         

Total: 0         Remaining: 0

  
        
  • Loading…
  •   
      Delete Checked   Delete All   `

然而,对于离线应用来说,关键是我们的html元素的manifest属性:

<html lang="en" manifest="checklist.manifest">

这里,我们指定了清单文件的相对路径,指明了离线缓存该应用各个部分的规则。

检查清单

因为清单是所有乐趣所在,所以让我们来看看Checklist的清单:

CACHE MANIFEST #version 54 styles.css main.js splashscreen.png

html 5 清单格式非常简单。它以一个CACHE MANIFEST行开始,后面是一个应该被缓存的文件列表(技术上来说,是相对 URL)。它还支持注释,注释是以#开头的行。

清单还可以有一个NETWORK:行,后面跟着不应该被缓存的相对 URL。类似地,清单可以有一个FALLBACK:行,后跟成对的相对 URL:试图从网络获取的 URL,后跟网络不可用时使用的缓存资源的 URL。

原则上,清单应该为应用运行所需的所有内容请求缓存,尽管请求缓存的页面(在本例中为index.html)也会被缓存。

网络存储

缓存 HTML5 应用的素材以供离线使用固然很好,但这本身会有相当大的局限性。在离线情况下,应用不能使用 AJAX 技术与 web 服务进行交互。因此,如果应用要能够存储信息,它需要在浏览器本身上这样做。

从 cookies 到 Google Gears 的一切都被用来解决这个问题,后一种工具为 HTML5 应用开辟了一条道路,现在被不同地称为 Web 存储或 DOM 存储。一个 HTML5 应用可以在客户端上持久存储数据,在客户端施加的限制内。这与离线素材缓存相结合,意味着 HTML5 应用在缺少互联网连接时,或者对于存储在“云中”没有意义的数据,可以提供更多的价值

**注意:**从技术上讲,Web 存储不是 HTML5 的一部分,而是一个相关的规范。然而,在日常对话中,它往往会与 HTML5 混为一谈。

这是什么意思?

在支持 Web 存储的浏览器上,您的 JavaScript 代码将可以访问一个代表应用数据的localStorage对象。更准确地说,每个(即域)在浏览器上将有一个不同的localStorage对象。

localStorage对象是一个关联数组,这意味着您可以通过数字索引或基于字符串的键来使用它。值通常是字符串。您可以使用localStorage执行以下操作:

  • 通过length()找出数组中有多少条目
  • 通过getItem()setItem()按键获取和设置项目
  • 通过key()获取数字索引的密钥
  • 通过removeItem()删除个别条目或通过clear()删除所有项目

这意味着您不具备 SQL 数据库的全部丰富性,就像您在原生 Android 应用中使用 SQLite 一样。但是,对于许多应用来说,这已经足够了。

你如何使用它?

Checklist将列表项作为键存储在关联数组中,常规项的值为0,删除项的值为1。在这里,我们看到了将一个新项目放入清单的代码:

try {   localStorage.**setItem**(strippedString, data); } catch (e) {   if (e == QUOTA_EXCEEDED_ERR) {     **alert**('Quota exceeded!');   } }

下面的代码将这些项目从存储中取出,放入一个数组中进行排序,然后在网页上显示为 DOM 元素:

`/get all items from localStorage and push them one by one into an array./ for (i = 0; i <= listlength; i++) {

  var item = localStorage.key(i);   myArray.push(item); }

/sort the array into alphabetical order./ myArray.sort();`

当用户选中某个项目旁边的复选框时,存储会更新,以持续切换选中的设置:

`/toggle the check flag./ if (target.previousSibling.checked) {   data = 0; } else {   data = 1; } /save item in localStorage./ try {   localStorage.setItem(name, data); } catch (e) {

  if (e == QUOTA_EXCEEDED_ERR) {     alert('Quota exceeded!');   } }`

Checklist也有从存储器中删除项目的代码,可以是所有被标记为选中的项目,也可以是所有项目。以下是删除所有选中项目的代码:

`/remove every item from localStorage that has the data flag checked./ while (i <= localStorage.length-1) {

  var key = localStorage.key(i);   if (localStorage.getItem(key) === '1') {     localStorage.removeItem(key);   }   else { i++; } }`

下面是删除所有项目的代码:

`/deletes all items in the list./ deleteAll: function() {

  /ask for user confirmation./   var answer = confirm("Delete all items?");

  /if yes./   if (answer) {

    /remove all items from localStorage./     localStorage.clear();     /update view./     checklistApp.getAllItems();  }  /clear up./  delete checklistApp.deleteAll; },`

Web SQL 数据库

Android 的内置浏览器还支持 Web SQL 数据库选项,这使您能够从 JavaScript 使用 SQLite 风格的数据库。这比基本的 Web 存储提供了更多的功能,尽管代价很复杂。它也不是现行标准的一部分——致力于该标准的网络超文本应用技术工作组(WHATWG)已经暂时将其搁置一旁。

您可以考虑评估 Lawnchair,它是一个 JavaScript API,允许您存储任意的 JavaScript 对象符号(JSON)编码的对象。它将使用任何可用的存储选项,因此将帮助您处理跨平台的多样性。

即将投入生产

创建一个小的测试应用并不需要什么神奇的东西。不过,假设您对让其他人使用您的应用感兴趣——可能是许多其他人。经典的基于 Java 的 Android 应用必须处理测试,对应用进行数字签名以供生产,通过各种渠道(如 Android Market)分发应用,并通过各种方式为应用提供更新。这些问题不会神奇地消失,因为 HTML5 被用作应用环境。然而,HTML5 确实大大改变了 Java 开发人员必须做的事情。

测试

由于 HTML5 可以在其他浏览器中工作,测试您的业务逻辑可以很容易地利用任何数量的 HTML 和 JavaScript 测试工具,从 Selenium 到 QUnit 再到 Jasmine。

为了在 Android 上进行测试——以确保没有与 Android 浏览器实现相关的问题——您可以使用 Selenium 的 Android 驱动程序或远程控制模式。

签署和分发

与原生 Android 应用不同,您无需担心 HTML5 应用的签名问题。这样做的缺点是不支持通过 Android Market 分发 HTML5 应用,Android Market 目前只支持原生 Android 应用。用户将不得不通过某种方式找到你的应用,在浏览器中访问它,给页面添加书签,可能还会创建一个主屏幕快捷方式。

更新

与默认情况下必须手动更新的原生 Android 应用不同,HTML5 应用将在用户下次在连接到互联网的情况下运行该应用时透明地更新。离线缓存协议将在回退到缓存的副本之前检查 web 服务器上文件的新版本。因此,除了发布最新的 web 应用资源,您没有什么可做的了。

您可能会遇到的问题

不幸的是,没有什么是完美的。虽然 HTML5 可能会使许多事情变得更容易,但它不是解决所有 Android 开发问题的灵丹妙药。

这一节涵盖了一些潜在的关注领域,当你使用 Android 的 HTML5 应用时,你将会考虑到这些领域。

安卓设备版本

并非所有 Android 设备都支持 html 5——只有运行 Android 2.x 或更高版本的设备才支持。因此,理想情况下,你应该在你的网络服务器上做一点用户代理嗅探,把老 Android 用户重定向到其他解释他们设备局限性的页面。

以下是运行 Android 2.1 的 Google/HTC Nexus One 设备的用户代理字符串:

Mozilla/5.0 (Linux; U; Android 2.1-update1; en-us; Nexus One Build/ERE27)![images](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7188b11f753d40e194fc35fb2b820902~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1773066454&x-signature=94C4ExObF67KrMF0UuxaQKg9YA8%3D)  AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17

如您所见,它的格式类似于典型的现代用户代理字符串,这意味着它相当混乱。它确实表明它正在运行Android 2.1-update1

最终,有人会为不同的设备型号创建一个用户代理字符串数据库,从那里我们可以导出适当的正则表达式或类似的算法来确定给定的设备是否可以支持 HTML5 应用。

屏幕尺寸和密度

HTML5 应用可以在各种屏幕尺寸上运行,从 QVGA Android 设备到 1080p 液晶显示器等等。同样,屏幕密度可能会有很大差异,因此尽管智能手机上的 48×48 像素图像可能是合适的尺寸,但对于 1080p 电视来说可能太大了,更不用说 24 英寸的液晶桌面显示器了。

除了增加低端屏幕尺寸的可能选项,这些都不是 Android 独有的。你需要决定如何最好地设计你的 HTML 和 CSS,以适应不同的大小和密度,即使 Android 不在其中。

有限的平台集成

HTML5 虽然提供了比以往更多的平台集成,但并没有涵盖 Android 应用可能希望能够做到的一切。例如,普通的 HTML5 应用不能执行以下操作:

  • 启动另一个应用
  • 使用联系人数据库
  • 用股票浏览器发出通知(注意,Android 版 Firefox 有一个解决方案)
  • 一定要真正在后台工作(尽管网络工作者有一天可能会减轻这一点)
  • 与蓝牙设备互动
  • 录制音频或视频
  • 使用标准的 Android 偏好系统
  • 使用语音识别或文本到语音转换

当然,许多应用不需要这些功能。其他应用环境,如 PhoneGap(在第四十六章中有所介绍),将很可能演变成 Android 的“HTML5 Plus”。这样,您可以创建一个适用于所有设备的普通应用和一个独立的增强 Android 应用,该应用利用了更好的平台集成,但代价是额外的编程量。

性能和电池

一段时间以来,人们一直担心基于 HTML 的用户界面与原生 Android 用户界面相比,在处理器时间、内存和电池方面效率低下。例如,避免在 Android 主屏幕上使用 BONDI 风格的 web 小部件的原因之一就是性能影响。

当然,设计消耗电池的 HTML5 应用是可能的。例如,如果每秒钟都有一大块 JavaScript 代码无限期地运行,这将消耗相当多的处理器时间。然而,除此之外,一个普通的应用似乎不太可能被如此频繁地使用,以至于严重影响电池寿命。当然,在这方面还需要做更多的测试。

此外,HTML5 应用的启动速度可能会比其他应用慢一点,特别是如果浏览器有一段时间没有使用,或者网络连接存在,但服务器的带宽很小。

外观和感觉

HTML5 应用当然可以看起来非常光滑和专业——毕竟,它们是用 web 技术构建的,web 应用可以看起来非常光滑和专业。

然而,HTML5 应用不一定看起来像标准的 Android 应用,至少最初不是。毫无疑问,一些有事业心的开发人员会创建一些可重用的 CSS、JavaScript 和图像,例如,镜像一个 Android 原生Spinner小部件(一种下拉控件)。类似地,HTML5 应用往往缺少选项菜单、通知或其他原生 Android 应用可能会使用的 UI 功能。

这不一定是坏事。考虑到创建一个看起来非常光滑的 Android 应用的难度,HTML5 应用可能会比 Android 应用看起来更好。毕竟,擅长创建漂亮的网络应用的人比擅长创建漂亮的安卓应用的人多得多。

然而,一些用户可能会抱怨外观和感觉的差距,只是因为它是不同的。

分布

HTML5 应用可以很容易地添加到用户的设备上——浏览、添加书签和添加主屏幕的快捷方式。然而,HTML5 应用不会出现在 Android 市场中,因此受过培训来查看可用应用市场的用户不会找到 HTML5 应用,即使这些应用可能比他们的原生应用更好。

可以想象,有一天,Android 市场将支持 HTML5 应用。还可以想象,有一天,Android 用户将倾向于通过搜索 Android Market 之外的其他方式找到他们的应用,并能够通过这种方式获得他们的 HTML5 应用。然而,在其中一个成为现实之前,HTML5 应用可能不像它们的本地对等物那样“容易被发现”。

浏览器更改贴吧冰淇淋三明治

谷歌已经宣布,他们的 Android 浏览器的未来方向将是 Chrome(或 Android 的 Chrome 衍生版本)。对于 Android 4.0 版本,没有迹象表明这种变化是通过一个渐进的过程慢慢收敛到一个类似 Chrome 的浏览器上,还是通过未来版本中的一个大变革。当这种变化真的发生时,使用 HTML5 的一些细微差别,甚至一些支持的特性,都可能会改变。这将影响所有使用 HTML5 应用的用户,不包括使用其他浏览器的用户。

HTML5 和其他安卓浏览器

虽然内置的 Android 浏览器将是许多 Android 用户的选择,但也有其他浏览器可用。以下是一些更著名的替代方案在 HTML5 支持方面的情况:

  • 火狐移动版:经过一段时间后仍处于测试阶段,支持离线缓存和本地存储。但是,此时无法正确运行Checklist样本。
  • Opera Mobile :稳步改进的产品,最近增加了触觉反馈支持等功能。不支持本地存储,渲染Checklist没有意义。它也不支持离线缓存。
  • 海豚浏览器 HD 4.0 :支持离线缓存和本地存储。虽然在Checklist中有轻微的渲染问题——可能与 CSS 有关——但是应用在其他方面运行良好,即使没有互联网连接。

HTML5:基线

对于传统的应用开发来说,HTML5 可能会变得相当流行。它为 web 开发者提供了一条通向桌面的道路。它可能是谷歌 Chrome OS 的唯一选择。而且,随着对流行移动设备(包括 Android)的支持不断改善,开发者肯定会被另一轮“一次编写,随处运行”的承诺所吸引。

随着时间的推移,HTML5 很有可能成为 Android 应用开发的第二选择,仅次于编写到 Android SDK 的传统 Java 应用。这将使 HTML5 成为比较可选 Android 开发选项的基准——这些选项不仅会与使用 SDK 进行比较,还会与使用 HTML5 进行比较。

四十六、PhoneGap

PhoneGap 可能是 Android 最初的替代应用框架,于 2009 年初出现。PhoneGap ( [www.phonegap.com/](http://www.phonegap.com/))是开源的,由 Nitobi 支持,Nitobi 传统上提供开源和商业产品的混合,以及咨询和培训服务。截至 2011 年 10 月,Nitobi 已同意被 Adobe 收购。为了确保 PhoneGap 代码的持久性,Nitobi 将 PhoneGap 代码库提交给了 Apache 软件基金会,该项目现在被命名为 Apache Callback。大多数人仍然知道这个项目的原始名称,许多文档仍然提到 PhoneGap,所以我们将坚持使用传统的名称。

什么是 PhoneGap?

PhoneGap 是一个围绕 HTML5 构建的平台,使您能够从一个代码库开发应用,并将它们部署到多个平台。使用 PhoneGap 只需遵循以下步骤:

  1. 使用 HTML5 和 JavaScript 等 web 标准语言构建您的应用。
  2. 用 PhoneGap 包装您的应用,以获得对本机 API 的访问。
  3. 将您的应用部署到多个平台。

[www.phonegap.com/about](http://www.phonegap.com/about)阅读更多关于 PhoneGap 及其工作原理的信息。

你在写什么?

PhoneGap 应用由 HTML、CSS 和 JavaScript 组成,与移动网站或 HTML5 应用没有什么不同,只是在 PhoneGap 中,web 素材与应用打包在一起,而不是即时下载。

因此,预装的 PhoneGap 应用可能包含相对较大的素材,如复杂的 JavaScript 库,可能太慢而无法通过较慢的 EDGE 连接下载。然而,PhoneGap 仍然受到移动设备速度以及 WebKit 浏览器加载和处理这些素材的速度的限制。

此外,WebKit for mobile 的开发不同于 WebKit for desktops 的开发,特别是在触摸和鼠标事件方面。在可行的情况下,您可能希望使用 JavaScript 框架的移动层进行开发(例如,jQTouch 与普通 jQuery)。

你有什么特点?

与 HTML5 应用一样,PhoneGap 为您提供了 web 浏览器的基本功能,包括 AJAX 支持。除此之外,PhoneGap 还增加了许多 JavaScript APIs,让你可以了解 Android 平台的底层特性。在撰写本文时,包括以下内容:

  • 加速度计接入,用于检测设备的移动
  • 录音
  • 相机接入,用于拍摄静态照片
  • 指南针访问,用于定向活动
  • 联系人访问,用于使用内置联系人提供程序
  • 数据库访问,既可以访问您创建的数据库(SQLite),也可以访问 Android 内置的其他数据库(例如,联系人)
  • 文件系统访问,例如对 SD 卡或其他外部存储器的访问
  • 地理定位,用于确定设备的位置
  • 通知服务,包括警报和声音效果
  • 存储,设备上和 SD 卡
  • 振动,用于摇动电话(例如,力反馈)

因为其中一些是 HTML5 规范的一部分(例如,地理定位),所以您可以选择 API。此外,这个列表会随着时间的推移而变化,但你会看到前面的列表是大多数当代 Android 设备上的本机功能的一个非常全面的列表。

app 是什么样子的?

PhoneGap 应用看起来像网页,比原生 Android 应用更像,如图图 46–1 所示,这是 PhoneGap 附带的示例应用的截图。您可以使用 CSS 和图像在某种程度上模仿 Android 的外观和感觉,但仅限于那些可以在 Android 和 HTML 中创建的小部件。例如,Android Spinner小部件,类似于一个下拉列表,可能很难在 HTML 中模仿。

images

**图 46–1。**PhoneGap 附带的示例应用

分销是如何运作的?

分发 PhoneGap 应用与分发任何其他标准 Android 应用非常相似,只是有一些额外的选项。使用独立的 PhoneGap,在测试之后,您使用 Android 构建工具从 PhoneGap 为您生成的 Android 项目中创建一个标准的 APK 文件。这个项目将包含 Java、XML 和其他必要的部分,来包装 HTML、CSS 和 JavaScript,以构成您的应用。然后,您对应用进行数字签名,并将其上传到 Android Market 或您希望使用的任何其他分发机制。Nitobi 还提供了一个托管构建服务,叫做 PhoneGap Build,我们将在本章稍后讨论。

其他平台呢?

PhoneGap 不只是针对 Android。你可以为 iPhone,Blackberry,Symbian,微软 Windows Phone,Samsung Bada 和 Palm 的 WebOS 创建 PhoneGap 应用。至少在理论上,您可以使用 HTML、CSS、JavaScript 和 PhoneGap JavaScript APIs 创建一个应用,并让它在许多设备上运行。

有几个限制会阻碍你实现这个目标:

  • PhoneGap 在所有这些平台上使用的网络浏览组件并不相同。根据 WebKit 集成到给定设备的固件中时可用的版本,即使使用 WebKit 的多个平台也会有不同的 WebKit 版本。因此,您需要进行测试,以确保您的 CSS 能够在尽可能多的设备上正常运行。
  • 由于各种因素,并非所有 PhoneGap JavaScript APIs 都可以在所有设备上使用(例如,没有在平台的本机 API 中公开,缺乏将功能提升到 PhoneGap APIs 中的工程时间,等等)。).PhoneGap wiki 可以让你知道在不同的设备上什么有效,什么无效。您将希望限制您的功能使用以匹配您想要的平台,或者限制您的平台以匹配您想要的功能。

使用 PhoneGap

现在,让我们来看看使用 PhoneGap 的更多技巧。在撰写本文时,PhoneGap 的安装和使用通常需要一名基于 Java 的 Android 开发专家。您需要安装一整套工具,手动编辑配置文件,等等。如果您想做到所有这些,PhoneGap 网站上提供了文档,我们将在下面简单介绍一下。如果你正在读这一章,你很有可能会跳过所有这些。同样,您可以使用 PhoneGap 构建服务([build.phonegap.com/](http://build.phonegap.com/)),我们将很快介绍它。

安装

您可以从 PhoneGap 网站下载最新的 PhoneGap 工具,作为一个 ZIP 存档,它可以重定向到 Apache Software Foundation 的 Apache Callback incubator 存储库。只要对您的开发机器和平台有意义,就将这些工具解包。对于 Android 开发,这就是你需要的所有 PhoneGap 特有的安装。但是,您将需要 Android SDK 和相关工具(例如,如果您希望使用 Eclipse,可以使用 Eclipse)来设置项目。

创建并安装您的项目

PhoneGap Android 项目本质上是一个常规的 Android 项目,您可以按照本书前面概述的说明来创建它。要将标准生成的“Hello,World”应用转换为 PhoneGap 项目,您需要执行以下操作:

  1. 从解压 PhoneGap ZIP 文件的位置的Android/目录,将 PhoneGap JAR 文件复制到项目的libs/目录。如果您正在使用 Eclipse,您还需要将它添加到您的构建路径中。
  2. 在您的项目中创建一个assets/www/目录。然后,从您解压 PhoneGap ZIP 文件的位置的Android/目录中复制 PhoneGap JS 文件。
  3. 将标准的“Hello,World”活动调整为继承自DroidGap而不是Activity。这就需要你导入com.phonegap.DroidGap
  4. 在活动的onCreate()方法中,用super.loadUrl("file:///android_asset/www/index.html");替换setContentView()
  5. 在您的清单中,添加 PhoneGap 请求的所有权限,这些权限将在本章后面列出。
  6. 同样在您的清单中,根据您想要测试和支持的屏幕尺寸添加一个合适的<supports-screens>元素。
  7. 同样在您的清单中,将android:configChanges="orientation|keyboardHidden"添加到您的<activity>元素中,因为DroidGap处理与方向相关的配置更改。

此时,您可以在项目中创建一个assets/www/index.html文件,并开始使用 HTML、CSS 和 JavaScript 创建 PhoneGap 应用。您需要包含对 PhoneGap JavaScript 文件的引用(例如,<script type="text/javascript" charset="utf-8" src="phonegap.1.2.0.js" />)。当您想要测试应用时,您可以像任何其他 Android 应用一样构建和安装它(例如,如果您使用命令行构建过程,可以使用ant clean install)。

对于有 Android SDK 开发经验的人来说,设置这个并不是一个很大的挑战。

PhoneGap 构建

PhoneGap Build 是一种工具即服务(TaaS)托管的创建 PhoneGap 项目的方法。所有 Android 构建过程都由 PhoneGap 提供的服务器为您处理。您只需专注于创建您认为合适的 HTML、CSS 和 JavaScript。

当您登录 PhoneGap Build 时,首先会提示您创建您的初始项目,提供名称和 web 素材以进入应用,如 Figure 46–2 所示。

images

图 46–2。 在 PhoneGap Build 中创建您的第一个项目

你以后可以通过一个新的应用按钮添加新的项目,这给了你同样的选项。

您提供素材的选择是上传一个包含所有素材的 ZIP 文件,或者指定一个公共 GitHub 存储库的 URL,PhoneGap Build 可以从这个存储库中提取素材。如果您习惯于使用 Git 进行版本控制,并且您的项目是开源的(因此有一个公共存储库),后一种方法会更方便。

点击上传按钮后,PhoneGap Build server 立即开始为 Android、Blackberry、Symbian 和其他支持的平台或您选择的平台构建您的应用,如图 Figure 46–3 所示。

images

图 46–3。 在 PhoneGap Build 中构建您的第一个项目

每个目标都有自己的文件扩展名(例如,Android 的apk)。单击该链接将允许您下载该文件。或者,点击项目名称,您将获得快速响应(QR)代码,以便直接下载到您的测试设备,如图 Figure 46–4 所示。

images

图 46–4。 你项目的二维码在 PhoneGap 建立

该页面还提供了从 GitHub 资源库更新应用的链接(如果您选择了该选项)。或者,您可以点击编辑来指定更多选项,如您的应用版本或其启动器图标,如图 Figure 46–5 所示。

images

图 46–5。 您的项目在 PhoneGap Build 中的设置

总而言之,如果您的开发机器上不需要 Android SDK 和相关工具,PhoneGap Build 无疑简化了 PhoneGap 构建过程。

Nitobi 推出了 PhoneGap Build,作为面向非开源应用的商业服务,价格可从 PhoneGap Build 网站获得。

PhoneGap 和清单示例

PhoneGap 的美妙之处在于它包装了 HTML、CSS 和 JavaScript。换句话说,你不需要做太多 PhoneGap 特有的事情,就可以利用 PhoneGap 为你提供一个适合安装在 Android 设备上的 APK。也就是说,PhoneGap 确实向您展示了比您从标准中获得的更多的东西,如果您需要它们并且愿意为它们使用专有的 PhoneGap APIs 的话。

坚持标准

给定一个现有的 HTML5 应用,要使它成为一个可安装的 APK,您需要做的就是将它包装在 PhoneGap 中。例如,要将 HTML5 版本的Checklist(来自第四十五章)转换成一个 APK 文件,你需要做以下事情:

  1. 按照本章前面概述的步骤创建一个空的 PhoneGap 项目。
  2. 将 HTML5 项目中的 HTML、CSS、JavaScript 和图像复制到 PhoneGap 项目的assets/www/目录中(注意,您不需要 HTML5 特有的任何东西,比如缓存清单)。
  3. 确保您的 HTML 入口点文件名与您在活动中使用的loadUrl()调用的路径相匹配(例如,index.html)。
  4. 从 HTML 中添加对 PhoneGap JavaScript 文件的引用。
  5. 构建并安装项目。

这是我们应用的DroidGap活动,来自PhoneGap/Checklist项目:

`package com.commonsware.pg.checklist;

import android.app.Activity; import android.os.Bundle; import com.phonegap.DroidGap;

public class Checklist extends DroidGap {   @Override   public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     super.loadUrl("file:///android_asset/www/index.html");   } }`

下面是清单,添加了 PhoneGap 要求的所有设置:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"       package="com.commonsware.pg.checklist"       android:versionCode="1"       android:versionName="1.0">     <application android:label="@string/app_name" android:icon="@drawable/cw">         <activity android:name="Checklist"                   android:configChanges="orientation|keyboardHidden"                   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:resizeable="true"         android:anyDensity="true"     />     <uses-permission android:name="android.permission.CAMERA" />     <uses-permission android:name="android.permission.VIBRATE" />     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />     <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />     <uses-permission android:name="android.permission.READ_PHONE_STATE" />     <uses-permission android:name="android.permission.INTERNET" />     <uses-permission android:name="android.permission.RECEIVE_SMS" />     <uses-permission android:name="android.permission.RECORD_AUDIO" />     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />     <uses-permission android:name="android.permission.READ_CONTACTS" />     <uses-permission android:name="android.permission.WRITE_CONTACTS" />     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> </manifest>

这里是 HTML,它几乎与 HTML5 的原始版本相同,但删除了一些 HTML5 离线内容(例如,iPhone 图标),并添加了对 PhoneGap 的 JavaScript 文件的引用:

`

         Checklist                                        Mail           

Checklist

                                                          Add                                       

Total: 0             Remaining: 0

          
                
  • Loading…
  •           
                            Delete Checked           Delete All                    `

对于许多应用,这就是你所需要的。你只是简单地看着 PhoneGap 给你一些你可以在 Android Market、iOS 应用商店等等上分发的东西。

添加 PhoneGap APIs

如果您想利用更多的设备功能,您可以扩充您的 HTML5 应用来使用 PhoneGap 特定的 API。这些功能涵盖了从告诉你设备的型号到让你获得指南针读数的所有范围。因此,它们的复杂性会有所不同。为了本章的目的,我们将看一些更简单的。

设置设备就绪事件处理程序

由于各种原因,当您的页面被加载时,PhoneGap 不会立即响应它的所有 API。相反,您需要寻找一个deviceready事件来确认使用 PhoneGap 特定的 JavaScript 全局变量是安全的。以下是典型的食谱:

  1. 向您的<body>标签添加一个onload属性,引用一个全局 JavaScript 函数(例如,onLoad())。
  2. onLoad()中,使用addEventListener()deviceready事件注册另一个全局 JavaScript 函数(如onDeviceReady())。
  3. onDeviceReady()中,开始使用 PhoneGap APIs。
使用 PhoneGap 提供的功能

PhoneGap 通过一系列虚拟 JavaScript 对象为您提供了许多方法。这里,“虚拟”意味着您不能检查对象是否存在,但是您可以调用方法并读取它们的属性。例如,有一个device对象有一些有用的属性,比如phonegap返回 PhoneGap 版本,而version返回 OS 版本。这些虚拟对象准备好在deviceready事件中或之后使用。

例如,下面是一个 JavaScript 文件(PhoneGap/ChecklistEx项目中的props.js),它实现了一个onLoad()函数(为deviceready注册)和一个onDeviceReady()函数(使用device对象的属性):

`// PhoneGap's APIs are not immediately ready, so set up an // event handler to find out when they are ready

function onLoad() {   document.addEventListener("deviceready", onDeviceReady, false); }

// Now PhoneGap's APIs are ready

function onDeviceReady() {   var element=document.getElementById('props');

  element.innerHTML='

  • Model: '+device.name+'
  • ' +                     '
  • OS and Version: '+device.platform +' '+device.version+'
  • ' +                     '
  • PhoneGap Version: '+device.phonegap+'
  • '; }`

    onDeviceReady()函数需要一个idprops的列表元素。这再加上首先加载这个 JavaScript,将需要对我们的 HTML 进行一些小的修改:

    `

             Checklist                               Mail  

    Checklist

                      Add         

    Total: 0         Remaining: 0

      
          
    • Loading…
    •   
          Delete Checked   Delete All                

    Device Properties

            
                       `

      Figure 46–6 显示了最终的应用。

      images

      图 46–6。 带有设备属性的 PhoneGap 清单应用

      显然,读取一些属性远比用设备的摄像头拍照简单。然而,复杂性的差异主要在于 PhoneGap 的虚拟 JavaScript 对象给了你什么以及你如何使用它们,这比 Android 特有的任何东西都要多。

      你可能遇到的问题

      PhoneGap 是创建跨平台应用的绝佳选择。然而,它也不是没有问题。其中一些问题可能会及时得到解决;有些可能是 PhoneGap 特有的性质。

      安全

      Android 应用使用权限系统来请求访问某些系统功能,例如提出互联网请求或读取用户的联系人。应用必须在安装时请求这些权限,因此如果请求的权限可疑,用户可以选择放弃安装。

      一般的经验法则是,您应该请求尽可能少的权限,并确保您可以证明您为什么请求它们。

      PhoneGap,对于一个新项目,请求相当多的权限:

      • CAMERA
      • VIBRATE
      • ACCESS_COARSE_LOCATION
      • ACCESS_FINE_LOCATION
      • ACCESS_LOCATION_EXTRA_COMMANDS
      • READ_PHONE_STATE
      • INTERNET
      • RECEIVE_SMS
      • RECORD_AUDIO
      • MODIFY_AUDIO_SETTINGS
      • READ_CONTACTS
      • WRITE_CONTACTS
      • WRITE_EXTERNAL_STORAGE
      • ACCESS_NETWORK_STATE

      保持这个花名册不变将使您的应用可以使用 PhoneGap 为您的 JavaScript 提供的所有 API...和一个会吓跑很多用户的应用。毕竟,您的应用不太可能能够使用所有这些权限,更不用说证明这些权限的合理性了。

      您当然可以通过修改 PhoneGap 项目根目录下的AndroidManifest.xml文件来缩减这个列表。然而,您将需要彻底测试您的应用,以确保您没有放弃您实际需要的权限。此外,您可能不清楚可以安全删除哪些权限。

      最终,PhoneGap 项目可能会有工具来帮助指导您选择权限,也许是通过静态分析您的 JavaScript 代码来查看您正在使用哪些 PhoneGap APIs。不过,与此同时,获得适当的权限集将涉及大量的尝试和错误。

      屏幕尺寸和密度

      普通的 web 应用主要关注屏幕分辨率和窗口大小作为它们的主要变量。移动网络应用不必担心窗口大小,因为浏览器和应用通常全屏运行。但是,移动 web 应用需要处理物理大小和密度——这些问题与传统 web 开发无关。

      上网本的屏幕可以是 10 英寸或更小,而台式机的屏幕可以是 24 英寸或更大。因此,物理屏幕尺寸似乎是 web 开发人员需要解决的问题。然而,在上网本/笔记本/台式机领域,屏幕分辨率(以像素为单位)通常与物理尺寸相符。这是因为 LCD 的屏幕密度相当一致,而且密度相当低。

      另一方面,智能手机有几种不同的密度,导致分辨率和尺寸之间的联系被打破。一些低端手机,尤其是小尺寸(例如 3 英寸)的 LCD,其密度与漂亮的显示器相当。中档手机的密度是普通手机的两倍(240 dpi 对 120 dpi)。苹果的 iPhone 4 密度更高,很可能很快也会有一些配备所谓视网膜显示屏的 Android 设备。因此,例如,800×480 的分辨率可以在 4 英寸到 7 英寸的屏幕上显示。平板电脑增加了更多可能的尺寸。

      触摸屏带来的问题更是雪上加霜。鼠标点击可以获得像素级的精度。手指就没那么精确了。因此,你可能需要在触摸屏上把你的按钮和类似的东西做得大一点,这样才方便手指操作。这导致了素材缩放的一些问题,尤其是图像。在低密度 3 英寸设备上可能对手指友好的东西对于高密度 4 英寸设备来说可能完全太小了。

      原生 Android 应用具有用于处理该问题的内置逻辑,其形式为多组资源(例如,图像),可以基于设备特征进行交换。最终,PhoneGap 和类似的工具将需要为他们的用户提供相关的建议,告诉他们如何创建同样能适应环境的应用。

      观感

      一个 web 应用看起来永远不会像一个本地应用。这未必是一件坏事。然而,一些用户可能会感到不安,特别是因为他们不明白为什么他们新安装的应用(例如,用 PhoneGap 制作的)与他们已经拥有的任何其他类似应用如此不同。

      随着 HTML5 应用在 Android 上变得更加突出,这个问题的重要性应该会下降。然而,这是需要记住的事情。如果您正在创建自己的图形元素(图标等)。)你几乎肯定会从使用 Android Asset Studio 中受益,这将在第四十三章中介绍,承担一些杂务:制作必要大小的图标,创建与你的图标和其他风格一致的网络图像,等等。

      了解更多信息

      在撰写本文时,还没有专门介绍 PhoneGap 开发的书籍。目前,关于 PhoneGap 的最佳信息可以在 PhoneGap 网站上找到,包括它的 API 文档,也可以在其他网站上的许多其他公开的教程中找到。