Android如何绘制视图

当一个Activity接收到焦点时,它将被请求绘制其布局(layout)。Android框架将处理这个绘制过程,同时Activity必须提供其布局层级的根节点。

绘制过程从布局的根节点开始。它被请求测量并绘制布局树。绘制过程通过遍历布局树并渲染每一个穿插于invalid区域View进行。每一个视图组顺次负责请求其每一个子视图进行绘制(以draw()方法),而每一个View则负责绘制自身。因为布局树是依次遍历的,这就意味着父视图将在其子视图之前(即位于其下方)被绘制,而兄弟视图根据其出现在数中的位置按顺序被绘制。

绘制布局是一个分为两条线路的过程:测量线路(measuring pass)和布局线路(layout pass)。测量线路由measure(int ,int)完成,是视图树的一次从上至下的遍历。在递归过程中每一个View沿着树向下推送它的尺寸。在测量线路的最后,每一个View就都已储存了其测量尺寸值。第二条线路在layout(int, int, int, int)中完成,它同样也是自上而下的。在这个线路中父视图负责通过在测量线路中计算得到的尺寸来定位其所有的子视图。

Android框架不允许绘制不在invalid区域内的View,同时它将负责绘制View的背景。

可以通过调用invalidate()来强制绘制一个View。

当一个View的measure()方法返回时,必须设定它与它的子孙类的getMeasuredWidth()和getMeasuredHeight()值。一个View的测量宽度和测量高度必须满足其父类的约束。这保证了在测量线路的最后,所有的父类可以符合所有子类的测量值。一个父类View可以不止一次对其子类调用measure()。例如,父类可以以未指定的尺寸来测量每一个子类以确定他们所需的尺寸,之后以实际值对其再次调用measure()来检查所有子类的未约束的尺寸是否过大或是过小。(即,如果子类和其应该占用的尺寸不符,父类将会进行干预,将规则设置为第二条线路)。【抱歉这部分翻译质量欠佳,之后会找时间考虑如何改进(其实很多部分翻译质量都需要提高,总之现在先完成第一遍再说了。渐渐开始理解那些中文版教材令人无法满意的翻译背后的翻译者了……)】

测量线路使用两个类来交流尺度。View.MeasureSpec类被View用来告诉其父类希望被测量和定位的情况。基本的LayoutParams类用来描述View所希望的高宽尺寸。对于每一个维度,可以指定以下之一:

  • 一个确切的数字
  • FILL_PARENT 表示View希望和其父类一样大(减去间隙)
  • WRAP_CONTENT 表示View希望其大小足以包含其内容(加上间隙)。

要实例化一个布局,需要调用requestLayout()。该方法也将在一个View认为自身不可能适合当前边框时自调用。

对于ViewGroup的不同子类有相对应的LayoutParams的子类。丽日,RelativeLayout有其专属的LayoutParams子类,它可以水平及垂直定位其子视图。

MeasureSpec用于将需求沿着布局树从父至子向下推送。一个MeasureSpec可以以下三种模式存在:

  • UNSPECIFIED: 这被父类用于决定某一子视图的期望尺度。例如,一个LineraLayout可以对其子类调用measure(),将高度设置为UNSPECIFIED,宽度设置为EXACTLY 240以确定该子类在被分配了240像素宽度时所需的高度是多少。
  • EXACTLY: 这被父类用于强制设置其子类的确切尺寸。子类必须使用该尺寸,且保证它的所有子嗣也都满足这个尺寸限定。
  • AT_MOST: 这被父类用语强制设置其子类的最大尺寸。子类必须保证它及其子嗣满足该尺寸。

返回“用户界面”

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

将数据与AdapterView绑定

AdapterView是ViewGroup的一个子类,其子类View由一个Adapter来决定与何种类型的数据相绑定。AdapterView在需要在布局中显示储存的数据(与资源字符串或是可绘制内容相对)时是非常有用的。

Gallery、ListView和Spinner是可以以特定方式用来与特殊类型数据绑定显示的AdapterView子类的几个例子。

AdapterView对象有两项主要任务:

  • 用数据填充布局
  • 处理用户选择

用数据填充布局

通过把AdapterView类与一个Adapter绑定可以将数据插入布局之中,Adapter会从外部资源获取数据(比如在代码中提供的一个列表或是来自设备的数据库中的一个查询结果)。

