Activity and Task设计指导
这篇文档主要讲述Android应用框架的核心原则。站在高层来说,以用户为中心来设计良好的交互程序,对于应用设计者或是开发者来说是非常重要的。
下面用例子来阐述了activities和tasks的一些底层原则和机制 ,例如导航,多任务,activity重用,意图和activity栈。这篇文档也着重讨论了一些设计决策,针对如何利用好它们去设计你的应用程序UI。
这篇文档中的例子均是Android应用程序,包括默认应用程序(比如拨号器)、Google应用程序(比如地图)。你可以自己在Android模拟器上或是Android手机上去试验这些例子。如果你使用Android手机试验时,可能未提供本文档中的某些例子。
再看这篇文档之前,请确保您看过本篇中的Design Tips章节。此篇算做是Application Fundamentals文档的的一部分(特指Tasks and Back Stack章节),它对于程序员来说覆盖了整个底层机制。
Applications, Activities, Activity Stack and Tasks
理解Android系统中的四个基本概念对你是很有帮助的,它们分别是:
* Tasks
Applications
一个Android应用程序其实就是由一个或者多个Activity组成。它们被捆绑在一起并存放进.apk文件中,这就是Android应用程序。Android中有着丰富的应用程序,比如邮件、日历、地图定位、文本消息、联系人、照相机、打电话、音乐播放器,系统设置等应用。
一般情况下,桌面上都会有Android应用程序快捷图标,用户可以选择某一个图标来启动应用程序。
Activity是Android应用程序的主要组成部分,activity可以是你自己创建的,当然,还可以是其它应用程序中的activity。它们都是在运行时绑定上的,以便于应用程序扩展其自身的功能,它们一旦组合在一起,就会如同一个应用程序一样。每个activity都有其独特的UI、明确的功能,诸如打电话、看照片、听音乐等。任何一个应用程序都应至少有一个activity。
当使用Android手机时,用户在屏幕上一个接一个地滑动或是点击图标启动activity,完全不会注意到底层的行为——他们体验是无缝的。activity后面有activity,task后面还会有task。
Activity可以处理特定的数据类型和接受一相关的动作。每个activity都有其各自的生命周期,互不干扰;并且它们都可以被用户或者系统独立启动、运行、暂停、恢复运行、停止、重新开始。正因为这个独立性,activity可以以不同的方式被其它的activity进行替换。
比如打电话的应用程序就包含了四个activity:打电话、联系人列表、查看联系人、添加联系人,如下图:
打电话 联系人列表 查看联系人 添加联系人
下面的应用程序同样也包含了很多activity:
* 邮件 - 查看文件夹、邮件列表、邮件,发送邮件和设置邮件账号。
* 日历 - 查看天、星期、月、议程,编辑事件、首选项。
* 照相机 - 运行照相机、查看图片列表、图片,编辑图片,运行录像机,查看录像列表和录像。
* 游戏 - 玩游戏和安装游戏。
* 地图 - 查看地图上的位置,查看朋友的位置以及他们的详细信息(朋友的位置、状态、照片)。
Activity是Android应用中最为突出的组件,其余组件分别为:service、content provider、broadcast receiver。更多activities的详情,参见Application Components。
Activity Stack
用户之所以能够从一个activity转到下一个activity,是因为Android系统针对activity而设计了一个线性的导航历史以供用户追溯访问,这就是activity栈,也称为back stack。当用户启动了一个新的activity,它就被添加进activity栈,以便按BACK键时能够返回到上一个activity。然而,用户不能按BACK键就直接返回到桌面(除非activity的前一个是桌面才可以)。
activity栈里面存放的只能是activity,而视图、窗体、菜单和对话框则不能。也就是说,如果你可以让用户从屏幕A跳转到屏幕B,当用户按BACK键时,他就应该会回到屏幕A,那屏幕A必须是一个activity。有个例外情况就是,你的应用程序需要利用BACK键控制自身的导航,那就要自己重新设定BACK键的导航功能。
任务则是一系列的activity集合,它能使用户完成既定的操作,而又不用去关心这些activity是哪个应用程序里面的,除明确指定一个新任务之外(参见“中断任务”小段),那么其他activity都属于当前任务的一部分。再次注意的是,这些activity可是任意应用程序中的其中一个,也就是说不管它们所属的应用程序是否相同。举个例子,用户打开了联系人的程序,任务随之也会启动,他选择了email地址准备发邮件,这时跳转到了email activity,之后他要添加附件,需要在画廊中挑选图片。这里面,联系人,email,图片画廊都是不同的应用程序。
当一个activity启动时,任务也随之启动的话,那个activity就是根activity。启动activity一般有这么几种方式,应用程序发射器、桌面快捷方式、最近任务切换器。Android系统内部一旦有任务,那么按BACK键就可以回到上一个activity。Activity栈可以是多个任务的组成部分。
下面是关于任务的例子,以供参考:
* 发送文本消息并含有附件
* 观看YouTube视频并以邮件的方式向其他人分享。
中断任务 ——任务中有一个重要特性就是能使用户中断当前正在做的操作(他们的任务)而去执行其它操作,当然他们也可以回到之前的任务上,也就是说支持同时运行多任务并且来回切换它们。
这里有两种情况来开始其它任务,并且都可以返回到原先的任务上。
* 打开通知:用户接收到通知并打开查看它。
* 用户转去做其它操作:用户在桌面启动。
当然,也有例外的。除了刚才提到的两种方式,另外还有一种打开新任务的情况,就在其内部开启一个activity。例如,在邮件中以新任务的方式打开地图activity或是打开一个浏览器activity,当按BACK键时就又会回到邮件activity中。
Activities和Tasks之旅
下面的例子阐述了应用程序的基本原则,主要有activities,activity栈,回退键,任务和意图;并展示了系统是如何响应用户请求的,例如用户开始了一个应用程序,用户不断的切换UI,程序内部就是利用在不同任务之间切换activities的。下面的许多例子你都可以在Android手机上运行起来。
在桌面上开始一个Activity
桌面是启动应用程序的主要地方,比如在桌面上点击应用程序图标就能将其打开,用户第一眼看到的就是应用程序中的主activity。如下图,所描述的是用户在桌面点击Email图标所发生的事情:
用BACK和HOME键进行导航
Activity保持或者丢掉其状态完全取决于用户是怎样离开这个activity的——使用HOME键还是BACK键。
默认情况下,按下BACK键来结束(销毁)当前activity并为用户显示上一个activity。例如下图,用户在桌面上打开了邮件,当前activity显示着邮件列表。用户将列表滚动条往下拉以便看到后面的邮件,这时用户按了BACK键,那么Android就会销毁这个邮件信息列表activity并返回之前的activity(桌面)。然后用户又重新打开邮件,还是那个列表,但是滚动条又回到了起始位置上。
上面的例子中,当按下BACK键就返回到了桌面,由于那是用户在上一次看到的activity。不过如果用户从其他activity里面跳转到邮件列表,然后按下BACK键则回到了先前的那个activity(这里只是说明一下BACK键的作用)。
相比之下,下面的图就是用HOME键离开邮件列表activity而不是BACK键,那么当前activity就呈stop状态并移置后台而不是销毁。当再次打开邮件列表activity时状态保持不变。
其它情况:有些应用程序则不是如上面所说的那样。例如联系人和画廊,用户在桌面打开联系人后查看了某个联系人的资料,接着再次打开联系人时,就不会显示之前的activity了。这是因为联系人的主activity有四个标签,是为了让用户能够看到全部的功能特性。
此外,也不是所有activity都是当按下BACK键之后销毁掉的。例如用户开始播放音乐,接着按下BACK键,却不会影响音乐的播放。即使它的activity不再可见,音乐应用程序依然会在状态栏上提示着用户。注意:你也可以让activity不再可见时停止掉或是继续在后台运行,但后者更适合像音乐这样的应用程序。
重用activity
有两个应用程序中,它们分别也有两个activity:activity A 和activity B。A的部分功能需要调用B的已实现功能,那么B就叫被重用。
联系人重用画廊来获取图片 — 联系人activity中会有联系人的照片,但是照片一般存放在画廊里面,所以联系人要重用画廊的功能来获取图片,画廊activity就是重用的绝佳例子。下面的图画出了重用的流程。具体流程是这样的:用户打开了联系人,查看某一个联系人的资料并想编辑他的照片,这时,打开了画廊activity,对图片进行设置并保存,那个联系人的图片也就相应的改变了。
注意画廊最终会返回给联系人一张图片。 下一个例子讲述一个activity的重用并且不返回任何结果。同样需要注意下面的插图是说明通过activity或是activity栈来实现历史导航——用户可以通过每个activity用任何方式回到桌面。
当开始设计一个应用程序时,一个不错的想法就是怎样能够在重用其它应用程序中的activity或是你的activity怎样被其它应用程序重用。如果用一样的intent filter(已经存在了一个activity)再添加一个activity,那么系统会为用户显示出一个选择UI,供用户选择使用那个activity。
画廊重用短信来与其他人分享图片。分享也是不同应用程序之前重用的好例子。如下图所示,用户打开了画廊,从中挑选了一张图片并点击了共享菜单,选择“短信”。这时,就打开了短信activity,在其里面写些文字和附加上那张图片之后发送出去。用户现在在短信activity当中,如果想回到画廊activity,就按BACK键返回。
注意这里的短信activity并没有给画廊返回任何的东西。
这些例子都在阐述任务——一系列的activities都在实现同一个目标。每一个例子中的activity都是从两个不同的应用程序中完成本职工作的。
替换activity
这个例子描述的是不用应用程序中的两个activity互相替换,activity A替换activity B。这种一般发生在activity A比activity B的功能更为强大一些。
换句话来说,A和B妥妥得等价,当然就可以实现A替换B。这个例子中的联系人应用程序重用了activity,A和B虽然是完全不同的activity,但是它们两个彼此形成了互补,使程序更加的强大。
在这里例子中,用户下载了一个手机铃声的activity,称之为“铃声扩展”。用户这时进入到“设置 > 声音&显示 > 手机铃声”里面,系统会展示两个可用activity供用户选择。此时弹出的对话有一个选项是让你设置“是否默认使用此activity”,选中它。当用户选择“铃声扩”时,以后在加载的时候就替换了Android默认铃声的activity了。
多任务
如前所述,当一个activity启动后,用户还可以回到桌面启动第二个activity,第一个activity则不会被销毁还是继续运行着,我们换个例子来说明这一小节——地图应用程序。
* 状态1:用户打开了地图应用程序并查询一个地址。这时,用户该说了,网络太TMD慢了!因为地图定位是需要一些时间的。
* 状态2:用户准备做些其它事情,按下HOME键,不过这样做不会干扰地图应用程序,还是保持其加载地图的状态。
* 状态3:地图activity现在是在后台运行着,桌面在来到了前台。这时用户打开了日历activity,比如查看今天是星期几。
* 状态4:用户回到桌面,重新打开Map,这时地图已经全部加载完毕了。
以上两个应用程序“地图”和“日历”是两个不同的任务,因此Android支持多任务模式。
两个入口点
相对于应用程序来说,必须至少要有一个入口点,也就是至少要有一个activity。桌面上的图标就代表着每个应用程序的入口点,同样也可以在其它应用程序中启动,当然,它们的入口点都在其内部。
而电话应用程序就有两个入口点:联系人和打电话。用户进入到联系人里面选择了一个电话号码并拨打该电话。如下图的图所示,用户打开联系人,也就是启动了联系人的activity,然后选择了一个电话号码随之进入了打电话的activity,最后拨打它。
一旦用户在应用程序里,它们就可以通过标签、菜单项、列表项、屏幕上的按钮或其他用户界面访问诸如新增联系人和编辑联系人。
意图(Intents)
用户点击一个mailto:的连接时,这实际上就被看作是一个意图,发邮件的意图。
关于意图有三点要说明:
* 如果是显式意图,Android就会立即启动那个activity。
* 如果是隐式意图,Android先去intent filter寻找合适的activity再启动。
* 如果有多个合适的意图,Android就会列出一个意图选择列表供用户选择。
下面就举用户发邮件的例子,此时用户的Android上有两个邮件应用程序,当他在页面点击了mailto:链接的时候,Android会提示给他一个对话框,其中有两个可用的程序供其选择(Gmail和Email)。
下面列举一些常用的意图和其对应的activity:
* 查看联系人列表:对应联系人列表查看activity
* 查看指定的联系人:对应联系人查看activity
* 编辑指定的联系人:对应联系人编辑activity
* 发邮件:对应邮件activity
* 拨打电话:对应电话拨打activity
* 查看图片列表:对应图片列表查看activity
* 查看指定的图片:对应图片查看activity
* 裁剪指定的图片:对应图片裁剪activity
意图必须由两部分构成:动作和数据。
* 动作:由上面的意图列表中可得出,查看、编辑、打电话、裁剪
* 数据:由上面的意图列表中可得出,联系人的列表、指定的联系人、电话号码、图片列表、指定的图片。
注意:任何在桌面上启动的应用程序都是显式意图,目的是指定其内部特有的那个activity。同理,应用程序也可以在内部以显式意图的方式启动自身的activity,外部activity都是访问不到它们的。
关于意图更多信息,参见Intents and Intent Filters。
切换任务
下面的例子描述的是用户如何在两个任务之间进行切换。
1. 开始第一个任务。你想要发送一条短消息并附加一张图片。你会这样操作:
桌面 > 短消息 > 新的短信息 > 菜单 > 附件 > 图片。最后一步启动了画廊activity来选择一张照片。注意画廊是另外的一个应用程序。
在选择照片之前,可以先去桌面打开日历,目的是为了开始第二个任务。
2. 开始第二个任务。你会这样操作:桌面 > 查看日历。从桌面上打开日历,就等于是开始一个新任务了。
3. 切换到第一个任务并完成后面的操作。
查看完日历之后,继续回到先前的任务上:桌面 > 短消息,此时进入的并不是短消息activity,而是画廊activity,也就是之前离开的activity。然后你就可以选择图片并发送短消息出去了,也就完成了第一个任务。
设计小贴士
下面的提示和指导都是针对应用设计者和开发者而提出的。
使用显式意图来防止外部应用调用你的activity
如果你不想自己的activity被外部使用,就别在manifest.xml里面配置intent-filter。这样的话,你的activity就只会在应用程序内部来启动了,同样也避免了安全漏洞。反之,创建一个意图并指定明确要启动的组件,这就是显示意图,在这个例子中,就不需要intent filters。Intent filters可以发布所有的应用程序,当你创建了一个intent filter时,其它应用程序就可以访问到你的activity了,至于它们怎么用,你就不知道了,这意味着不经意间形成了安全隐患。
如果使用外部的activity,但却没有匹配上,该怎么办?
有这一种情况,你利用Intent去调用外部应用程序中的activity,但遗憾的是,那个应用程序并没有安装进手机里,因此我们需要妥善的处理这种情况。
(译者注:官方提出了两个不太完善的解决方案,我们来看下:)
1. 在启动那个activity之前用intent先对其测试一下。
2. 如果启动activity会失败的话,则捕获它的异常信息。
以上更多信息请参阅官方文档提供的博客文章:Can I use this Intent?。
该博文中提供了一种比较好的解决方式,正如其提供的样例代码中的isIntentAvailable()方法,我们可以在初始化阶段调用它;如果该应用不存在的话,我们就给用户提示一条消息,告诉他某某应用不存在,请去Google Market下载等友好信息。如果要让意图决定显示哪个activity,那我们就使用startActivity()或是startActivityForResult()来启动activity。
思考:以怎样的方式来启动activity
做为Android设计者或开发者,完全取决于用户如何启动你的应用程序,而应用程序则是由一系列的activity组成,用户会从Home或是其他应用程序中启动这些activity。
* 在桌面点icon来启动应用程序主activity
如果你的应用程序是独立运行的,它应该是用户在屏幕上触摸应用程序的icon或是任务选择器当中来启动(这个机制需要在manifest.xml中配置intent filter,action为MAIN,category为LAUNCHER)。
* 在其它应用程序中启动你的activity
这种方式就意味着你的activity是可重用的,也就是隐式意图。许多应用程序中的数据都需要共享给其它用户的,例如,email、文本消息、上传下载等。
还会有一种情况是这样,就是当用户选择了一个功能,正好有一个或多个activity符合用户的这种需求,就会向用户提供一个activity列表供其选择。举一个具体的例子,Gallery(画廊),它能让用户查看并共享图片,这时用户选择了“共享”菜单,Android系统会在intent filter中寻找适合该请求的activity,如果有多个,就会以列表的形式展现给用户,供其选择。在这个例子中,intent filter能找到Email、Gmail、Messaging、Picasa等。
当其它的activity启动了你的activity时,会根据需求给它们返回一个结果。
> 启动一个activity并需要返回一个结果
官方称这种方式为closed loop,也就是说当启动一个activity之后,会返回一个结果回来。再拿上面那个例子来说,当用户完成上传或者发送的过程之后,会将图片信息返回给Gallery。这个例子中的上传过程所用到的activity就是由外部的Gallery启动的。(这种方式需使用startActivityForResult())
> 启动一个activity不需要返回结果
官方称这种方式为open-ended。举个例子,在Email中可定位一个住址,那么应用程序便会启动地图activity来定位地址,完成之后不会再给Email返回任何的结果;此时用户可以按BACK键回到Email中来。(这种方式需使用startActivity())
* 只从其他应用程序中启动activity -前面所说的例子,Gmail、消息、Picasa(在Gallery启动)都是activity,它们均从桌面上的icon来启动的,与之形成对比的是,像裁剪图片和添加附件则不是在桌面启动的,因为这些都不是独立运行的。
实际上,并非所有的应用程序都有icon以供启动,它们都算作是一种小小的应用而已,因为它们使用并不频繁,而且其启动点都嵌在已有的应用程度当中。例如,Android手机里面的打电话程序,其内部有个铃声设置功能,它存在于Android手机里面的设置(Setting)菜单里面,你也可以使用同样的Intent开发出一个定制的铃声设置应用,这样,在用户需要改变铃声时,会向其展示出两个铃声设置应用,一个是Android内置的,另一个就是你开发出来的。如下图:
铃声设置并不是经常使用,而且其定义的功能也很明确,所以也就不需要在桌面提供应用程序icon了。
* 不同的图标能够启动多个相同的应用程序 -由于Android应用程序的运行代码均存在于.apk文件中,因此就把这个文件看作是一个应用程序。我们甚至还可以让其内部存在两个主activity,也就是两个应用程序启动入口点。
Camera.apk(照相机)就是一个非常好的例子,它内部就含有两个独立的主activity,Camera和Camcorder(摄像机);它们均拥有自己的icon并且独立运行;在用户角度上来看,这就是两个应用程序。它们都共享使用一个镜头、在Gallery里面保存图片等。
实现这样的功能其实很简单,只需将它们都关联到不同的任务上即可。(每个activity都其各自的任务,每个任务都有不同的亲缘性(affinity)。(这个例子的两个应用它们所在的两个包是"com.android.camera"和"com.android.videocamera",有兴趣的可以深入研究)。
联系人和拨号器也是同一个应用两个主activity的典型例子。
* 应用程序部件 - 我们也可以将应用程序以部件的形式嵌进桌面上或是其它应用程序中并能它们持续更新。
允许你的activity添加到当前任务中
如果你的activity是在外部应用程序已经启动的话,那么也允许把它们添加到当前的任务中来(或者是已存在的任务——它有自身的affinity),这样做的话能会使用户能够在其它任务和你的activity之间进行自由切换。但不包括你的activity仅有一个实例的情况。
对于这种行为,你的activity应该有一个standard 或singleTop的启动模式,而不是singleTask或singleInstance,这样,你的activity就会以多实例的模式来运行。
通知,应该能让用户更容易的返回上一个activity
利用后台运行的服务能够给用户发出他们感兴趣的事件消息。下面举个例子,主要是以Calendar为主,这个例子含两部分,一个是以Email的形式发出即将来临的消息,另一个是当有新消息时就发出通知。
我们来模拟一个应用场景,当某个用户处于activity A中,这时获取到了activity B发出的通知,他打开了这个通知,也就是进入到了activity B,当用户按下BACK键,他应该回退到activity A。
下面的具体流程描述了当用户响应通知时,activity栈是怎样工作的:
1. 首先,用户在Calendar中设置了一个开会通知,也就是创建了一个新的事件,并将已写进Email中的部分信息复制到该事务上。
2. 其它用户选择 桌面 > Gmail。
3. 他们打开Gmail,接收到来自Calendar发出的一个开会通知。
4. 接着他们打开了那条通知,进入到Calendar activity中,并查看会议的简要说明。
5. 这时用户进入到其里面查看更为详细的内容(就是在第一步当中复制的信息)。
6. 当用户完成查看的操作时,按下了BACK键。他们回到了Gmail上,也就是打开通知的那个地方。
但上面的流程在默认情况下却不是这样的。
通常情况下主要有两种方式来发出通知:
* 通知专用的activity - 接着上面的应用场景来说,某用户接受了一条Calendar通知,用户首先进入Gmail,查看详细的内容就要进入Calendar activity中;用户查看完后,按下BACK键必须要返回到Gmail activity上。实现此功能的前提是,Calendar activity不能有与Calendar或其它activity同样的亲缘性(affinity),也就是将该亲缘性设为空字符串即可。下来解释一下为什么这么做。
那个Calendar activity拥有其默认的任务亲缘性(taskAffinity),当按下BACK键时(如上述第六步)回到了Calendar,而不是Gmail,这就是上面那样做的主要原因。特定应用程序中的所有activity都具有相同的任务亲缘性,因此Calendar activity的亲缘性匹配了Calendar的任务,这个任务是在第一步当中运行起来的,那第四步就表示打开Calendar activity,又回到了Calendar的任务中,所以最后返回的还是Calendar activity。但这不是我们想要的结果,只有将任务亲缘性设为空字符串才能解决这个问题。
* 选择已有的activity,但只会展示其初始的状态 -例如,用户在于Gmail交互的过程中进入到了其它Activity,稍后再在回到Gmail activity时显示要显示其初始的状态,而不是先前的状态。首先,你要确保通知触发器起作用时,intent的标识是“clear top”,所以当activity启动时,它显示的是初始化之后的activity,防止Gmail再次来到前台时还是用户上一次的看到的那些状态。(你需要在intent对象中设置FLAG_ACTIVITY_CLEAR_TOP标志)
另外还有其它方式去处理通知,比如让一个activity到前台并设置好其显示的指定数据,比如短信息。
一般情况下都是以新任务的形式来启动通知activity的(也就是说,在intent对象中设置FLAG_ACTIVITY_NEW_TASK),这么做是避免这个任务成为另一个任务中的一部分。
请使用通知系统,而不要使用对话框来代替通知
如果你的后台服务在某个时刻要通知给用户一个消息,那么请使用标准通知系统-,不要使用dialog或toast来通知。这两个会直接弹出来提示用户,再说通俗一些就是会突然打断用户的当前操作,这是一个极为不友好的用户体验。通知系统在这方面就做得就比较好,用户可以在适当的时候从屏幕上方拉下通知列表以便回应消息。
不要重新设置BACK键的功能,除非你有绝对的需要
BACK键的主要功能就是从当前的activity回退到上一个activity,就是所谓的导航功能。大多数的activity都是一些比较通用的操作,诸如查看联系人列表,查看照片等,如果按BACK键,就直接返回先前调用它的activity就好了,不需其它的功能需求。
但要考虑一个问题,如果是应用程序非常得大,并需要细粒度的BACK键来加以控制该如何呢?例如Google浏览器,已经打开了几个web页面和地图页面,其中有一些关于地图数据的图层面板,我们需要在它们之间进行切换操作,也就是说在其内部通过BACK键来对其进行回退导航,而不是针对整体的activity。
接上面的例子继续说,地图应用程序展现给用户不同的数据图层面板:有用来显示查询结果的定位信息、有显示朋友的位置、有显示街道方向的路线等。地图应用程序将这些图层面板保存在自身的历史记录里面,所以需要BACK键来进行回退导航。
同样地,浏览器使用浏览器窗体为用户展示多个的web页面,每个窗体都有它自身的历史导航,也就是桌面操作系统上的浏览器中的标签。例如,你在Android浏览器中的一个窗体上打开Google进行查询,并点击一条查询结果,那么这个结果页面会在当前窗体上打开,然后按BACK键就会回到了查询页面。总结一句话就是,当前窗体是从先前的窗体上跳转过来的,这时按BACK键就会回到了先前的窗体。如果用户一直按BACK键的话,最后就会离开浏览器所在的activity,回到了桌面。
这些例子就是你有绝对的需要才要重新设置BACK键功能的理由。
本文转自博客园农民伯伯的博客,原文链接:Android开发者指南(16) —— Activity and Task Design,如需转载请自行联系原博主。