(自己翻译的,转载请注明,谢谢。缺图、排版问题以及无效链接,我会慢慢修正,之前请和AndroidSDK文档对照来看)
这个文档,从高层的、以用户为中心的视角,描述了Android应用框架的 核心原则。这对交互和应用设计者和应用开发者是有用的。
本文用多个例子说明了Activity和Task,而且描述了一些它们的底层原则和 机制,像导航,多Task,Activity重用,Intent和Activity栈。 文档还强调了一些你能用到的设计结论,和你如何控制你应用的UI。
这个文档使用了很多Android应用作为例子,包括一些默认应用, 例如Dialer,以及Google应用,例如Maps。你可以在你的Android模拟器 或者Android的手机上试试它们。注意你的手机或许只提供一部分这些文档 中的例子应用。
在设计建议部分中,会提到一些原则、 建议、和要避免的事情。Application基础覆盖了程序的底层 机制,本文档是对它的补充和完善。
以下四个基本概念有助于你的理解:
一个典型的Android Application由一个或多个相关的、松耦合的、用户可以 与之互动的Activity组成。典型的情况,是一个Application打包进 一个单独的apk文件里。Android会伴随着一大票的应用,可能包括电子邮件、日历、浏览器、地图、 短信、联系人、拍照、拨号、音乐播放、设置等等。
Android主屏一般就是应用启动者。一般来说,是一个有很多应用图标的滑动抽屉(就是默认Android系统下面那个,用手指往上一拖就出现很多图标的那个东西。),用户可以从上面启动应用。
Activity是Android应用的主要组成部分。 当你建立Apllication的时候,你可以自己建立的Activity或者重用其它Application的Activity,来组装Application。 这些Activity是在运行时绑定在一起的,所以,新安装的Application能从已安装的Activity中获益。 一旦组合在一起,这些Acitivity会像一个整体那样一起工作。 一个Activity拥有一个独立的可视UI,这个UI应该基于单独的、明确界定的意图。 例如看图、编辑、拨号打电话、拍照、搜索、发送数据、语音命令等等其他用户行为。 一个需要界面显示的Application至少要有一个Activity。
当使用一个Android设备时,用户会从一个界面跳到另一个界面,这种跳转应该是流畅的。 不应该让用户察觉那些底层的行为,比如Activity间或者Task间的切换。
一个Activity持有了一种特定种类的内容(数据),以及接受一系列相关的用户动作。 一般来说,每个Activity的生命周期,与其他的同一个Application或Task内的Activity是无关的。 每一个Activity独立地被运行,用户或者系统可以按需要start、run、pause、resume、stop或者restart这些Activity。 由于这种独立的特性,有很多种方法可以覆盖或者重用Activity。
Android提供的拨号程序就是一个Activity组合的例子。这个程序是由4个Activity组合成的: 拨号,联系人列表,联系人详情,和新建联系人。如下所示:
拨号
|
联系人
|
详情
|
新建联系人
|
下面是一些其它的由多个Activity组合的Application的例子:
Activity,是组成Application的四种组件中,最重要的一个。另外的组件是, Service,ContentProvider,BroadcaseReceiver。更多关于Activity的细节,请参考 Application组件中的Activity部分。
当用户在Application中,从一个Activity跳到另一个时,Android系统会 保存一个用户访问Activity的线性导航历史。 这就是activity栈,也被称为返回栈。 一般来说,当用户运行一个新的Activity,这个Activity就会被加到Activity栈里。因此,当用户 按BACK键的时候,栈中的上一个Activity就会被展示出来。 用户可以一直按BACK键,直到返回到了主屏。 The adding of an activity to the current stack happens whether or not that activity begins a new task (as long as that task was started without going Home), so going back can let the user go back to activities in previous tasks. 把Activity加入到当前栈里的操作,与Activity是否启动了一个新Task无关。 返回操作可以使用户从当前Task回到上一个Task。 用户可以在应用管理器、主屏、或者“最近Task”屏幕,恢复到刚刚的Task。
只有Activity可以加到Activity栈里去,其它的,包括View、Window、Menu或者Dialog都不行。 这就是说,假设,界面A跳到界面B,然后用户可以用BACK跳回界面A。这种情况下,A和B都要被实现成Activity。 这个规则有一个例外的情况。那就是除非你的应用 控制了BACK键并且自己管理界面导航。
一个Task是用户可以完成一个特定目标的一组Activity。与Activity属于哪个Application无关。 除非明确地新建一个Task,(参考打断Task),用户启动的所有Activity都默认是当前Task的一部分。 需要注意的是,这些Activity可能属于任何一个Application — 属于同一个Application或者属于不同的Application。 这就是说,一个Task可以是,从联系人列表开始,然后选择一个邮箱地址(通过电邮Activity),然后附加一个照片(通过图片Activity)。 联系人列表、电邮、图片,这些都存在于不同的Activity中。
启动Task的Activity被称作根Activity。 通常,Task是从应用管理器、主屏或者最近Task(长按HOME键)开始的。 用户可以通过点击根Activity的图标回到Task里去,就像启动这个Activity一样。 在这个Task中,BACK键可以回到这个Task的前一个Activity里。 Activity栈可以由一个或多个Task组成。
一些关于Task的例子:
打断Task - Task的一个重要的特性就是,用户可以中断他当前正在做的事(他的任务),去进行另一个Task,然后可以返回到原来的那个Task去完成它。 这个特性的意图,就是用户可以同时运行多个任务,并且可以在这些任务间切换。 有两种主要的的方法离开一个Task,这两种情况中,应该让用户能够返回到他们离开的那个任务:
当然,规则总是有例外。除了上面提到了两种方法,确实存在第三种方法开始一个新任务,即,在代码中startActivity的时候,定义它要开始一个新Task。 地图和浏览器两个应用就是这么做的。 例如,在电邮中点击一个地址,会在新Task调出地图Activity,在电邮中点击一个链接,会在新的Task中调出浏览器。 在这种情况下,BACK键会回到上一个Activity(另一个Task中的电邮Activity),因为它不是从主屏启动的。
接下来的例子,说明了Application、Activity、Activity栈、BACK键、Task和Intent的基本原理。 它展示了系统响应用户操作的方式,包括启动Activity和在Task之间切换。 你可以在你的Android手机上,按照下面指示的那样,执行这些例子。
主屏是绝大部分Application启动的地方。(某些应用只能从其它应用进入。) 当用户点击应用管理器的一个图标(或者主屏上的一个快捷方式),这个应用的主Activity会被启动到前台,并拥有用户的焦点。 像下面图标展示的那样,用户进入主屏并点击电邮图标的行为,启动了电邮Application的消息列表Activity。 主屏Activity被置到后台,stop。当用户重新进入主屏时,restart。
一个Activity是否保持它的状态,取决于用户离开它的方式 - 是通过HOME键还是BACK键。
默认情况下,按BACK键会finish(destroy)当前Activity,然后显示上一个Activity给用户。 在下面的图中,用户通过在主屏点击电邮图标启动了显示出电邮消息列表的电邮应用。 用户滚动列表(改变了初始状态)。 点击BACK键销毁了消息列表Activity,然后回到了上一个Activity,即是主屏。 用户重新启动电邮应用后,电邮应用展示出来的列表,是重新初始化,没有经过滚动的。
在上面的例子中,按BACK键会回到主屏,是因为主屏是用户能看到的最后一个Activity。 要是用户是从其它Activity进到消息列表Activity的,那么按BACK键会回到来的地方。
作为对比,下一个图表展示了用户用HOME键而不是BACK键来离开消息列表Activity的情况 - 消息列表Activity会stop然后移到后台,不会被销毁。从快捷方式重新启动电邮Application,会把后台的这个应用 置到前台(从stop变成running)。滚动状态会跟用户离开的时候一样。
有例外情况。 某些后台Activity回到前台后,会进到它的初始界面(而不是离开时的状态)。 联系人应用和看图应用就是这种Activity。 当用户从主屏进入联系人应用,然后选择一个联系人看详细资料。(然后HOME键)。 如果用户再次从主屏进入,就会回到联系人列表而不是离开时的详细资料。 这样设计联系人应用,是因为联系人列表是整个应用4个Tab的主进入点。
另外,不是所有的Activity都会在按BACK的时候被销毁。 当用户使用音乐应用播放音乐,然后按BACK键时,应用重写了后退的行为,Activity没被销毁,而仅仅是变得不可见。(所以保存了状态)。 音乐继续播放,并出现一个Notification,让用户可以回到音乐Activity去控制音乐的播放。 注意,你可以写不可见就销毁的Activity,也可以写像音乐Activity一样仅仅会切到后台的Activity。
假设Activity A启动了另一个Application中的Activity B,就说Activity B被重用了。 在Activity A广播查找能力然后B有这个能力时,这种情况就会发生。
Contacts重用了Gallery来取图片 - Contacts Activity有一块区域用来显示联系人的图片,而一般来说,所有的图片都保存在Gallery里。 于是Contacts可以重用Gallery Activity来取图片。 这是一个重用Gallery Activity的很好的例子。 下面的图表说明了这个流程(主要部分)。 流程是这样的:用户点击Contacts,然后点选一个联系人查看详细资料,按MENU键,编辑联系人,点击图片区域, 于是就会启动Gallery Activity。用户找到想要的图片后,裁剪并保存。保存这个动作,会导致图片被插入到联系人详情的图片区域中。
Gallery会返回图片给启动自己的Contact Application。 下面一个例子说明了重用Activity但没有返回的情况。 还要注意下面的图标是一个对Activity导航历史(即Activity栈)的说明。用户可以一直按BACK直到回到主屏。
设计Application的时候,考虑如何能够重用其他Application中的Activity, 以及考虑自己的Activity如何被其它Application重用,是一个很好的思路。 如果你的Activity拥有一个和已存在的Activity一样的Intent Filter, 系统会提供给用户一个对这些Activity的选择界面。
Gallery重用Messaging来分享图片 - Sharing is 分享图片是另一个很好的关于在不同的Application重用Activity的例子。 如下图所示,用户启动Gallery,选一个图,按MENU键,选Share,选Messaging。 这就会启动Messaging Activity,然后建立一个新的消息并把图片附在上面。 然后用户填写“收件人”、写个短消息然后发送。 用户的关注点现在在Messaging程序上。 如果用户想回到Gallery,他必须按BACK键。(用户可以通过BACK键从任何一个Activity一路返回到主屏。)
与上一个例子不同,这个对Messaging Activity的重用什么都没有返回给启动它的Gallery Activity。
两个例子实际上都说明了Task — 为了完成一个目标的一系列Activity。 每个例子都是用了来自不同的Application的Activity来完成任务。
下面这个情况,是Activity A在不同的Application里代替了Activity B。 这种情况一般发生在Activity A做某项工作比Activity B好的时候。 换句话讲,A的功能足够代替B。 与重用不同的是,重用的时候,A和B做的是不同的事情。
在本例中,用户下载了一个PhoneRingtone Activity的替代品,叫做RingsExtended。 现在,当用户进入Setting,SoundDisplay,PhoneRingtone,系统会提供一个选择界面,让用户选择是使用Android系统的电话铃Activity还是新的这个。 这个对话框有一个多选框,让用户可以选择“这种情况下默认使用这个选项”。 用户选了RingsExtended,然后新的Activity被启动,代替了原来的Android系统电话铃Activity。
上面说过,用户可以从一个Activity通过HOME切到主屏,然后进入另一个Activity,而前一个Activity不会被销毁。 下面演示了进入Map Application的情况。
注意,你可以自己决定,在程序被挪到后台后,是继续运行还是停止。 (参考onStop(),Activity生命周期)。 对于从网络上下载数据这类Activity来说,建议还是让它们在后台的时候,继续工作。用户就可以多任务了。
主屏的应用启动器,在两个不同的Task里,启动了ViewMap和DayView两个Activity。 这个时候,系统就是多任务的 — 运行了多个Task。
每个Application必须至少有一个进入点 — 好让系统或者用户进到Application的Activity里。 在主屏的ApplicationLauncher,每一个图标表示了一个进入点。 Application也可以从其他Application进入。 每个Activity都是一个Application的潜在进入点。
Android提供的PhoneApplication有两个进入点:Contact和Dialer。 一个在Contact中的用户可以点选一个电话号码,来进入Dialer。 如下图所示,用户能点击Contact的图标来进入ContactActivity,然后点击一个电话号码,来进入DialerActivity,拨打电话。
一旦用户进入到了Application里,就可以通过tabs、menu items、list items、buttons或者其他界面控制元素, 来进入其它的Activity,例如NewContact或者EditContact之类。
当用户要对某类数据做出行为时,例如,点击一个mailto:[email protected]链接, 他们实际上是初始化了一个Intent对象(一个Intent)。这个Intent会被解决成为一个特定的Commpnent。 (这里我们只说ActivityComponent)。 (翻译成人话:用户要做一件事情,实际上会初始化出来一个Intent,广播询问系统,然后最终导致一个特定的Activity被调用。) 进而,用户点击mailto:链接的结果,是一个Intent对象。系统会尝试用这个Intent来匹配一个Activity。 如果这个Intent明确地指名了一个Activity(一个明确Intent),系统会立即进入这个Activity来响应用户动作。 然而,如果这个Intent没有指名Activity(一个不明确Intent),系统会比较这个Intent和所有可用的Activity的IntentFilter 。 要是多个Activity都能处理这个动作和数据,系统会让用户选择一个。
下图展示的就是点击mailto:链接的例子。 如果设备上有两个处理Email的应用,当用户点击电邮地址的结果,是出现一个对话框,让用户在两个应用中间选一个。(Gmail和Email两个应用)。
Here are some examples of Intent objects and the activities they resolve to: 这里是一些Intent对象(不明确的)和Intent对应Activity的例子:
注意,Intent对象指定了两件事:行为和数据:
任何一个在主屏上启动Activity的Intent都是明确的Intent。 灵位,一些Activity进入自己Application内部的Activity使用的也是明确Intent。
Intent的详情,参见Intent class
和 intent filters.
下面一系列图展示了用户如何在两个Task间转换。 在这个例子中,用户写了一个文本消息,然后附上一张照片。在做这些事儿的时候,用户看了一眼日历。 然后,用户回来,继续附照片和发送消息。
主屏 > Messaging > NewMessage > MENU > Attach > Pictures 最后一步操作启动了Gallery来取照片。 注意到,Gallery是在另外一个Application中。
在用户取图之前,用户决定去看一眼日历,这就是另一个Task了。 但是当前界面没有一个直达Calendar的按钮,所以用户必须回到主屏去。
以下是对应用设计者和开发者的建议和指示。
当你不想你的Activity被其它Application重用的时候,要确定这个Activity上没有定义任何的IntentFilter。 只能从应用管理器进入,或者只能从同一个Application进入的Activity,应该应用这种做法。 对于这类Activity,使用目标明确的Intent来进入。(而不是通过Intent向系统查询能力这种做法)。 这种情况,也没必要使用IntentFilter。 IntentFilter是发布给所有其它Application的。 所以,如果你用了IntentFilter,你实际上就是在给你的Activity加入了一个外部入口,有可能会造成无意识的安全漏洞。
你的Application可以重用其它的Activity,如果这些Activity可以重用的话。 重用的时候,你不能假设你的Intent一定能匹配到一个外部Activity。你必须考虑没有合适Activity的情况。
你可以在真正启动Activity之前测试一下是否有匹配的,也可以直接启动Activity然后处理异常。 两种解决方案见博文Can I use this Intent?。
通过查询PackageManager可以知道一个Intent能否被匹配到一个Activity。 博文提供了一个例子,在isIntentAvailable()方法中。 然后,如果无法匹配,你可以停止构造Intent对象,或者再提示给用户一个地址,例如Market,让他去下载需要的应用。 如此,你的代码在调用startActivity()或者startActivityForResult()的时候,可以保证Intent对象都是经过测试的并可以匹配到合适Activity的。
一个设计者,或者开发者,应该清楚用户可以如何启动你的Application,以及使用Application内的Activity。 用户可以通过主屏,或者其他Application来使用你应用里的Activity。
当你的一个或多个Activity可以替代别的Application已存在的Activity时,在用户需要这个Activity时你可以让你的Activity也对用户可用。 例如,如果你的Activity也可以发送数据(电邮、文字信息或者上传),要考虑把你的这个Activity作为一个选择提供给用户。 给出一个特定的例子。Gallery让用户可以分享图片。 当用户从菜单选择“分享”时,系统会比较“分享”请求(一个Intent对象)和可用的Activity(通过它们的IntentFilter),然后提供给用户来选。 此时,系统匹配到了Email,Gmail,Messaging和Picassa。 要是你的Activity也能发送图片或者上传图片的话,只需要通过配置IntentFilter让你的Activity可用就可以了。
其它Activity启动你的Activity的时候,可能期望或者没有期望有返回。
startActivityForResult()
启动的Activity就是这样)。 startActivity()
启动的Activity就是这样)。 实际上,连Application都不是所有的都有图标并且可以在主屏启动的。 拿一个不常用的小应用做例子,这个小应用能够替换一个其它Application中有进入点的功能。 例如,Android一般有一个内建的铃声选择器,用户可以从设置应用中的声音设置来进入。 你可以写一个自己的铃声选择器,可以用跟系统的选择器同样的Intent来进入。 如下图所示,当用户选择“电话铃”的时候,会有一个对话框,来让他选择是使用系统的选择器还是你的选择器,用户可以保存他们的选择。 铃声选择器是一个不怎么常用,并且有一个已经明确定义的开始点(其它应用中)。所以,其实没必要在主屏弄一个单独的图标给它。
Camera.apk就是一个应用多个分开的主Activity的很好的例子。它有两个主Activity,Camera和Camcorder, 有不同的图标,并且可以独立进入。 在用户看来,好像是两个不同的应用。 它们共享同一个镜头,而且都把数据(静态图片或者动态图片)存到Gallery里去。
In order for your application to contain two different, independent activities launchable from Home, you must define them to be associated with different tasks. 如果你希望你的应用包括两个能从主屏进入的不同的不相关的Activity,你需要定义它们关联于不同的Task。 (意思就是说,要为每个主Activity设置不同的TaskAffinity。上面这个例子中,是“com.android.camera”和“com.android.videocamera”。)
Contacts和Dialer实际上也是这种情况,在同一个Application的两个主Activity。
如果你的Activity能够被其他Application启动的话,应该允许它们进入到当前Task, 或者一个有TaskAffinity的已经存在的Task。 让Activity能够被加到Task里去,可以让用户在包含你的Activity的Task和其它Task之间切换。 除非你的Activity只允许一个实例。
要达到这种效果,你的Activity的LaunchMode应该是standard或者singleTop,而不是singleTask或者singleInstance。 这些LaunchMode(standard或者singleTop)允许你的Activity有多个实例。
后台运行的应用,可以用一个Service来向用户发送Notification,让他们知道他们感兴趣的事件。 Calendar和Email是两个例子。Calendar会发送Notification作为闹铃提醒,而Email会发送Notification来提醒用户新邮件。 建议,当用户在ActivityA,收到了一个Notification,通过这个Notification进入到ActivityB,再按BACK键时,应该回到ActivityA。
下面的流程展示了,当用户处理一个Notification的时候,ActivityStack会如何工作。
上述行为并不是必须的。
Notification一般来说通过下述方式之一来实现:
由于Task的工作方式,如果这个专用Activity的TaskAffinity保持默认的话,在上面的第6步按BACK键后, 会回到Calendar,而不是Gmail。 原因是,默认情况下,同一个Application的所有Activity有同一个TaskAffinity。 因此,这个专用的Activity的TaskAffinity会匹配第1步的那个Calendar的Task。 这意味着,选Notification会把已存在的Calendar事件(第1步的那个)拉到前面, 然后把这个专用Activity放到最上面。 这个不是你想要的。把这个专用Activity的TaskAffinity设成空字符串就能修正这个问题。
FLAG_ACTIVITY_CLEAR_TOP
放到传递到StartActivity()的Intent来做到这一点)。 其它方式也可以用来处理Notification,包括把Activity拉到前台,展示特定的数据(展示刚刚收到新消息的文字消息线程)
一个Notification总是把Activity启动到新的Task。(Intent总是带着FLAG_ACTIVITY_NEW_TASK)。 这是因为,一个对当前任务的打断,不应该属于当前任务。
如果你的后台Service需要通知用户什么的话,使用标准的Notification系统 — 不要使用Dialog或者Toast来通知。 Dialog或者Toast会立即吸引用户的注意力然后打断用户,把他的注意力从他正在做的事情上吸引开: 用户可能正在输入文字,此时Dialog出现的话,这些文字会被突然应用到Dialog上。 用户习惯于在方便的时候再处理你的消息和通知。
用户从一个Activity到另一个Activity的时候,系统会把它们放到Activity栈里去。 这些以一个导航历史的形式被BACK键利用。 大多数的Activity都有有限的目的,有单一系列的数据,例如看一批联系人,写邮件,或者拍照。 但是,如果你的应用只是一个大Activity,其中有一些页,需要细粒度控制BACK键的时候呢? 这样应用的例子,是浏览器和地图。Google浏览器有一系列的web页,Google地图有一系列的图层可以切换。 这两个应用都接管了BACK键,有内部的返回栈。
例如,Map使用了层在地图上来展示不同的信息给用户:展示地址搜索的结果,展示朋友的地址,展示两点之间的路线等。 Map自己储存了层的栈,所以BACK键可以回到上一层。
同样地,Browser使用浏览窗口展示不同的web页。 每个窗口有它们自己的导航历史,类似桌面操作系统中浏览器的Tab,每个Tab都有不同的返回栈。 For example, if you did a Google web search in one window of the Android Browser, clicking on a link in the search results displays a web page in that same window, and then pressing BACK would to the search results page. Pressing BACK goes to a previous window only if the current window was launched from that previous window. If the user keeps pressing back, they will eventually leave the browser activity and return Home.
↑ Go to top