下面的范例代码的功能是:

  1. 借助于一个已有的View创建一个Spinner并将其绑定至一个新的ArrarAdapter来从本地资源中读取一列色彩。
  2. 在一个View中创建另一个Spinner并将其绑定至一个新的SimpleCursorAdapter来从设备通讯录中读取联系人姓名(参见Contacts.People)。

注意必须让People._ID栏通过CursorAdapter投影,不然会收到一个异常。

如果在程序的生命周期内改变了Adapter所读取的数据,那就该调用notifyDataSetChanged()。这将告诉被绑定的View数据已被改变以刷新显示。

处理用户选择

可以通过将类的AdapterView.OnItemClickListener成员与一个监听器(listener)相结合并捕获选择动作来处理用户选择。

关于如何创建不同类型的AdapterView的更多内容,请阅读下面的教程:Hello Spinner、Hello ListView以及Hello GridView。

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

Android中如何自动弹出软键盘

代码与范例:

说明:同样是在写Techo Lite时遇到的问题。希望程序可以自动弹出软键盘,可是尝试了各种办法都没有效果。问题的原因似乎是必须等待UI绘制完成后弹出软键盘的代码才会有效。于是就采用了上面的方法。需要注意的是,在此之前必须让edittext获取焦点,不然也是无效的。

Android中EditText如何定位光标位置

代码:edittext.setSelection(int);

范例:

说明:在写Techo Lite时遇到的问题。找了一下发现EditText控件提供了这样一个函数以设置光标所在位置。

进程和线程

在一个程序的仅有的一个应用程序组件启动时(除此之外没有其他组件在运行),Android系统会为该程序开启一个包含一个单独执行线程的新的Linux进程。默认来说,同一个程序的所有组件都运行于同一个进程的同一个线程中(被称为“主”线程(main thread))。如果一个组件在启动时该程序已经有一个进程存在的话(这是因为该程序存在有其他的组件),那么组件将运行于这个进程中且共用线程。不过,可以安排程序中的不同组件运行于不同的线程,以及为任何进程创建新的线程。

本文档将讨论Android应用程序中的进程和线程是如何工作的。

进程

默认地,同一个应用程序的所有组件都运行于同一个进程,大部分的程序不应该改变这一点。不过,如果认为有必要控制某个特定的组件所属的进程,可以在manifest文件中进行设定。

每一种类型的组件元素——<activity>、<service>、<receiver>和<provider>的manifest条目都支持android:process属性来指定组件应当运行于哪一个进程之中。可以设置该属性以使组件运行于其自有的进程或让一些组件共用进程的同时另一些使用独立进程。还可以通过设置android:process来事不同程序的组件运行于统一进程——假设这些程序共享同样的Linux用户ID并被相同的权限标记。

<application>元素也有android:process属性,用来给所有的组件设定默认值。

Android在某些时候(比如内存不足而用户又马上需要更多的内存来运行其他的进程)会做出关闭某个进程的决定。运行于该进程的应用程序组件也就相应地会被销毁。当再次需要它们时将会重启一个进程。

在决定终止哪一个进程时,Android会权衡它们对于用户的重要性。比如,系统更有可能关闭一个活动在屏幕上不可见的进程而不是一个活动仍然可见的进程。因此,是否终止一个进程取决于进程中组件的运行状态。具体决定终止哪一个进程的规则将在之后讨论。

进程生命周期

Android系统总是试图尽可能长时间地保持一个程序进程,不过旧的进程最终将被移除来为更为重要的进程提供内存。为了决定保留哪个进程,杀除哪个进程,系统将每一个进程置于一个基于进程内运行的组件及其状态的“重要性层级”之中。最低重要性的进程将被最先去除,之后是次低重要性的,以此类推直至能够提供足够的系统资源。

重要性层级中有五个等级。下面根据重要性顺序列出了不同类型的进程(第一种进程是最为重要的,将被最后杀除):

1. 前台进程

一个用户当前正在使用的进程。如果以下任一条件被满足,进程就被划归为该类型:

  • 它包含一个用户正在进行交互操作的活动(该活动的onResume()方法已被返回)。
  • 它包含一个与用户正在进行交互操作的的活动相绑定的服务。
  • 它包含一个“在前台运行的”服务——服务调用了startForeground()。
  • 它包含了一个正在执行其生命周期回馈方法之一(onCreate(),onStart()或onDestroy())的服务。
  • 它包含了一个正在执行其onReceive()方法的广播接收者。

