服务

服务是一种可以在后台执行长期运行操作而不提供用户界面的应用程序组件。另外的某一个应用程序组件可以启动一个服务,之后该服务会一直运行,即使用户切换到了另外一个程序。此外,一个组件可以与服务绑定,与之交互,甚至进行进程间通信(IPC,interprocess communication)。例如,一个服务可以在后台完成处理网络事务、播放音乐、执行文件I/O操作,或是与一个内容提供者(content provider)交互等工作。

服务通常有两种存在形式:

启动的(started)

一个服务在某个应用程序组件(例如活动)通过调用startService()启动它之后成为“启动的”。一旦被启动,服务可以永远运行于后台,即使启动它的组件本身已被销毁。通常,一个启动的服务将执行单一的操作且不返回其调用者结果。例如,它可以通过网络下载或上传一个文件。当操作完成后,服务应当能结束自己。

绑定的(bound)

一个服务在某个应用程序组件通过调用bindService()绑定它之后成为“绑定的”。一个绑定的服务将提供一个客户端-服务器接口,允许组件与之交互,发送请求,接收结果,甚至通过IPC执行跨进程工作。一个绑定的服务将在另一个应用程序组件与之绑定时运行。多个组件可以同时绑定单个服务,不过当所有的组件都解除绑定时,该服务将会销毁。

尽管本文档原则上将分开讨论这两种类型的服务,一个服务可以同时以这两种形式存在——它可以被启动(而永久运行下去)同时也允许被绑定。这一切只不过取决于是否使用了回馈方法onStartCommand()允许组件启动服务并且还使用了onBind()允许绑定。

无论程序是启动的、绑定的,或者两者都是,任何应用程序都可以像使用一个活动那样使用一个服务——即通过Intent来启用。不过,服务可以在manifest文件中被声明为私有的,以阻断其他程序对其的读取。这点将在“在manifest中声明服务”一节中更加详细地进行讨论。

注意:一个服务运行于其宿主进程的主线程中——服务不会创建其自有的线程,也不会运行于另外的进程(除非你特别指定)。这意味着,如果一个服务要执行CPU高占用工作或是阻断性的操作(如MP3播放或是网络任务),就应该新建一个线程令服务在其中工作。使用另外的线程将减低程序不响应(ANR,Application Not Responding)的风险,使得程序的主线程中的活动对用户交互保持流畅。

基本内容

应该使用服务还是线程?

服务只是一个在用户没有与程序进行交互时也能于后台运行的组件而已。因此,只应当在必要时才创建一个服务。

如果仅仅需要在用户与程序进行交互时于主线程之外执行工作,应当创建一个新的线程而不是一个服务。例如,如果希望在程序正在运行时播放音乐,可以在onCreate()中创建一个线程,在onStart()中运行它,之后在onStop()中停止它。还可以考虑使用AsyncTask或是HandlerThread来取代传统的Thread类。关于线程的更多信息请参见“进程和线程”文档。

需要记得的是,如果使用了一个服务,它默认是运行于程序的主线程。因此应当在需要执行密集或是阻塞性的操作时为服务另外创建一个线程。

要创建一个服务,就必须创建一个Service(或它的一个现有子类)的子类。在实现时,需要正确覆盖一些控制服务的生命周期关键方面并且提供了与其他组件相绑定的机制的回馈方法。最为重要的一些需要被覆盖的方法有:

onStartCommand()

系统在另一个组件,例如一个活动,通过调用startService()来请求启动服务时将调用这个方法。一旦这个方法被执行,服务将被启动并永久地运行于后台。如果使用了它,就必须负责在工作完成后通过调用stopSelf()或stopService()来终止服务。(如果只是想要提供绑定,不需要使用这个方法。)

onBind()

系统在另一个组件要通过调用bindService()来与服务相绑定(例如执行RPC)时将调用这个方法。在使用这个方法时,必须提供一个接口供客户端返回IBinder来与服务通信。这个方法总是需要被使用,不过要是不允许绑定的话,则应当返回null值。

onCreate()

系统在服务初次被创建时将调用这个方法,来执行一次性设置步骤(这会在调用onStartCommand()或onBind()之前)。如果服务已经在运行,这个方法则不会被调用。

onDestroy()

系统将在服务不再被使用,即将被销毁时调用该方法。服务应当使用它来清除线程、已注册的监听器(registered listener)、接收者(receiver)等资源。这将是最后一次调用服务接收者。

如果一个组件通过调用startService()(这将会调用onStartCommand())来启动一个服务,服务会不停地运行直至有stopSelf()来中止其自身或是有另一个组件通过调用stopService()来中止它。

如果一个组件调用bindService()来创建服务(此时onStartCommand()不会被调用),那么服务只会在组件与之绑定时运行。一旦服务与所有的客户端解除绑定,系统就会销毁它。

Android系统会在内存低时强制终止一个服务,因为它必须保有处于用户焦点的活动的系统资源。如果一个服务与用户焦点下的活动相绑定,那就不太容易被杀除。如果该服务被声明于前台运行(之后会讨论该情况),那它就几乎不可能被杀除了。反过来说,如果服务是启动的并且长期运行,系统将会在一段时间后降低其在后台任务列表中的位置,这样服务会变得较为可能被杀除——如果这个服务是启动的,那就必须做好精心设计以使系统能够重新启动它。如果系统杀除了这个服务,它将在资源可用后马上再次启动(尽管如后面将要提到的那样,这也取决于onStartCommand()所返回的值)。关于系统如何销毁服务的更多信息,参阅“进程和线程”文档。

在下面的小节中,将能看到如何创建各种类型的服务以及如何在其他应用程序组件中使用它们。

在manifest中声明一个服务

就像活动(和其他组件)一样,必须在程序的manifest文件中声明所有的服务。

要声明一个服务,添加<service>作为<application>元素的子元素。例如:

在<service>元素中可以包含其他的一些属性以定义其特性,例如启动服务所需的许可和服务所要运行于的进程等。更多信息请参见<service>元素的参考文档。

正如活动一样,一个服务可以定义其意图过滤器以允许其他组件使用非显式的意图来激活它。通过声明意图过滤器,用户设备上所安装的其他程序中的组件可以在服务声明的意图过滤器与该程序传递给startService()的意图相匹配时启动这个服务。

如果只打算在本地使用服务(其他的程序不会使用该服务),那就不必(而且不应该)提供任何意图过滤器。没有任何意图过滤器时,必须使用一个显式地标明了服务类的名称的意图来启动该服务。关于启动一个服务的更多信息将在之后讨论。

此外,当包含了android:exported属性且将其设置为“false”后就可以确保该服务是本程序私有的。这种做法在服务支持意图过滤器时将会很有效。

关于为服务创建意图过滤器的更多信息,参见“意图和意图过滤器”文档。

创建一个启动的服务

一个启动的服务是由另一个组件通过调用startService()来启动的,这也将会调用该服务的onStartCommand()方法。

当一个服务是启动的之时,它拥有一个和启动它的组件不相关的生命周期,因此这个服务可以在后台无限期地运行,即使那个启动它的组件被销毁。因此,这个服务应当在工作完成后通过调用stopSelf()来终止自己,或者被别的组件调用stopService()来中止。

诸如活动这样的应用程序组件可以通过调用startService()并传递指向该服务的意图以及服务所需的数据来启动一个服务。这个服务将在onStartCommand()方法中接收这个Intent。

比如说,假定有一个活动需要在一个在线数据库中保存一些信息。这个活动可以启动一个协同服务,将需要保存的数据通过一个意图传递给startService()。该服务会在onStartCommand()中接收这个意图,与因特网连接并执行数据库事务。当事务完成后,服务将终止自身并销毁。

注意:服务与声明它的程序在同一个进程中,且默认运行于同一个线程。因此,如果服务要在用户和该程序的一个活动进行交互时执行密集型或阻塞性的操作,就会降低活动的性能表现。要避免这样的程序性能下降,就应当为服务新建一个线程。

一般来说,有两个类可以继承以创建一个启动的服务:

Service

这是所有服务的基类。在继承这个类之后,由于服务默认运行于程序的主线程而会降低程序内正在运行的活动的性能表现,因此必须创建一个新的线程供服务运行。

IntentService

这是Service的一个子类,使用一个工作线程(worker thread)来依次处理所有的请求。如果服务不需要同时处理多个请求的话,这将是最好的选择。只需要使用onHandleIntent()来接收每一个启动请求便可以在后台完成工作。

下面的小节将介绍如何利用这两个类来实现一个服务。

继承IntentService类

由于大部分启动的服务不需要同时处理多个请求(这将是一种危险的多线程情况),通过IntentService类来实现一个服务可说是最佳选择。

IntentService将会做一下工作:

  • 创建一个不同于程序主线程的默认工作线程以执行所有传送至onStartCommand()的意图。
  • 创建一个工作队列以依次传递意图至onHandleIntent(),这样就不必再担心多线程问题。
  • 在所有请求被处理后终止服务,这样就不必再调用stopSelf()。
  • 提供一个返回null值的onBind()默认实现。
  • 提供一个能发送意图至工作队列的onStartCommand()默认实现(意图之后将被传送至onHandleIntent())。