通常,在某一时刻只会存在少数的前台进程。它们将被最后杀除——在内存极端不足的情况下。通常此时设备处于内存分页状态,需要杀除前台进程来保持用户界面响应。

2. 可见进程

这类进程没有任何前台组件,但是可以影响用户在屏幕上看到的内容。如果以下任一条件被满足,进程就被划归为可见:

  • 它包含一个不在前台但是对用户可见的活动(其onPause()方法已被调用)。比如,前台活动是一个对话框,而前一个活动仍可以被看到存在于其后方的情况。
  • 它包含一个与可见活动(或前台活动)相绑定的服务。

3. 服务进程

这是一类运行着由startService()方法启动的服务但不属于前两种进程的进程。尽管服务进程不与任何用户直接看到的内容绑定,它们确实在执行用户所希望的工作(比如背景播放mp3或是从网络下载数据),因此系统只有在亟需内存维持所有的前台和可见进程时才会杀除他们。

4. 后台进程

这是一类包含一个当前不对用户可见的活动(活动的onStop()方法已被调用)。这些进程对用户体验没有直接影响,当前台、可见或服务进程需要内存时它们会被随时杀除。通常有着大量的后台进程在运行,它们被保存在一个LRU(Least recently used)表中以确保最后看到的活动所在的进程会被最后杀除。如果一个活动正确地执行其生命周期方法并保存其状态,杀死它的进程不会影响用户体验。这是因为当用户返回活动时,所有的可见状态将被恢复。参见“活动(Activities)”文档以了解关于保存与恢复状态的更多信息。

5. 空进程

这是一类没有任何活动的(active)应用程序组件的进程。保持这样一个进程的唯一理由是作为缓存来提高下一次有组件要在其内运行时的启动速度。系统为了在进程缓存和底层核心缓存间平衡整体系统资源而常常杀除这些空进程。

Android根据进程中当前活动着的组件的重要性来尽可能高地给一个进程定级别。比如,如果一个进程包含一个服务和一个可见活动,那么进程会被分级为可见进程而不是服务进程。

另外,进程的层级可能会因为其他进程依赖于它而被提升。服务于另一个进程的进程不可能比那个进程的层级低。比如,进程A中的一个内容提供者服务于进程B中的一个客户端,又或是进程A中的一个服务与进程B中的一个组件绑定,那么进程A被认为至少要和进程B同样重要。

由于运行服务的进程比运行后台活动的进程层级高,一个执行长时间操作的活动应该开启一个服务来处理该工作而不是简单地创建一个工作线程——特别是该操作可能比活动本身持续更久时。这样的例子有个后台播放音乐或是上传一张拍摄的照片到网上。使用服务至少可以确保该操作有服务进程的优先级,而不必在意活动本身怎样。正如之前在广播接收者生命周期一节中提到的,这也是为什么广播接收者应当用服务而不是用线程来执行花费时间的工作的原因。

线程

当一个程序被启动后,系统将创建一个被称为“主线程(main)”线程来执行该程序。这个线程非常重要,它负责正确地向UI控件分发事件,包括绘图事件。它同时还与Android UI toolkit中的组件(即android.widget和android.view包中的组件)进行交互。因此,主线程有时也被称作为UI线程。

系统不会为每一个组件的实例创建新的线程。所有运行于同一进程的组件都在UI线程中被实例化,系统对它们的调用也都是从该线程中分发。因此,要响应系统回馈的方法(比如回馈用户操作的onKeyDown()或是生命周期回馈方法)总是要运行于进程的UI线程中。

例如,当用户点击屏幕上的一个按钮,程序的UI线程将分发触摸事件至控件,控件将设置其点击状态并向事件队列发送invalidate请求。UI线程会将此请求出列并通知控件进行重绘。

当程序在响应用户交互操作中需要处理密集型工作时,这样的单线程模式可能会在没有很好的设计程序的情况下降低程序表现。特别是如果所有的任务都由UI线程执行,网络连接或是数据请求等长时间操作将会阻塞整个UI。当线程被阻塞,事件就无法被分发,包括绘图事件。从用户的角度来看,程序似乎就停止响应了。更糟糕的情况下,如果UI线程被阻塞的时间过长(目前来说大约是5秒)用户就会看到糟糕的“程序没有相应”(application not responding,ANR)对话框。用户之后就会考虑退出程序,甚至不满地卸载它。

另外,Android UI toolkit不是线程安全的。因此,不能在工作线程中进行UI处理——必须将所有用户交互操作在UI线程中实行。于是,对于Android的单线程模式有两条规则:

  1. 不要阻塞UI线程
  2. 不要在UI线程外读取Android UI toolkit

工作线程

由上面所描述的单线程模式可知,不阻塞UI线程对于程序UI的响应性是很重要的。如果不得不执行无法立即完成的工作,就必须确保它们处于不同的线程之中(“后台”或“工作”线程)。

例如,下面是一个点击监听器(click listener)的代码,它将从另一个线程中下载一张图片并将其显示在一个ImageView中:

最初这看起来能很好的运行,因为它创建了一个新的线程来处理网络操作。不过,它违反了单线程模式的第二条规则:不要在UI线程外读取Android UI toolkit——这个范例在工作线程中修改了ImageView而不是在UI线程中。这可能导致未定义的不被希望的行为,并且可能要花费很长时间来解决这个问题。

要解决这个问题,Android提供了几种不同的方法来从其他线程中接入UI线程。这里列出了有用的一些方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

例如,可以用View.post(Runnable)方法来解决上面的问题:

现在代码是线程安全的了:网络操作在另一个线程中完成,同时ImageView是由UI线程处理的。

不过,当操作的复杂性提升时,这样的代码会变得很复杂而难以维护。要处理更为复杂的与工作线程的交互,应该考虑在工作线程中使用Handler,来处理接收自UI线程的消息。不过,最好的解决方案或许是继承一个AsyncTask类,来简化工作线程与UI间交互任务的执行。

使用AsyncTask

AsyncTask允许在用户界面中执行异步工作。它在一个工作线程中执行阻塞性操作并把结果发布于UI线程而不需要处理线程关系。

要使用它,必须继承AsyncTask并使用doInBackground()回馈方法(它运行于后台线程池中)。要更新UI,需要使用onPostExecute()以传递来自doInBackground()的结果来安全地更新UI。之后可以通过在调用execute()来运行UI线程中的任务。

例如,可以像这样使用AsyncTask来实现上一个范例:

现在不但UI是安全的,代码也变得更简单了,因为工作被分成了需要在工作线程中完成的部分和应当在UI线程中完成的部分。

应该阅读AsyncTask的参考文档来了解关于如何使用这个类的完整信息,不过这里有关于它工作方式的简介:

  • 可以使用generic来指定参数类型,进度值(progress value)和任务的最终值
  • doInBackground()方法自动在工作线程中执行
  • onPreExecute()、onPostExecute()和onProgressUpdate()都在UI线程中
  • onPreExecute()所返回的值被发送至onPostExecute()
  • 可以在onInBackground()执行的任意时刻调用publishProgress()以在UI线程中执行onProgressUpdate()
  • 可以在任一时刻,在任意线程中,取消任务

注意:在使用工作线程时可能遇到的另一个问题是由于运行配置的改变而活动自动重启(比如用户改变了屏幕方向),这可能会销毁该工作线程。要防止任务在这种情况下重启,并在活动被销毁时正确地取消任务,请参见Shelves范例程序的源代码。

线程安全方法(Thread-safe methods)

在某些情况下,所使用的方法可能会被不止一个线程调用,因此这些方法必须是线程安全的。

对于可以被远程调用的方法——比如在绑定的服务中的那些方法——来说,这是非常必要的。当在一个IBinder所运行的进程中调用一个在该IBinder中使用的方法时,这个方法会在调用者的线程中被执行。然而,如果这个调用是在另一个进程中的,那么这个方法将在从线程池中选择的一个系统保留的与IBinder处于同一进程的线程中执行(并不是在进程的UI线程中被执行)。例如,尽管一个服务的onBind()方法会被在服务的进程的UI线程中调用,对象中onBind()所返回的方法(比如,使用了RPC方法的一个子类)会在线程池中的线程中被调用。因为一个服务有不止一个客户端,所以多个池线程可以同时和同一个IBinder方法相关联。因此IBinder()方法必须是线程安全的。

线程间通信(Interprocess Communication,IPC)

Android提供了一种使用远程过程调用(remote procedure call,RPC)的进程间通信(IPC)机制,使得一个被某一活动或其他应用程序组件调用的方法将被(于另一进程中)远程执行,而所有的结果将被返回给调用者。这将会分解一个方法的调用及其数据至操作系统可以理解的级别,将其从本地进程及地址空间传输至远程进程及地址空间,然后在远程进程中重新组装激活这个调用。之后返回值将被反向传输。Android提供了所有执行这样的IPC事务所需的代码,因此只需关注如何定义和使用RPC编程接口即可。