这样一来就只需实现onHandleIntent()以执行客户端提供的工作即可。(不过,仍然需要为服务提供一个简单的构造函数。)

这里有一个使用IntentService的例子:

所有需要的就只是一个构造函数以及一个onHandleIntent()。

如果还要覆盖其他的回馈方法,比如onCreate(),onStartCommand()或是onDestroy(),就必须要调用其父类的实现,这样IntentService才能正确处理工作线程的生命周期。

例如,onStartCommand()必须返回一个默认的实现(就像被意图获取并传递给onHandleIntent()的那样):

除了onHandleIntent(),仅有的另一个不必调用其父类的方法是onBind()(不过这个方法只有在服务允许绑定时才会被使用)。

下一节中将会介绍在继承Service基类时相同类型的服务是如何被使用的。代码会稍有些长,不过在处理同步开始的请求的情况下很有用。

继承Service类

正如在之前的小节中所见到的,使用IntentService可以很轻松地实现一个启动的服务。不过,如果需要一个服务可以多线程工作(而不是通过工作队列处理启动请求),就要继承Service类来处理每一个意图。

为了做对比,下面的范例代码中Service类的实现和之前的IntentService将执行相同的工作。也就是说,对于每一个启动请求,将依次一个个地使用一个工作线程来执行任务:

可以看到,这比使用IntentService要复杂许多。

不过,因为是由自己来处理每一个onStartCommand()的调用而可以同时执行多个请求。本范例并没有这么做,但是在需要的情况下可以为每一个请求创建新的线程来运行(而不需要等待前一个请求完成)。

注意,onStartCommand()方法必须返回一个整型数。该整型数是用以描述系统应当如何在要杀除该服务时进行操作的值(就像之前所讨论的,IntentService的默认实现已经处理了这个问题,当然也可以修改其行为)。onStartCommand()的返回值必须是以下常量之一:

START_NOT_STICKY

系统在onStartCommand()返回后杀除服务,且不重建该服务(除非有未处理的意图要传递)。要避免服务在不需要时或是程序可以轻松地重启任何未完成的工作时运行,这将是最安全的选项。

START_STICKY

系统在onStartCommand()返回后杀除服务,重建该服务并调用onStartCommand()。不过系统不会重新传递最后一个意图,而是以一个空(null)意图调用onStartCommand()(除非有未处理的意图要启动服务。这种情况下,那些意图将被传递)。这个选项适用于媒体播放器之类的不执行指令但是要永远运行以等待任务的服务。

START_REDELIVER_INTENT

系统在onStartCommand()返回后杀除服务,以最后传递给服务的那个意图来重建该服务并调用onStartCommand()。所有未处理的意图将被依次传递。这适用于主动执行那些需要迅速被继续的工作(例如下载文件)之类的服务。

关于这些返回值的更多信息,请参见每一个常量的链接参考文档。

启动一个服务

通过向startService()传递一个Intent(来指定要启动的服务),可以在一个活动或是其他的应用程序组件中启动一个服务。Android系统将调用服务的onStartCommand()方法并将该Intent传递给它。(不应当直接调用onStartCommand()。)

例如,一个活动可以显式地使用意图和startService()来启动前面一节的范例服务(helloService):

startService()方法会立即返回,之后Android系统将调用服务的onStartCommand()方法。如果服务没有正在运行,系统将先调用onCreate(),然后再调用onStartCommand()。

如果服务没有提供绑定,那么传递给startService()的意图就成为应用程序组件和服务之间唯一的通信模式。不过,如果希望服务回送一个结果,那么可以让启动了这个服务的客户端创建一个PendingIntent(配合getBroadcast())来广播,之后将其传递给启动了服务的Intent内的这个服务。这样这个服务就可以用广播来传送结果。

多个服务启动请求会引起服务的onStartCommand()方法多次响应调用。不过,要终止一个服务则只需要一个请求(配合stopSelf()或stopService())。

终止一个服务

一个启动的服务必须自己管理自己的生命周期。也就是说,除非系统内存不足并且服务在onStartCommand()返回后继续运行,否则系统不会终止或是销毁该服务。所以,服务必须通过调用stopSelf()来自己终止自己,或者让另一个组件调用stopService()来终止它。

一旦用stopSelf()或是stopService()来请求终止,系统会立即销毁这个服务。

不过,如果服务要同时处理多个onStartCommand()请求,那就不应该终止这个服务,因为可能还接收到了一个新的启动请求(在一个请求的最后终止这个服务也将会终止后一个请求)。要避免这个问题,可以使用stopSelf(int)来确保终止服务的请求是总之基于最后接受的启动请求的。也就是说,当调用stopSelf(int)时,需要将终止请求相对应的启动请求的ID(即传递给onStartCommand()的startID)传入。这样如果服务在调用stopSelf(int)之前接收到了一个新的启动请求,ID就会不符合,服务便不会被终止。

注意:程序要在服务不工作时终止它是很重要的。这样以避免浪费系统资源及消耗电力。如果必要,可以让其他组件通过调用stopService()来终止服务。即使允许了服务的绑定,也必须总是在服务接收过onStartCommand()的调用时终止服务。(抱歉这句的翻译有点问题,请参考原文 Even if you enable binding for the service, you must always stop the service yourself if it ever received a call to onStartCommand().)

关于服务生命周期的更多信息,请参见之后关于“管理服务生命周期”的小节。

创建绑定的服务

绑定的服务允许应用程序组件通过调用bindService()与之绑定来建立一个长期连接(并且通常不允许组件通过调用startService()来启动它)。

应当在希望通过程序内的活动或其他组件与服务交互,或者是以进程间通信(interprocess communication,IPC)让本程序的部分功能开放给其他程序时,创建绑定的服务。

要创建一个绑定的服务,必须使用onBind()回馈方法来返回一个定义了与服务通信的接口的IBinder。这样其他应用程序组件可以调用bindService()来检索这个接口并调用服务的方法。该服务只为了与之绑定的应用程序组件而存在,因此当没有组件与之绑定时,系统将销毁它(不需要像通过onStartCommand()启动一个服务时那样来终止一个绑定的服务)。

要创建一个绑定的服务,要做的第一件事是定义一个指定了客户端如何与服务通信的接口。这个服务与客户端之间的接口必须是IBinder的一种实现,并且必须在服务的onBind()回馈方法中被返回。一旦客户端接收到了IBinder,它将能够通过这个接口开始与服务交互。

多个客户端可以同时与一个服务绑定。当一个客户端完成了与服务的交互后,它会调用unbindService()来解除绑定。一旦没有任何客户端与服务绑定,系统将销毁这个服务。

有多种方法来实现一个绑定的服务,它们比实现一个启动的服务要复杂得多,因此将在一篇独立的文档中来讨论关于绑定的服务的问题。

向用户发送通知

一个服务一旦运行,它就可以通过Toast通知或是状态栏通知来提示用户当前事件。

Toast通知是一种在当前窗口表层出现一段时间后便会消失的消息,而状态栏通知则在状态栏显示一个带有消息的图标,用户可以选择以进行处理(例如启动一个活动)。

通常, 状态栏通知是后台工作完成时(例如文件下载完成)最佳的通知方式,用户可以对完成的工作进行处理。当用户从扩展视图中选择该通知,它就会启动一个服务(比如来查看下载的文件)。

参见Toast通知或是状态栏通知的开发指南以获取更多信息。

在前台运行服务

前台服务是一种用户能够意识到的服务,且不会再内存不足时被系统杀除。一个前台服务必须提供状态栏通知并归于“正在运行”标题之下,这意味着除非该服务被终止或是移出前台,它将一直存在于通知窗口中。

例如,一个通过服务来播放音乐的音乐播放器应当被设置为运行于前台,因为用户将明确地意识到它正在运行。状态栏中的通知应该显示当前歌曲,并允许用户由此通知启动一个活动来与音乐播放器交互。

要请求服务运行于前台,需要调用startForeground()。该方法使用两个参数:一个唯一识别了通知的整型数以及一个状态栏的Notification。例如:

要把服务从前台移出,需要调用stopForeground()。该方法需要一个布尔量来确定是否需要同时移除状态栏通知。这个方法并不会终止服务。不过,如果终止了仍在前台运行的服务,那么通知也将会被一同移除。

注意:startForegound()和stopForeground()方法是在Android 2.0(API Level 5)中被引入的。为了在较早版本的平台上使服务在前台运行,比如使用较早的setForeground()方法——参见startForegound()文档以获取关于提供向下兼容性的更多信息。

要获取关于通知的更多信息,请参见“创建状态栏通知”。

管理服务的生命周期

服务的生命周期要比活动的简单许多。不过,需要更加地注意一个服务是怎样被创建和销毁的,因为服务可以不被用户发觉地在后台运行。

服务生命周期——从它被创建直到被销毁——可以沿着两条不同的路线发展:

  • 一个启动的服务
    • 服务会在另一个组件调用startService()时被创建。之后服务将会永久运行,必须调用stopSelf()来终止它自己。也可以由另一个组件调用stopService()来终止服务。当服务被终止后,系统将销毁它。
  • 一个绑定的服务
    • 服务会在另一个组件(客户端)调用bindService()时被创建。客户端之后通过一个IBinder接口与服务通信。客户端可以通过调用unbindService()来关闭连接。多个客户端可以与同一个服务绑定。当所有客户端都与之解除绑定后,系统将销毁这个服务。(服务不必终止自己。)

这两条路线并不是完全独立的。也就是说,可以与一个通过startService()启动的服务相绑定。例如,一个后台音乐服务可以通过以一个指定了所要播放的音乐的Intent来调用startService()启动。之后,当用户想要控制音乐播放或是获取当前音乐的信息时,可以通过调用bindService()来将其与一个活动相绑定。在这样的情况下,直到所有的客户端与之解除绑定之前,stopService()或是stopSelf()并不会真正终止这个服务。

使用生命周期回馈

和活动一样,服务也有生命周期回馈方法。可以使用它们来监视服务的状态的改变以在恰当的情况下做出恰当的动作。下面的一个服务的框架演示了每一种生命周期方法:

图2. 服务的生命周期。左边的图表展示了由startService()创建的服务的生命周期,右边的图标则是由bindService()创建的服务的生命周期。

注意:和活动的生命周期回馈方法不同,不需要调用这些方法父类实现。

通过使用这些方法,可以监视服务生命周期中的两个嵌套循环:

  • 一个服务的整个生命期从onCreate()开始,至onDestroy()返回时结束。和活动一样,一个服务在onCreate()中进行初始化工作并在onDestroy()中释放所有剩余资源。例如,一个音乐播放服务在onCreate()中创建音乐播放线程,在onDestroy()中终止线程。onCreate()和onDestroy()方法能被所有的服务调用,不论它是由startService()还是bindService()创建的。
  • 一个服务的活动生命期(active lifetime)从onStartCommand()或是onBind()的调用开始。这两种方法将分别处理传递给startService()或是bindService()的Intent。如果服务是启动的(started),则活动生命期的结束和整个生命期相同(服务在onStartCommand()返回后依然是活动的)。如果服务是绑定的(bound),那么活动生命期将会在onUnbind()返回时结束。

注意:虽然一个启动的服务在调用stopSelf()或stopService()时终止,但对此却没有一个专门的回馈方法(没有onStop()回馈方法)。所以,除非服务与客户端相绑定,否则系统将会在服务终止时销毁它——onDestroy()是唯一会被接收的回馈方法。

图2描绘了一个服务的典型回馈方法。尽管它区分了通过startService()和bindService()创建的服务,要注意任何服务,无论是如何被创建的,都允许被客户端绑定。因此,一个最初由onStartCommand()启动的服务(由一个客户端调用startService())也可以接收onBind()调用(在客户端调用bindService()时)。

关于创建可以绑定的服务的更多信息,请参见“绑定的服务”文档。其中包含了“管理绑定的服务的生命周期”一节中onRebind()回馈方法的更多信息。

本页部分内容根据Android Open Source Project创作并共享的内容修改,并在知识共享 署名2.5许可协议中所述条款的限制下使用。

创建状态栏通知

状态栏通知会向系统状态栏添加一个图标(还可以附有文本消息)以及一条“通知”窗口内的可扩展消息。当用户选择了该可扩展消息,Android发出一个有通知定义的Intent(通常来启动一个活动)。也可以设置通知来以声音、振动和设备闪光来提醒用户。

状态栏通知应当被用于任何后台服务需要提示用户发生了事件并要求回应的情况。后台服务不应自己启动活动来接收用户交互操作。它应当创建一个状态栏通知,在用户选择该通知时启动一个活动。

下面的截图展示了一个左侧有通知图标的状态栏。

下一张截图展示了“通知”窗口中扩展的通知消息。将状态栏下拉可展开通知窗口(或从主页面选项菜单中选择“通知”)。

基本内容

活动或是服务可以初始化一个状态栏通知。由于一个活动只有在其状态是活动的且处于焦点时才能执行行动,所以应当通过服务来创建状态栏通知。这样,通知就能从后台创建,即使用户在使用另一个程序或是设备处于休眠状态。要创建通知,必须使用两个类:Notification和NotificationManager。

使用Notification类的一个实例来定义状态栏通知的属性,例如状态栏图标,扩展消息,或要播放的音效等更多内容。NotificationManager是一个Android系统服务,它执行并管理所有Notification。不必实例化NotificationManager。为了传递Notification,必须通过getSystemService()获取NotificationManager的一个引用,之后再希望通知用户时,使用notify()将其传递给自己的Notification对象。

要创建一个状态栏通知:

1.获取NotificationManager的一个引用

2.实例化Notification:

3.定义Notification的扩展消息和Intent:

4.将Notification传递给NotificationManager:

这样之后,用户将收到通知。

管理通知

一个Notification对象定义显示于状态栏和通知窗口的通知消息的具体细节以及其他提示设置,例如声音或是灯光闪烁。必须通过getSystemService()方法检索一个指向它的引用。例如:

在希望发送状态栏通知时,需要用notify(int, Notification)来传递Notification对象至NotificationManager。第一个参数是Notification的唯一ID,第二个是Notification对象。ID唯一识别了程序中的Notification。如果需要更新Notification或(程序管理多个不同Notification时)在用户由Notification中定义的Intent返回程序时选择正确的行动,这将会是很必要的。

要在用户从通知窗口中选择状态栏通知的同时清除它的话,就向Notification对象添加”FLAG_AUTO_CANCEL”旗标。也可以手动地通过向cancel(int)传递通知ID来清除,或使用cancelAll()来清除全部通知。

创建通知

一个Notification对象定义了显示在状态栏和通知窗口的通知消息的细节,以及其他提示的设置,例如声音和灯光闪烁。

一个状态栏通知需要以下所有内容:

  • 一个状态栏的图标
  • 一个标题和扩展视图中的扩展消息(除非另外自定义了一个的扩展视图)
  • 一个PendingIntent,以供在通知被选中时放出

状态栏通知的可选设置则包括:

  • 状态栏的ticker-text消息(注:暂时还没有决定怎么翻译这个ticker-text比较好)
  • 提示音
  • 振动设置
  • LED灯设置

新建一个Notification的基本内容包括构造函数Notification(int, CharSequence, long)和setLatestEventInfo(Context, CharSequence, CharSequence, PendingIntent)方法。它们定义了一个Notification所需的所有设置。下面的片段展示了一种基本的通知创建方式:

更新通知

在程序中的事件持续发展的过程中可以不断更新状态栏通知中的信息。例如,在前一条短信被阅读前又收到新的一条时,短消息程序会更新现有通知,显示收到的未读短信的总数。这一更新通知的做法要比向NotificationManager添加新的通知更好,因为它避免了使通知窗口变得杂乱。

由于每一个通知都有NotificationManager通过一个整型ID唯一定义,因此可以通过以新值调用setLatestEventInfo()来修改通知,改变Notification的一些域的值,之后再次调用notify()。

可以通过对象成员域来修改每一种属性(除了Context和扩展消息标题及文本)。应当始终在通过新值调用setLatestEventInfo()更新通知时修改contentTitlecontentText的文本消息。之后调用notify()来更新通知。(当然,如果创建了自定义扩展视图,那更新这些标题和文本的值就没有效果了。)

添加声音

可以使用(用户所定义的)默认消息提示音或是程序指定的声音来提示用户。

要使用用户默认提示音,在defaults域中添加“DEFAULT_SOUND”:

要在程序中使用不同的提示音,需要传入一个Uri引用至sound域。下面的例子使用了一个保存于设备SD卡中的已知音频文件:

接下来的例子中,音频文件是从内部的MediaStore的ContentProvider中选择的:

在本例中,媒体文件的确切ID(”6″)是已知的,附加于内容Uri之上。如果不知道确切的ID,就必须使用ContentResolver来请求所有MediaStore内可用的媒体。参见内容提供者文档中关于使用ContentResolver的更多信息。

如果希望声音在用户响应通知或通知被取消前不断持续,就在flags域中添加“FLAG_INSISTENT”。

注意:如果defaults域包含了“DEFAULT_SOUND”,那么默认声音将会覆盖其他所有定义过的sound域。

添加振动

可以使用默认振动模式或是程序自定义的振动模式来提醒用户。

要使用默认模式,在defaults域中添加“DEFAULT_VIBRATE”:

要定义属于自己的振动模式,需向vibrate域传递一组long型数组值:

long型数组定义了震动有无的时长间隔(以毫秒计)。第一个值是在开始之前等待的时间(无震动),第二个值是首次震动的长度,第三个值是之后的暂停时长,依此类推。振动模式可以为任意长度,不过不能设置循环。

注意:如果defaults域包含了“DEFAULT_VIBRATE”,那么默认振动将覆盖所有在vibrate域中定义的震动。

添加灯光闪烁

要使用LED灯光闪烁来提醒用户,可以用默认灯光模式(如果可用),或自定义灯光模式及色彩。

要使用默认灯光设置,在defaults域中添加“DEFAULT_LIGHTS”:

要自定义模式和色彩,需要为ledARGB域(色彩),ledOffMS域(LED关闭时长,单位毫秒)和ledOnMS域(LED开启时长,单位毫秒)设定值,并在flags域添加“FLAG_SHOW_LIGHTS”:

在这个例子中,绿灯将反复闪烁,亮300毫秒,暗1秒。设备的LED并不支持光谱中所有的色彩,每一种设备支持的色彩也有不同,硬件会尽可能提供最佳效果。绿色是最常用的通知色彩。

更多特性

可以通过Notification域和旗标向通知添加更多特性。包括以下有用的特性:

“FLAG_AUTO_CANCEL”

旗标将其加入flags域以在通知被从通知窗口中选择时自动取消

“FLAG_INSISTENT”

旗标将其加入flags域以重复音频播放直至用户回应

“FLAG_ONGOING_EVENT”

旗标将其加入flags域以将通知分组于通知窗口的“正在进行”标题之下。这表示程序正在运行——它的进程仍在后台运行,尽管程序本身不可见(例如音乐和通话)

“FLAG_NO_CLEAR”

旗标将其加入flags域以表明通知不应当被“清除通知”按钮清除。这在程序正在运行时很有用。

number

这个值显示了当前被通知所表示的事件数。正确的数字会在状态栏图标顶部显示。如果要用这个域,那就必须在通知被创建时从“1”开始计数。(如果在一次更新时将这个值由零改为了比零大的任何数,这个数字就不会被显示。)

iconLevel

这个值显示了当前用于通知图标的LevelListDrawable等级。配合在LevelListDrawable中定义的drawable,可以通过改变这个值使状态栏中的图标呈现动画效果。参见LevelListDrawable以获取更多信息。

参见Notification类参考资料以获取关于添加特性来自定义程序的更多信息。

创建自定义扩展视图

默认情况下用于通知窗口的扩展视图包括基本的标题和文本消息。这些是由setLatestEventInfo()方法的contentTitlecontentText参数所定义。然而,也可以使用RemotiViews来自定义扩展视图的布局。下面的屏幕截图展示了一个以线性布局使用ImageView和TextView的自定义扩展视图。

要定义扩展消息自有的布局,需要实例化RemoteViews对象并将其传递给Notification的contentView域。还要将PendingIntent传递给contentIntent域。

可以通过一个范例来更好的理解如何创建一个自定义扩展视图:

1.创建扩展视图的XML布局。例如,创建一个名为custom_notification_leyout.xml的布局文件并像这样编写:

该布局将被用于扩展视图,不过ImageView和TextView的内容仍需要由程序来定义。RemoteViews提供了一些用以定义这些内容的便利方法…

2.在程序代码中,使用了RemoveViews方法来定义了图片和文本。之后将RemoteViews对象传递至Notification的contentView域,就像这样:

如上所示,将程序的package名和布局资源ID传递给RemoteViews的构造函数。之后,通过setImageViewResourse()和setTextViewText()来定义ImageView和TextView的内容。两者都需要传递希望使用的View对象的正确的引用ID以及其值。最后RemoteViews对象被传递到了Notification的contentView域。

3.因为在使用自定义视图时不需要使用setLatestEventInfo()方法,所以必须在contentIntent域中定义Notification所使用的Intent,如下所示:

4.现在通知就可以像往常一样被发送了:

RemoteViews类还包括可以用来在通知的扩展视图中轻松添加Chronometer(计时器)或是ProgressBar(进度条)的方法。关于通过RemoteViews来创建自定义布局更多信息,请参考RemoteViews类的参考资料。

注意:在创建自定义扩展视图时,必须非常小心以确保自定义布局在不同设备方向(orientation,即横屏或是竖屏)和分辨率下都能正常工作。尽管这个建议对于Android上创建的所有View布局来说都是值得注意的,但在这一部分里尤为重要,因为布局所受的限制非常严格。所以不要将自定义布局设计的过于复杂,此外应当在多种不同情况下作测试。

返回通知用户

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

创建Toast通知

toast通知是一种跳出于屏幕表面的消息。它只占用信息所需的空间,用户当前的活动仍然是可见且能够操作的。该通知自动地淡入淡出,不接受交互事件。toast可以由后台服务创建,因此即使程序本身不可见,也能够被显示。

下面的截图展示了闹钟程序的toast通知示例。一旦闹钟被打开就会显示一条toast以确认其已经打开。toast可以在一个活动或是一个服务中被创建并显示。如果是从服务中创建了一个toast通知,它会出现在当前处于焦点的活动之前。

如果如要用户对通知进行相应的话,考虑使用状态条通知。

基本内容

首先,用某一makeText()方法来实例化一个Toast对象。该方法需要三个参数:程序Context,文本信息,和toast的持续时间。它将返回一个正确初始化的Toast对象。可以用show()来显示该toast通知,就像下面的范例那样:

该范例演示了大部分toast通知所需的各项内容。通常不会需要使用其他代码。不过,如果想要在其他位置显示toast或是要用自己的布局替换默认的简单文字消息的话,下一节中将描述如何实现。

还可以将方法连接起来以避免持有Toast对象,就像这样:

定位Toast通知

标准的toast通知出现在屏幕中下方。可以通过setGravity(int, int, tin)方法来改变位置。它接受三个参数:重力常量,X方向补偿和Y方向补偿。

例如,如果决定把toast置于左上角,可以这样设置重力:

如果想要把位置推到右侧,只需增加第二个参数的值。要向下移动,就增加最后一个参数的值。

创建自定义toast视图

如果不满足于简单的文本消息,还可以为toast通知创建一个自定义布局。要创建自定义布局,需要通过XML或是在程序代码内定义一个View布局,然后传递根View对象至setView(View)方法。

例如,可以通过以下的XML(保存为toast_layout.xml)创建出下面截图中的布局:

请注意,LinearLayout元素的ID是“toast_layout”。必须用这个ID从XML中解压出布局内容,就像这样:

首先,用getLayoutInflater()(或getSystemService())来检索,之后使用inflate(int, ViewGroup)将布局内容从XML中解压。其中第一个参数是布局资源ID,第二个参数是根View。可以在被解压的布局中找到其他的View对象,之后获取并定义ImageView和TextView元素的内容。最后,用Toast(Context)创建一个新的toast,设置其gravity和duration等属性。然后调用setView(View)将其传递给所解压的布局。现在就可以通过调用show()来显示自定义布局的toast了。

注意:除非要用setView(View)来定义布局,不然不要使用公有构造函数来构造Toast。如果没有可用的自定义布局,则必须要用makeText(Context, int, int)来创建Toast。

返回通知用户

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

通知用户

在不少情况下会需要通知用户程序当前发生的事件。一些事件需要用户回应,另一些则不需要。例如:

  • 当比如保存文件之类的事件完成时,应该出现一条信息确认保存成功。
  • 如果程序运行于后台,需要获取用户的注意时,程序应该创建一条通知让用户根据情况作出回应。
  • 如果程序正在执行需要用户等待的任务(比如读取文件),程序应该显示进度圈或是进度条。

以上每一种通知任务都是通过不同技术实现的:

  • Toast通知,用于来自后台的简单信息。
  • 状态条通知,用于来自后台需要用户回应的长期消息。
  • 对话框通知,用于和活动相关的通知。

本文综述了这些用以通知用户的技术,并包含了指向完整文档的链接。

Toast通知

toast通知是出现在窗口表层的弹出式信息。它只占用信息所需的空间,用户当前的活动仍然是可见且能够操作的。该通知自动地淡入淡出,不接受交互事件。toast可以由后台服务创建,因此即使程序本身不可见,也能够被显示。

toast最适合于确定用户正在关注屏幕时显示简短信息,例如“文件已保存”。toast不能接受用户交互事件;如果希望用户进行回应并作出操作的话,就要改用状态条通知。

更多信息,请参考“创建toast通知”。

状态条通知(Status Bar Notification)

状态条通知会向系统状态条添加一个图标(还可以附有文本消息)以及一条“通知”窗口内的可扩展消息。当用户选择了该可扩展消息,Android发出一个有通知定义的Intent(通常来启动一个活动)。也可以设置通知来以声音、振动和设备闪光来提醒用户。

这类通知适用于程序在后台服务中工作、需要通知用户某一事件时。如果需要在活动仍然处于焦点时提示用户某一事件的发生的话,考虑该用对话框通知比较好。

更多信息,参阅“创建状态条通知”。

对话框通知(Dialog Notification)

对话框通常是一个出现于当前活动前方的小窗口。下层的活动失去了焦点使得对话框将接受所有的用户交互操作。对话框常用作通知以及和程序执行过程直接相关的活动。

应当在需要显示一个进度条或有需要用户确认的简短消息(比如一个有“Ok”和“取消”按钮的提示)的情况下使用对话框。也可以将对话框作为程序UI的一个组成部分等其他非通知用途。要获取关于所有可用的对话框类型的完整讨论以及其使用方法,参阅“创建对话框”。

返回用户界面

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

常见布局对象

这一节将描述一些在程序中较为常用的布局对象类型。和其他布局一样,它们是ViewGroup的子类。另请参阅“Hello Views”教程以获取使用Android View布局的更多指导。

FrameLayout

FrameLayout是最为简单的布局对象类型。简单说它是屏幕上的一块空白空间,之后可以在其中填充单个对象——例如,一张需要读取的图片。所有FrameLayout的子元素位于屏幕的左上角;不能为子视图指定另外的位置。后继的子视图将简单地覆盖之前的视图,部分或是完全遮住它们(除非新的那个对象是透明的)。