要进行IPC,程序必须通过bindService()和一个服务相绑定。更多信息请参阅“服务开发者指南”。

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

Android用记事本程序 Techo Lite

虽然标题中用的是“记事本”,不过更确切的说,应该是一个简单的便筏程序。

Android平台上各类记事本程序已经数不胜数了,自己写这样一个程序的动机纯粹是满足自己的需要。和其他同类程序相比,Techo Lite的特点在于:只能保存一条记录,因而进入程序后直接可以编辑。也就是说,比其他类似程序要少一个选择要编辑的文件的步骤。

这一点对我来说很重要。我平时常常会需要用手机记录一些简单的文本,因此希望程序可以尽可能快地进入编辑模式。此外我一般只是用作短时记忆用,所以也并不需要能保存多条记录。万一遇到要记录几条不同的内容,将它们都记录在一条中也不会有太大的问题。之前也用过各种各样的程序。Color Note的问题在于进入程序后仍然要点击两次才能进入编辑模式,而且两次点击还需要移动手指位置,不是特别人性化。而另一款程序AK Notepad在进入程序后只需要再点击一次,选择要编辑的文件后即可开始编辑(如果使用桌面插件也可以实现一次点击即进入编辑)。可惜最新升级的版本不知怎么在自己的手机上经常会崩溃,于是只好暂时放弃使用。

由于以上原因,自己决定动手写一个简单的便筏程序供自己使用。程序本身相当简单。进入程序后即可编辑文本内容,退出程序后内容就会被自动保存。另外还有清空已输入内容和更换背景颜色这两个功能,除此之外就没有其他多余的功能了。事实上,设计程序的图标和考虑中日英的程序简介文本所花的时间比程序本身所花的时间更长。程序名称中的Lite有两个意义。其一是指明这个程序的功能将会相当简单,其二是因为我另外还有编写一个全功能记事本的计划,会命名为Techo。它将无缝集成文本和涂鸦两种输入模式并提供文件输出等一系列功能。不过这个计划的优先级不是很高……

总结来说,相比现有的记事本程序,Techo Lite具有以下特点:

  • 操作快捷。由简洁的设计理念,使得Techo Lite不但程序结构简单,没有冗余的操作步骤,而且事实上的反应速度也相当迅速。
  • 功能精简。完全没有同步等文本编辑之外的多余功能。没有繁琐的设置选项。这在一定程度上也保证了Techo Lite基本不存在Bug,且与任何终端都保持了高度的兼容性。
  • 可更换背景色。并不是所有同类程序都可以更换背景色。Techo Lite虽然简单,但是仍提供了多种背景色可供选择,以满足不同的人的喜好。

目前Techo Lite可以在Android Market的效率类应用程序中找到,搜索Techo或是pub:”BREEZE”即可。如果正在寻找一款将“减法法则”发挥到极致的记事本程序,不妨试试看Techo Lite吧。

Android Market页面:

https://market.android.com/details?id=org.breezesoft.techolite

最后是几张Techo Lite的截图:

索尼终于发表平板电脑

在今天召开的Sony IT Mobile Meeting上,索尼正式发表了两款平板电脑。

图像版权属于Sony Corporation

目前已知情报的一个整理:

左侧的平板型代号为S1。搭载了9.4寸屏幕,分辨率1280×800。采用独特设计以适合单手把持。

右侧的折叠型代号为S2。配有两块5.5寸屏幕,分辨率1024×480×2。上下屏幕可以分别执行不同任务。

两者都搭载了PlayStation Suite,运行于Android 3.0系统。

软硬件都经过了优化,而具有高速流畅的响应速度。同时网络娱乐内容丰富,支持跨设备连接等特性。

更为详细的内容还有待公布。

关于Sony IT Mobile Meeting的更多信息请参见(日文):

http://www.sony.co.jp/united/simm/?fm=prx000000

服务

服务是一种可以在后台执行长期运行操作而不提供用户界面的应用程序组件。另外的某一个应用程序组件可以启动一个服务,之后该服务会一直运行,即使用户切换到了另外一个程序。此外,一个组件可以与服务绑定,与之交互,甚至进行进程间通信(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许可协议进行许可。