LinearLayout

LinearLayout根据在orientation属性中所定义的,沿着单一方向——水平或是垂直——排列子视图。所有的子视图一个接一个堆砌,因此一个垂直列表中无论子视图有多宽,每一行都只有一个子视图。而一个水平列表高度只有一行(最高的子视图的高度加上间隙)。LinearLayout能显示每一个子视图之间的边距和重力(右侧、中心或左侧)。

LinearLayout还支持为其单独的子元素设定权重(weight)。该属性给视图设定了“重要性”。允许其扩展填充父视图中的任意剩余空间。子视图可以指定一个整型权重值,之后视图组中的剩余空间会按子视图所声明的权重比来分配。默认权重是零。例如,如果有三个文本框,其中两个的权重被声明为1,另一个没有权重(0),那么这个没有权重的文本框不会扩展,只会占它内容所需的空间。另两个会按相同比例扩展,充满三个文本框被计算后所剩余的空间。如果第三个文本框权重为2(而不是0),那它就被声明为比其他两个”更为重要“,而获得总剩余空间的一半,另两个则是平分另一半。

下面的两种形式代表了含有一组元素的LinearLayout:一个按钮、一些标签和一些文本框。文本框的宽度设置为fill_parent;其他元素设置为wrap_content。重力(gravity)默认是向左(left)。两个版本哦差异在于左边的没有设置权重(默认为零),右边的一个文本框的权重被设为了1。如果Name文本框也被设为了1的话,Name和Comments文本框则会一样高。

 

linearlayout
linearlayout

Tip:为了在屏幕上创建比例合适的布局,可以创建一个容器视图组,layout_widthlayout_height属性设为fill_parent;设置子视图的高和宽为0;根据所希望的屏幕比例设置每一个子视图的相对权重(weight)。

在一个水平LinearLayout中,项目根据其文本基准线的位置排列(最上部或最左侧的第一个元素的第一条线被认为是参考线)。这样在浏览元素时就不必上下移动视线以阅读相邻元素的元素文本了。可以通过在布局XML中设置android:baselineAligned=”false”来关闭这一功能。

要查看其他的范例代码,请参阅“Hello LinearLayout”教程。

TableLayout

TableLayout将其子视图按行、列排列。TableLayout容器不显示其行、列或单元格的边线。表格的列数和拥有单元格最多的那行相同。一张表格可以有空单元格,不过单元格无法跨越列,就像HTML中一样。

TableRow对象是TableLayout的子视图(每一个TableRow定义了表中单独的一行)。每一行有零个或更多的单元格,其每一个可以被其他类型的视图所定义。因此,一行单元格可能是由多种View对象,比如ImageView或TextView,构成的。一个单元格也可以是一个ViewGroup对象(例如,可以把另一个TableLayout嵌套于一个单元格内)。

下面的范例布局有两行,每行有两个单元格。相应的截图显示了其结果,单元格边界以点线显示(作为视觉效果而添加)。

可以将竖栏隐藏、拉伸填充屏幕或是标记为可缩小的以强制缩小表格适应屏幕。参见TableLayout参考文档以了解更多细节。

参见“Hello TableLayout”教程以查看范例代码。

RelativeLayout

RelativeLayout允许子视图指定它们相对于父视图的位置或它们之间的相互位置(通过ID来指定)。因此可以将两个元素靠着右侧边缘排列,或是于屏幕中心、或偏左侧上下排列,或是其他排列方式。元素将按指定的方式排列,因此如果第一个元素位于屏幕中央,其他和该元素相关的元素也会相对于屏幕中央进行排列。同时,由于有这样的顺序,如果是通过XML指定这种视图的话,要引用的元素(为了能定位其他的视图对象)必须在在其他视图引用其ID之前就被列于XML文件之中。

下面的例子展示了一个XML文件及其屏幕界面的显示结果。需要注意,引用了相对元素的那些属性(比如,layout_toLeft)是通过相对资源(@id/id)的语法来引用其ID的。

这些属性中的一部分直接被元素所支持,另一些则被其LayoutParams成员(RelativeLayout对于屏幕上所有元素的子类,这是因为所有元素都是RelativeLayout父对象的子对象)所支持。所定义的RelativeLayout参数有:widthheightbelowalignToptoLeftpadding[Bottom|Left|Right|Top]margin[Bottom|Left|Right|Top]。注意,其中一些参数是用于指定相对布局位置——它们的值必须是希望作为相对视图基准的元素的ID。例如,设置参数toLeft=”my_botton”至一个TextView将把该TextView放在ID为my_button的元素(比如在XML中先于TextView出现)的左侧。

要查看该范例代码,请参见“Hello RelativeLayout”教程。

重要视图组的总结

这些对象都有子UI元素。一些提供了可见的界面自有形式,另一些则是不可见的结构,只能用于管理其子视图。

描述
FrameLayout 用于显示单个对象的视图框架布局。
Gallery 一个可水平滚动的图像列表。
GridView 显示m列n行可滚动的表格。
LinearLayout 将子元素水平或是垂直排列为一行/列的布局。如果视窗长度超过屏幕长度,将会自动创建滚动条。
ListView 显示一列滚动列表。
RelativeLayout 允许指定子对象相互之间的位置(如:子对象A在子对象B的左侧) 或与父对象的相对位置(如:在父对象的上部)。
ScrollView 一列垂直的可滚动元素。
Spinner 在一个单行文本框中显示所绑定列表的其中一行。就像一个列表框,可以水平或是垂直下拉。
SurfaceView 提供所指定绘图表层的直接读取。 它可以持有位于屏幕表面的子对象,不过其实是为在屏幕上进行像素操作而非使用控件而设计的。
TabHost 提供了一个标签页选择器,通过监视点击行为以在标签被选择时切换屏幕内容。
TableLayout 一个行列数固定的表格布局。每一个单元格可以持有一个所选择的控件。每一行会自适应最大的列数。单元格的边界是不可见的。
ViewFlipper 在一个单行文本框中一次显示一个项目的列表。可以设置如同幻灯片一样在一定时间间隔后切换项目。
ViewSwitcher 和ViewFlipper相同。

返回用户界面

本页部分内容根据Android Open Source Project创作并共享的内容修改,并在知识共享 署名2.5许可协议中所述条款的限制下使用。

Android模拟器

Android SDK包含了一个移动设备模拟器——一个运行于你的电脑上的虚拟设备。模拟器可以让你进行原型和开发工作,同时可以在没有实体设备的情况下测试Android应用程序。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

开发者指南

欢迎来到Android开发指南!本指南提供了开发Android程序的实用指导和有关平台主要特性的文档。指南挖掘了Android背后的理念,解析了构造应用程序的框架,并介绍了在平台上开发、测试和发布软件的工具。

开发指南包含了Android平台的大部分文档,但不包括框架API的参考资料。对于API的特性,请见参考。

本指南分为若干部分:

Android基础

对Android的最初介绍——它是什么,它提供了哪些内容,你的程序如何适于Android。

框架主题

讨论Android框架和API的具体部分。通过应用程序基础一文获取对框架的简单介绍。之后按需探索其他的主题——从设计用户界面并设置资源到存储数据及使用权限。

Android Market主题

与在Android Market中发布并销售应用程序的主题有关的文档,例如如何加强授权政策以及应用程序内付款。

开发

使用Android开发调试及测试工具的说明。

发布

关于如何准备部署你的应用程序以及如何在准备好之后发布它的一些说明。

基本练习

关于编写高效率高兼容性的应用的推荐技巧。

网络应用程序

关于如何创建无缝运行于Android设备上的网络应用程序以及创建嵌有基于网络的内容的Android应用程序的文档。

附录

参考信息和特性,以及常见问答、术语表和其他信息。

在下载了SDK之后,从阅读开发指南起步吧。如果想要通过查看代码来开始学习,Hello Wordl教程通过一个标准的“Hello World”程序向你介绍了Android应用程序的一些基础知识。应用程序基础文档是学习应用程序框架的一个不错的入门材料。

要获取更多帮助,可以考虑加入一个或多个Android讨论小组。前往开发者论坛页面以了解更多。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

创建菜单

菜单是程序重要的一部分,它提供给用户一个熟悉的接口以进入程序功能或是设置。Android提供了一个简单的框架以供为应用程序提供标准的菜单。

有三种类型的应用程序菜单:

选项菜单

当用户按下菜单键后出现的一个程序中主要的菜单项目的集合。如果程序运行在Android3.0或更新的版本时,可以将菜单项目直接置于操作条作为快捷方式,也就是“操作项目”。

上下文菜单

当用户长按一个注册了上下文菜单的视图时出现的浮动菜单项目列表。

子菜单

当用户按下一个包含嵌套菜单的菜单项目时出现的浮动菜单项目列表。

本文档将说明创建每一种菜单的方式,如何使用XML来定义菜单的内容以及程序中响应用户选择项目的回馈方法。

创建菜单资源

不应当在程序代码中实例化菜单,而要在XML菜单资源中定义一个菜单和它的所有项目,并在程序代码中解压(作为可编程对象载入)该菜单资源。通过菜单资源来定义菜单是推荐的做法,因为这样可以从程序代码中分离菜单内容。通过XML也能更容易地可视化菜单的结构和内容。

要创建菜单资源,要在工程的res/menu目录下创建XML文件并通过以下元素构建菜单:

<menu>

定义包含菜单项目的菜单。<menu>元素必须是文件的根节点以包含一个或多个<item><group>元素。

<item>

创建一个在菜单中表示单个项目的MenuItem。该元素可以包含潜逃<menu>元素以创建子菜单。

<group>

一种可选的<item>元素的不可见的容器。它可以分类菜单项目以共享如活动状态和可见性等属性。参见“菜单组”一节。

这里有一个名为game_menu.xml的菜单范例:

该范例定义了一个包含两个项目的菜单。每一个项目包含这些属性:

android:id

项目唯一的资源ID,让程序可以在用户选择时识别项目。

android:icon

作为项目图标的drawable资源的引用。

android:title

作为项目标题的字符串的引用。

还可以在<item>中包含更多的属性,其中一些可以定义项目如何出现在动作条中。关于XML语法和菜单资源的属性的更多信息,参阅“菜单资源引用”。

解压菜单资源

可以在程序代码中通过MenuInflater.inflate()解压菜单资源(将XML资源转换为可编程对象)。比如,下面的代码在onCreateOptionsMenu()中解压了前面定义的game_menu.xml,用以作为活动的选项菜单:

getMenuInflater()方法返回给活动一个MenuInflater。这个对象可以调用inflate()来解压一个菜单资源使之成为一个Menu对象。在本例中,game_menu.xml定义的菜单资源被解压为Menu后被传递至onCreateOptionsMenu()。(这一选项菜单的回馈方法将在下一节中详细讨论。)

创建选项菜单

选项菜单中应当包含有基本的活动行为和必要的导航项目(比如,一个用以打开程序设置的按钮)。选项菜单中的项目可以通过两种不同的途径访问:MENU键或者是动作条(仅用于运行Android 3.0或更高版本的设备)。

在使用Android 2.3或更低的系统的设备上,选项菜单出现于屏幕底部,如图1所示。当被打开后,选项菜单首先可见的部分是图标菜单。它包含了前六个菜单项目。如果选项菜单中添加了六个以上的项目,Android会将第六个和之后的项目放入溢出菜单,在用户点击“More”菜单项目时被打开。

Figure 1
图1. 浏览器中选项菜单的截图

在Android 3.0或更高版本中,选项菜单中的项目被置于位于活动顶部的动作条中,取代了传统的标题栏的位置。选项菜单中所有的项目默认是置于溢出菜单中的,用户可以点击动作条右部的菜单图标来打开。不过,也可以把一些菜单项目作为“动作项目”直接置于动作条以便快速选择,如图2所示。

 

图 2. Email程序中动作条的截图,有两个选项菜单中的动作项目,以及一个溢出菜单。

当Android系统第一次创建选项菜单时,会调用活动的onCreateOptionsMenu()方法。在活动中覆盖这个方法,加入要传递给这个方法的Menu,按照之前在“解压菜单资源”中所说的方式解压菜单资源。比如说:

也可以在代码中加入菜单,使用add()来添加项目至菜单。

注意:在Android 2.3 或更低的版本中,系统在用户第一次打开选项菜单时才调用onCreateOptionsMenu()来创建它,但是在Android 3.0及以后,系统在活动被创建时就会马上创建选项菜单以设置动作条。

响应用户动作

当用户从选项菜单(以及动作条中的动作项目)中选择了一个菜单项目,系统会调用活动的onOptionsItemSelected()方法。该方法传递用户选择的MenuItem。可以通过调用getItemId()来识别菜单项目,它会返回菜单项目的唯一ID(它是在菜单资源中通过android:id属性定义,或者使用add()方法给定的一个整数)。可以将此ID与已知的菜单项目配对以执行正确的动作。例如:

在这个例子中,getItemId()请求所选菜单项目的ID,switch语句则比较这些ID和XML资源中分配的菜单项目资源ID。当一个switch case成功配对菜单项目,它返回true以表示菜单选择已被处理。否则,如果父类可以处理项目的话默认语句将传递菜单项目至其父类。(如果是直接扩展于Activity类的话,父类会返回false,不过比起直接返回false,传递未被处理的菜单项目至其父类是一种比较好的做法)。

此外,Android 3.0增加了可以在XML菜单资源中通过使用android:onClick属性定义菜单项目单击行为的功能。因此不需要使用onOptionsItemSelected()。使用android:onClick属性,可以指定用户选择菜单项目时调用某一方法。活动之后会调用在android:onClick属性中指定的方法以接受一个单独额MenuItem参数——当系统调用该方法时,会传递所选择的菜单项目。

Tip:如果程序包含多个活动,并且一些活动提供了相同的选项菜单的话,可以考虑创建一个活动只使用onCreateOptionsMenu()和onOptionsItemSelected()方法。之后将这个类扩展用于共用该选项菜单的活动。这样一来,只需要管理一组处理菜单动作的代码即可,其他每一个后继类都会继承其菜单行为。

如果想要在某一个后继活动中增加菜单项目,就在该活动中覆盖onCreateOptionsMenu()。调用super.onCreateOptionsMenu(menu)以创建菜单,之后使用menu.add()增加新菜单项目。也可以覆盖父类的行为来设置单个的菜单项目。

在运行时改变菜单项目

一旦活动被创建,onCreateOptionsMenu()方法只会像前面描述的那样被调用一次。系统会在活动被销毁前保持和重用在该方法中定义的菜单。如果想在创建后的任意时间改变选项菜单,就必须覆盖onPrepareOptionsMenu()方法。它将传递当前存在的Menu对象。当希望根据程序的当前状态来删除、添加、禁用或启用菜单项目时,这种做法会很有用。

在Android 2.3或更低版本上, 系统会在每一次用户打开选项菜单时调用onPrepareOptionsMenu()。

在Android 3.0或更高版本,则需要在想要更新菜单时调用invalidateOptionsMenu(),因为菜单是始终打开的。系统之后会调用onPrepareOptionsMenu()来更新菜单项目。

注意:不能改变基于当前处于焦点下的视图的选项菜单的项目。在触摸模式下(即用户没有使用轨迹球或是指点杆),视图无法成为焦点,因此不应该将焦点作为改变选项菜单项目的基础。如果想要创建对视图上下文敏感的菜单项目,请使用上下文菜单。

如果开发平台是Android 3.0或更高,确保阅读过“使用动作条”一文。

创建上下文菜单

上下文菜单从理念上来说类似于在PC上用户“右键单击”时显示的菜单。应该提供给用户上下文菜单以执行对于用户界面上属于某一特定项目的操作。在Android上,上下文菜单会在用户“长按”(按下不放)某一项目时被显示。

可以为任意View创建上下文菜单,不过上下文菜单通常被用于ListView中的项目。当用户长按ListView中的一个项目并且该列表被注册提供一个上下文菜单时,该列表项目会改变背景色以告知用户上下文菜单是可用的——它用橙黄色转变为白色并打开上下文菜单。(通讯录程序展示了这一特性。)

为了让一个View提供上下文菜单,比如给该视图“注册”一个上下文菜单。调用registerForContextMenu()并传递想要添加上下文菜单的视图。当该视图收到长按操作时,它会显示上下文菜单。

要定义上下文菜单的外观和行为,需要覆盖活动的上下文菜单回馈方法,即onCreateContextMenu()和onContextItemSelected()。

注册一个ListView

如果活动使用ListView并且希望所有的列表项目都提供上下文菜单,就要通过传递该ListView至registerForContextMenu()来为所有的项目注册上下文菜单。比如,如果正在使用ListActvity,就像这样注册所有的列表项目:

registerForContextMenu(getListView());

举例来说,这里有一个使用了context_menu.xml菜单资源的的onCreateContextMenu():

MenuInflater用于从菜单资源中解压上下文菜单。(也可以使用add()来添加菜单项目。)回馈方法参数包括用户所选择的View和提供所选项目附加信息的ContextMenu.ContextMenuInfo对象。可以用这些参数来判定应当创建哪一个上下文菜单,不过在这个例子中,活动所使用的所有上下文菜单都是相同的。

之后当用户从上下文菜单中选择了一个项目时,系统将调用onContextItemSelected()。这里有一个例子来说明如何处理选中的项目:

这段代码的结构与创建选项菜单的例子中的相似,getItemId()询问了所选项目的ID,一个switch语句来匹配菜单资源中所定义的项目ID。和选项菜单的例子一样,默认语句调用了父类以备处理当前类无法处理的项目。

在本例中,所选的项目是ListView中的一个项目。为了实现对所选项目的操作,程序需要知道所选项目的列表ID(即在ListView中的位置)。为了取得此ID,程序调用了getMenuInfo(),来返回包含id域中所选项目列表ID的一个AdapterView.AdapterContextMenuInfo对象。本地方法editNote()deleteNote()方法将接受该列表ID来对指定列表ID的数据进行操作。

注意:上下文菜单中的项目不支持图标或快捷键。

创建子菜单

子菜单是一种用户在选择另一菜单中的项目时打开的菜单。可以向任何菜单(除了子菜单)添加子菜单。子菜单在程序有大量可以按主题来组织的功能时很有用,就好象是PC程序的菜单栏(文件、编辑、视图等)。

当创建菜单资源时,可以通过添加一个<menu>元素作为一个<item>的子元素来创建子菜单,例如:

当用户从子菜单中选择了一个项目时,父级菜单的项目被选择时的回馈方法会接受这一事件。例如,如果上级菜单是一个选项菜单,那么onOptionsItemSelected()方法会在子菜单项目被选择时被调用。

也可以用addSubMenu()来动态地想已存在的菜单添加SubMenu。它会返回新的SubMenu对象,可以通过add()来对其添加子菜单项目。

其他菜单特性

对于大多数菜单来说还可以应用其他一些菜单特性。

菜单组

菜单组是共享某些特性的菜单项目的集合。将它们分成组后,可以:

  • 通过setGroupVisible()显示或隐藏所有项目
  • 通过setGroupEnabled()启用或禁用所有的项目
  • 通过setGroupCheckable()来设定是否所有项目都是可标记的

可以通过在菜单资源内的<group>元素内嵌套<item>元素以创建菜单组或是通过add()方法来指定一个组ID。

这里是一个包含了组的菜单资源的例子:

组中的那些项目似乎和那不在组中的第一个项目看起来是一样的——所有三个菜单内的项目是同级关系。不过,可以通过引用组ID和前面提到的方法来调整组中的两个项目的特性。

可标记菜单项目

对独立选项设置复选框或是对互斥选项设置单选框的话,菜单可以作为一种有用的开启或关闭选项的接口。图3展示了具有可单选项目的子菜单。

图 3.一个具有可选中项目的子菜单的截图

注意:图标菜单(来自于选项菜单)中的菜单项目不能设置复选框或是单选框。如果希望让图标菜单中的项目可被选中,必须在每次状态改变时通过改变图标和/或文字来手动指定选中状态。

可以使用<item>元素中的android:checkable属性为单个菜单项目定义选中行为,或使用<group>元素中的android:checkableBehavior属性来为整个组设置行为。比如,这个菜单组中的所有项目都可以单选:

android:checkableBehavior属性接受下列值其中之一:

single

组中只有一个项目可被标记(单选框)

all

所有项目可被标记(复选框)

none

没有项目可被标记

可以使用<item>元素中的android:checked属性来给项目设定默认标记状态,并在代码中用setChecked()方法来改变它。

当一个可标记项目被选中时,系统将调用特定的项目选择方法(比如onOptionsItemSelected())。在这里必须设定复选框的状态,因为复选框或是单选框不会自动改变其状态。可以用isChecked()获取项目当前状态(在用户选中之前)之后用setChecked()来设定其选中状态。例如:

如果不这样设定选中状态,项目(复选框或是单选框)的可见状态则会在用户选中时才改变。当设置了状态后,活动会保存项目的选中状态,这样之后用户打开菜单时,所设定的选中状态将是可见的。

注意:可选中菜单项目仅用于单次活动,不会再程序被销毁后保存。如果程序包含希望保存的设置,应当使用“共享偏好(Shared Preferences)”来存储数据。

快捷键

如果用户的设备有物理键盘,那么可以通过<item>元素中的android:alphabeticShortcutandroid:numericShortcut属性添加键盘字母/数字快捷键以支持对选项菜单中项目的快速选择。也可以用setAlphabeticShortcut(char)和setNumericShortcut(char)方法。快捷键对大小写敏感。

比如,如果应用“s”字符作为“保存(save)”菜单项目的字母快捷键,那么当菜单打开(或是用户按住菜单键)并且用户按下“s”键时,菜单项目“保存”就被选中。

快捷键作为一种提示被显示在菜单项目里,位于菜单项目名称之下(不过图标菜单中则是只有在用户按住菜单键时才会显示)。

注意:菜单项目的快捷键只能用于有物理键盘的设备。快捷键不能被添加到上下文菜单中的项目。

动态添加菜单意图

有时会希望菜单项目通过一个Intent来启动一个活动(无论这个活动属于自身所在的程序还是其他的程序)。当确定想要使用的意图并且有一个用以初始化该意图的特定菜单项目时,可以在恰当的项目选中方法中(比如onOptionsItemSelected()回馈方法)通过startActivity()来执行该意图。

不过,如果不确定用户的设备中包含可以处理该意图的程序的话,由于意图可能不能启动一个活动,所以就要增加一个在被调用时会生成一个无功能的菜单项目的菜单项目。为了实现这一效果,Android允许当在设备上发现可以处理该意图的活动时再向菜单动态添加菜单项目。

为了增加基于可接受一个意图的活动的菜单项目:

  1. 定义一个类别为CATEGORY_ALTERNATIVE和/或CATEGORY_SELECTED_ALTERNATIVE的意图,以及其他所需的内容。
  2. 调用Menu.addIntentOptions()。Android之后会搜索可执行该意图的程序并将其添加至菜单。

如果没有安装满足该意图的程序,就不会添加任何的菜单项目。

注意:CATEGORY_SELECTED_ALTERNATIVE用于处以屏幕上当前选中的元素。因此,只应当在当在onCreateContextMenu()创建Menu时使用。

例如:

每一个被发现提供了匹配所定义的意图的意图过滤器的活动将被添加一个菜单项目。意图过滤器的android:label的值会被作为菜单项目的标题,程序的图标则作为菜单项目的图标。addIntentOptions()方法会返回所添加的菜单项目的数量。

注意:当调用addIntentOptions()时,它会覆盖所有的由第一个参数指定的菜单组的菜单项目。

允许活动被添加至其他菜单

可以使自己的活动提供为其他程序所用的服务,这样自己的程序就可以被其他活动包含在菜单中(即前述内容的角色互换)。

为了能被包含在其他程序菜单中,需要和通常一样定义一个意图过滤器,不过必须在意图过滤器类别中包含CATEGORY_ALTERNATIVE和/或CATEGORY_SELECTED_ALTERNATIVE的值。例如:

可以在“意图和意图过滤器”文档中阅读更多关于意图过滤器的书写。

参见Note Pad范例代码以了解一个使用该技术的范例程序。

本页部分内容根据Android Open Source Project创作并共享的内容修改,并在知识共享 署名2.5许可协议中所述条款的限制下使用。

声明布局

wrap_content 使视图自动调节尺寸到内容所需的大小布局(layout)是一个活动的用户界面的结构。它定义了布局结构并包含了所有将向用户呈现的元素。可以通过两种途径声明布局:

  • 在XML中声明UI元素。Android提供了一组直观的XML词汇,它与View类及其子类(比如部件和布局所要使用的)对应。
  • 在运行时实例化布局元素。程序可以通过代码创建View和ViewGroup对象(并设置其属性)。

Android框架可灵活地使用以上一种或两种方法来声明并管理程序界面。比如,可以在XML中声明程序的默认布局,像是屏幕上显示的元素及其属性。之后可以在程序中添加代码以在运行时修改包括XML中所声明的屏幕物件的状态。

  • Eclipse的ADT插件提供了XML的布局预览——打开XML文件后选择Layout标签。
  • 还可以试着用层次查看器工具(Hierarchy View Tool)来调试布局——它显示了布局的属性值,以间距/边缘指示器来画出框架,并在通过模拟器或实机调试时渲染整个视图。
  • layoutopt工具可以快速分析布局和层次中的效率低下或其他问题。

在XML中声明UI的优势在于可以更好地分离程序显示与控制行为的代码。UI描述不属于程序代码。意味着不必修改源代码并重编译就能修改UI。比如,可以为不同屏幕方向、不同屏幕尺寸与不同语言创建多个XML布局。另外,在XML中声明布局使UI的可视化变得更为容易,也就更容易调试。因此,本文档着眼于讲授如何通过XML声明布局。如果对于在运行时实例化View对象的话,可以参考ViewGroup和View类的内容。

通常,用以声明UI元素的XML词汇和结构、类与方法的命名紧密相关,元素名称对英语类名,属性名称对应于方法。事实上,这一对应关系非常直接以至于可以猜出XML属性对应的类方法,或猜出一个给定的XML元素对应于哪一个类。不过,注意并非所有词汇都是相同的。在一些情况下,命名上有些小差异。比如,EditText元素有一个text属性,它对应的是EditText.setText()

Tip:可以在”常用Layout对象“一文中了解更多不同的布局类型。在”Hello Views“教程中有关于如何构建不同布局的一系列教程。

编写XML

使用Android的XML词汇,可以像用HTML创建网页一样通过一系列的嵌套元素快速地设计UI布局与其中包含的屏幕元素。

每一个布局文件必须包含一个View或ViewGroup对象作为根元素。一旦定义了根元素,就可以添加其他的布局对象或是部件作为其子元素,逐渐构造出能够用于定义布局的视图层次。比如,下面是一个XML布局,它使用了垂直LinearLayout并含有一个TextView和一个Button:

在XML中声明了布局之后,以.xml后缀保存该文件与Android项目的res/layout/文件夹,这样它就能被正确编译。

为了方便,UI相关的类的API参考文档列出了与类的方法对应的可用的XML属性,以及继承属性。

要了解更多关于可用的XML元素和属性以及XML文件的格式的信息,参阅“布局资源”。

稍后将讨论这里出现的每一个属性。

载入XML资源

在编译程序时,每一个XML布局文件会被编译为一个View资源。需要在该活动的onCreate()方法中使用程序代码来载入布局资源。通过调用setContentView(),以R.layout.layout_file_name的形式传递这个引用给布局资源。比如,如果XML布局被保存为main_layout.xml,那么应该像这样将它读入活动:

活动中的onCreate()回馈方法在活动被启动时被Android框架调用(参见“应用程序基础”中的关于生命周期的讨论以了解更多)。

属性

每一个View和ViewGroup对象支持其自有的XML属性。有些属性专用于某一View对象(比如,TextView支持textSize属性),但这种属性也会被扩展于这个类的View对象所继承。一些属性对所有View对象都适用,因为它们继承于根View类(比如id属性)。同时,其他的属性被看作是“布局参数”,在View对象的ViewGroup父对象中定义用以描述View对象特定的布局位置。

ID

任何View对象都有一个整数ID与之关联,以在层次树种唯一确定该View。当程序在编译时,ID作为整数被引用,不过ID却是作为字符串被分配给布局XML文件的。这是一个对所有View都适用的XML属性(由View类定义),将被经常使用。ID在XML标签中的语法格式如下:

字符串头部的@符号表明XML语法分析器应该把ID字符串的剩余部分识别为ID资源并分析与扩展它。加号(+)意味着这是一个新的资源名,必须被创建并添加至资源中(在R.java文件里)。还有其他许多Android框架提供的ID资源。在引用Android资源ID时,不需要使用加号,但必须像这样加上android包的命名空间:

由于有了android包的命名空间,现在可以引用android.R资源类中的ID,而不仅仅是本地资源类。

为了在程序中创建视图并引用它,通常采用这样的方式:

1、在布局文件中定义一个视图/部件,之后分配其一个专有的ID:

2、然后创建该视图对象的一个实例,并从布局中获取它(通常是在onCreate()方法中):

当创建一个RelativeLayout时为视图对象定义ID是很重要的。在相对布局(relative layout)中,同层次视图可以通过引用其唯一的ID来相对地定义它们之间的布局。

在整棵层次树中ID不需要是唯一的,但在被搜索的这部分中它需要是唯一的(不过由于通常会搜索整棵树,所以最好尽可能设定唯一的ID)。

布局参数

被命名为layout_XXXX的XML布局属性为View定义了其布局参数以适合于其所在的ViewGroup。

每一个ViewGroup类有一个扩展于ViewGroup.LayoutParams的嵌套类。这个子类包含了定义每一个子视图适合于视图组的尺寸和位置的正确类型。如下图所示,父视图组为每一个子视图(包括子视图组)定义了布局参数。

注意每一个LayoutParams子类都有其自有的赋值语法。每一个子元素必须定义适合于其父元素的LayoutParams,不过可以为它的子元素定义不同的参数。

所有的视图组包含有宽度和高度参数(layout_widthlayout_height),每一个试图都需要定义它们两者。许多LayoutParams还包含可选的边距等参数。

可以通过精确的单位来制定宽度和高度,不过往往不这样做。更常见的做法是用一下常量之一来设置宽度或高度:

  • wrap_content 使视图自动调节尺寸到内容所需的大小
  • fill_parent (在API Level 8中重命名为match_parent)使视图变为其父视图所允许的最大尺寸

通常,不推荐用绝对单位比如像素来定义布局的宽度和高度。改用相对度量方式比如密度无关像素单位(dp),wrap_content,或fill_parent是更好的方式,因为这样可以帮助确保程序可以在不同屏幕尺寸的设备上正确显示。可用的度量方式在“可用资源”文档中被定义。

布局位置

一个视图占有一块矩形区域。视图有一个通过一对左侧顶部坐标表示的位置,以及通过宽度和高度表示的两个维度。位置和维度的单位是像素。

可以通过调用getLeft()和getTop()方法来获得一个视图的位置。前者将返回视图所占矩形区域的左侧或者说X坐标,后者返回该矩形的顶部或者说Y坐标。这两种方法返回的位置都与其父视图相关。比如,如果getLeft()返回20,就意味着该视图处于其直接父视图左侧边缘起向右20像素的位置。

另外,有一些便利的方法可以用来避免不必要的计算,即getRight() 和getBottom()。这些方法返回视图矩形区域的右侧和底部坐标。比如,调用getRight()就类似于进行了getLeft() + getWidth()的计算。

尺寸、间隙和边距

视图的尺寸由宽度和高度表示。一个视图实际上有两组宽度和高度的数值。

第一组是测量宽度(measured width)测量高度(measured height)。它们订立了一个视图在其父视图内的大小。可以通过调用getMeasuredWidth()和getMeasuredHeight()来获取它们的值。

第二组是宽度高度,有时候也被称为绘制宽度绘制高度。这两个维度是视图在绘制和布局后在屏幕上的实际尺寸。它们的值有可能和测量宽度和高度不同。宽度和高度可以通过调用getWidth()和getHeight()获得。

为了测量一个视图的尺寸,需要考虑它的间隙(padding)。可以用像素表示视图上下左右的间隙。可以用一定像素的间隙来代替视图的内容。比如说,值为2的左侧间隙可以将视图的内容自左侧边缘向右推2个像素。可以用setPadding(int, int, int, int)方法来设定间隙,而通过getPaddingLeft()、getPaddingTop()、getPaddingRight()和getPaddingBottom()方法可以获得间隙的值。

尽管一个视图可以定义间隙,不过它不支持任何类型的边距(margin)。但视图组支持边距。参考ViewGroup和ViewGroup.MarginLayoutParams以获得进一步的信息。

关于维度的更多信息,请参见“维度的值”。

← 返回“用户界面”

本页部分内容根据Android Open Source Project创作并共享的内容修改,并在知识共享 署名2.5许可协议中所述条款的限制下使用。

Android 2.3中文简介

北京时间今天凌晨Google发布了Android最新的版本2.3。本文将以官方文档为基础对这一新版本做一个简单介绍。

Android 2.3 平台

API Level: 9

对于开发者来说,Android2.3现在已经能够作为SDK组件下载。它们包括了Android库,一些系统图片,模拟器皮肤等,不过没有包含外部库。

API 概览

接下来将对Android2.3中的新要素做技术概览,包括一些新功能和框架API的一些变化。更加具体的内容请参见Android Developer网站

  • 基于SIP的VoIP
  • 新版本包括了一个SIP协议栈以及框架API,使得开发者能够编写互联网电话应用。使用这个API的程序可以提供语音呼叫而不必处理分组管理等细节问题,它们将由Android 2.3的SIP服务来处理。具体的内容请参见Android Developer网站。

  • 近场通讯(NFC)
  • Android2.3包括了一个NFC栈以及框架API,它允许支持NFC的设备读取NDEF标签中的信息。Android2.3提供了一个底层的NFC服务,设备一旦在范围内发现一个NDEF标签,系统就会广播一个意图。

  • 螺旋传感器等
  • Android2.3开始支持一些新的传感器。螺旋仪、旋转感应、线性加速仪和重力感应等。开发者利用这个传感器可以快速流畅地识别出设备的位置和运动状态。

  • 多摄像头支持
  • 程序现在可以使用设备上的任何一个摄像头来拍照或是录制视频。

  • 可混合音效
  • Android2.3开始支持单音轨或是全局音效,包括低音加强、均衡器和回声效果等。

  • 下载管理器
  • 新版本提供了一个新的下载管理器来处理HTTP下载。程序可以要求一个URI被下载到某一文件,然后下载管理器会在后台处理这项工作,并会在下载出错、系统重启等时重试下载。

  • 严格模式(StrictMode)
  • 为了帮助开发者监视并提高程序的性能,Android2.3提供了新的系统工具StrictMode。它能捕获并通知开发者程序运行过程中意外发生的的会降低程序性能的磁盘或网络活动。开发者可以据此决定是否有必要修改这些问题。

  • UI框架
  • 滚动至底部/顶端的提示
    当滚动至视图端部的时候会有动画效果提示用户。
    触摸过滤
    可以增加对确定交易、授权允许等敏感触摸操作时的安全性。
    另外经过改良的还包括事件管理和触摸动作管理、文字选择控制、活动控制、通知文字及图表的风格和WebView。

  • 对超大尺寸屏幕的支持
  • 增加了xlarge屏幕尺寸的选项。

  • 图像
  • 增加了OpenGL ES 2.0中的 glDrawElements() 及glVertexAttribPointer()的支持。
    对VY12像素格式提供支持。

此外,这次升级还对Content Provider、Location、存储、Package管理、电话等一系列内容进行了修改和改良。限于篇幅在此不多说明,从官方网站可以得到更为详细的介绍。

而对于用户来说,Android2.3主要有以下改进:

  • 更为简洁高速的UI。
  • 更加方便快捷的文字输入。
  • 单击实现的文字选中、复制和粘贴。
  • 增强的电源管理功能。
  • 对应用程序更方便的控制。
  • 新的通讯和管理方式,包括近场通讯、网络电话、下载管理和多摄像头支持。