<service>

语法

<service android:enabled=["true" | "false"]
         android:exported=["true" | "false"]
         android:icon="drawable resource"
         android:isolatedProcess=["true" | "false"]
         android:label="string resource"
         android:name="string"
         android:permission="string"
         android:process="string" >
    . . .
</service>

包含于

<application>

可包含

<intent-filter>

<meta-data>

描述

将一个服务(Service 的子类)声明为某个应用程序的组件。与活动不同,服务没有用户界面。它用于处理长时间的后台操作,或实现可被其他应用程序调用的富交互 API。

属性

android:enabled

该服务是否能被系统实例化。“true”表示可以,“false”表示不可。默认值为“true”。

<application> 元素也具有 enabled 属性。这一属性作用于包括服务在内的该应用程序所包含的所有组件。如果要使用某个服务,<application> 与 <service> 的 enabled 属性必须同时为“true”。如果其中有一方为“false”,该服务就将被禁用,无法被系统实例化。

android:exported

该服务能否被其他应用程序的组件调用或与之交互。“true”表示可以,“false”表示不可。如果为“false”,只有同一应用程序,或具有相同用户 ID 的应用程序的组件才能启动或绑定该服务。

该属性的默认值由服务是否包含意图过滤器决定。如果该服务不含任何意图过滤器,则表示要调用它必须明确指定其类名。这意味着该服务被设计为仅供应用程序内部使用(因为其他应用程序无法知晓服务的类名)。此时,exported 属性的默认值为“false”。另一方面,如果存在一个或更多的意图过滤器,则表示该服务可被外部程序调用,于是默认值为“true”。

要限制一个服务被其他应用程序调用,这只是方法之一。我们也可以利用 permission 来限制外部实体与服务发生交互(参见 permission 属性)。

android:icon

代表该服务的图标。该属性必须是某一drawable资源(包含了图像的定义)的引用。如果不为该属性赋值,该服务将默认使用应用程序的图标(参见 <application> 元素的 icon 属性)。

无论通过该属性设置,还是直接沿用 <application> 元素的图标,服务的图标也是该服务所有意图过滤器的默认图标(参见 <intent-filter> 元素的 icon 属性)。

android:isolatedProcess

如果为“true”,该服务将运行于某个特殊进程,与系统其他进程分隔,且不提供任何访问权限。此时,我们只能通过 Service API 与之通信(绑定与启动)。

android:label

用于向用户显示的服务名称。如果不为该属性赋值,该服务将默认使用应用程序的名称(参见 <application> 元素的 label 属性)。

无论通过该属性设置,还是直接沿用 <application> 元素的名称,服务的名称也是该服务所有意图过滤器的默认名称(参见 <intent-filter> 元素的 label 属性)。

该标签应该是某一 string 资源的引用,以便与其他在用户界面中显示的字符串一样进行本地化。不过,在开发过程中,为了方便,该属性也支持直接使用字符串字面量。

android:name

实现了该服务的 Service 子类的名称。它应当为完整的类名(如”com.example.project.RoomService”)。不过,为便于使用,我们可以仅使用包名之后的部分(如”.RoomService”),<manifest> 元素中指定的包名将被自动补全。

一旦我们发布了应用程序,就不该再去修改它的名称(除非 android:exported 被设为“false”)。

该属性没有默认值,但必须被赋值。

android:permission

未启动或绑定该程序所必需的权限名称。如果 startService()、bindService() 或 stopService() 的调用方不具有该权限,就无法访问该服务,Intent 对象也不会被传送。

如果该属性未赋值,<application> 元素的 permission 属性将作为其默认值。如果 <application> 元素同样为对 permission 属性赋值,该服务将不受权限保护。

如需获得更多信息,参见简介中的权限一节,或参见文档《安全与权限》。

android:process

运行该服务的进程名称。通常,一个应用程序的所有组件都运行于为其创建的默认进程。该进程与应用程序的包名同名。我们可以通过 <application> 元素的 process 属性为所有组件设置一个不同的默认进程名称。同时,组件也可以通过改写其 process 属性的值,使应用以多线程的方式运行。

如果该属性的值以冒号(”:”)起始,则会在必要时创建一个应用程序私有的新进程来运行该服务。如果进程名称以小写字母起始,该服务将运行于一个同名的全局进程。此时,该属性表示服务有权限使用该进程。于是,不同应用程序中的组件可以共享一个进程,节约了资源的使用。

参见

<application>

<activity>

引入版本

API Level 1

动作栏(Action Bar)

Action Bar是活动中的一种控件,用以代替传统的屏幕顶端的标题栏。默认,Action Bar包括了左侧的应用程序图标(logo),其右是活动的标题,以及Option Menu中的可选项目。Action Bar提供了多种便利特性,包括:

  • 直接在Action Bar中显示Option Menu中的项目,称之为“动作项目(action item)”——以提供对于关键用户操作的快速选择。没有作为动作项目显示的菜单项目则位在浮动式菜单中,通过Action Bar的下拉列表显示。
  • 提供在不同片段之间的导航标签。
  • 提供导航用的下拉列表。
  • 提供交互式的“动作视图(action view)”以代替动作项目(例如搜索框)。
图1. Email应用程序中Action Bar的一个截图,包含了编写新邮件和刷新收件箱的动作项目。

添加Action Bar

Action Bar在Android 3.0及以上的活动中是默认被包含的。更为具体地说,所有使用了新的“holographic”主题的活动将包含Action Bar,而任何以Android 3.0为目标平台的应用程序将自动使用该主题。当一个应用程序在<uses-sdk>元素中设置了android:minSdkVersion或android:targetSdkVersion属性为“11”或更高时,被认为是以Android 3.0为目标平台的。例如:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example.helloworld"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="4"
              android:targetSdkVersion="11" />
    <application ... >
        ...
    </application>
</manifest>

在本范例中,程序请求了最小API等级为4(Android 1.6),而目标API等级为11(Android 3.0)。这样,当程序安装于运行Android 3.0或是更高版本的设备上时,系统将为每一个活动提供holographic主题,因而每个活动都将包含Action Bar。

不过,如果希望使用Action Bar的API,例如添加标签或是修改Action Bar风格,就需要将android:minSdkVersion设为“11”,这样才能使用ActionBar类。

移除Action Bar

如果希望从某个指定的活动中移除Action Bar,只需将该活动的主题设为Theme.Holo.NoActionBar。例如:

<activity android:theme="@android:style/Theme.Holo.NoActionBar">

提示:如果希望从一个自定义的主题中移除Action Bar,只需将android:windowActionBar样式属性设为false。关于更多Action Bar样式的内容请参见“对Action Bar使用样式”。

还可以在运行时通过调用hide()来隐藏Action Bar,之后通过调用show()来再次显示它。例如:

ActionBar actionBar = getActionBar();
actionBar.hide();

当Action Bar隐藏时,系统将调整活动的内容来填充可用的屏幕空间。

注意:如果移除了使用主题的Action Bar,那么该窗口将完全禁用Action Bar而无法再在运行时添加——调用getActionBar()将返回null。

添加动作项目

动作项目仅仅是Option Menu中被声明要直接显示在Action Bar上的菜单项目。一个动作项目可以包括一个图标和/或文本。如果一个菜单项目不是动作项目,那么系统将把它放在浮动式菜单中,用户可以通过选择Action Bar右侧的菜单图标打开浮动式菜单。

当活动启动时,系统将通过调用onCreateOptionMenu()来为活动生成Action Bar和浮动式菜单。如同在“菜单”开发者指南中所描述的,这是为活动定义Option Menu的回馈方法。

可以指定某一菜单项目作为动作项目显示——如果有这样的空间的话——通过在菜单资源中为<item>元素声明android:showAsAction=”ifRoom”。这样,该菜单项目仅会在空间足够时显示在Action Bar中以供快速选择。如果空间不足,该项目将被置于浮动式菜单中(通过Action Bar右侧的菜单图标打开)。

可以在应用程序代码中声明一个菜单项目为动作项目,只需对MenuItem调用setShowAsAction()并传递SHOW_AS_ACTION_IF_ROOM。

如果菜单项目同时提供了标题和图标,那么动作项目默认只显示图标。如果希望让动作项目包含文本,需要在XML中添加”with text”旗标、对android:showAsAction属性添加withText,或是在程序代码中调用setShowAsAction()并使用SHOW_AS_ACTION_WITH_TEXT旗标。图2展示了有两个带有文字的动作项目以及浮动式菜单图标的Action Bar。

图2. 有两个动作项目以及浮动式菜单图标的Action Bar的屏幕截图。

下面是一个在菜单资源文件中将菜单项目声明为动作项目的一个范例:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_save"
          android:icon="@drawable/ic_menu_save"
          android:title="@string/menu_save"
    android:showAsAction="ifRoom|withText" />
</menu>

在这时,ifRoom和withText旗标都被设置了,所以当这个项目作为动作项目显示时,它包含了图标以及标题文本。

Action Bar中的菜单项目和Option Menu中的其他项目都会启发相同的回馈方法。当用户选择了一个动作项目时,活动将收到一个onOptionsItemSelected()的调用,并传递项目ID。

注意:如果用片段中添加菜单项目,那么该片段相应的onOptionsItemSelected()方法将被调用。然而活动可以在此之前先对其进行处理,也就是说系统对活动的onOptionsItemSelected()调用要先于对片段的。

可以声明一个项目总是作为动作项目出现,不过这并不被推荐,因为这会在有太多的动作项目时让UI变得混乱,Action Bar中的动作项目将会项目重叠。

关于菜单的更多信息,请参见“菜单”开发者指南。

将应用图标用作菜单项目

应用程序图标默认出现在Action Bar的左侧。它也将响应用户交互操作(当用户点击它时,它会有和其他动作项目一样的视觉响应),但需要手动指定用户点击时的行为。

图 3. Email的Action Bar,左侧有应用程序图标。

通常的行为应当是在用户单击图标时让应用程序回到“主活动”或是(比如,在活动没有发生变化而片段变化了的时候)回到初始状态。如果用户已经处于主活动或是初始状态,则不必进行任何处理。

当用户单击图标时,系统以android.R.id.home的ID调用该活动的onOptionsItemSelected()方法。因此,需要在onOptionsItemSelected()方法中添加一个条件判断来侦听android.R.id.home并执行正确的行为,例如启动主活动或是将最近的片段事务出栈。

如果通过返回主活动来响应应用图标的点击,那么需要在Intent内包含FLAG_ACTIVITY_CLEAR_TOP旗标。这样,如果要启动的活动已经存在于当前任务的话,所有在其上的活动将被销毁,该活动将回到最上层。这种方式是令人满意的,因为回到“主活动”和“返回”是等价的,不应该为主活动创建新的实例。否则,最终在当前任务中会有一个很长的活动栈。

例如,下面是一个onOptionsItemSelected()的实现,它将返回应用程序的“主活动”:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            // app icon in Action Bar clicked; go home
            Intent intent = new Intent(this, HomeActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

使用应用图标来进行”向上一级“导航

还可以使用应用程序的图标来为用户提供“向上一级”的导航。这在程序中的活动总是以某种固定的顺序出现并期望用户能方便地返回上一级活动的情况下特别有用(不过无所谓用户是怎样进入当前的活动的)。

响应这一事件的方式和返回主活动的方式是相同的(和上文所说的类似,只不过现在是根据当前的活动是哪一个来启动另一个不同的活动)。为了告诉用户这时的情况将有所不同,唯一需要做的就是把Action Bar设为“show home as up”。这通过对活动的ActionBar调用setDisplayHomeAsUpEnabled(true)即可完成。这时,系统将为应用程序的图标增加一个表示向上一层动作的箭头,就像图4这样。

图 4. Email应用的标准图标(上)和“向上一层”图标(下)。

例如,下面是将应用图标表示为“向上一层”动作的方法:

@Override
protected void onStart() {
    super.onStart();
    ActionBar actionBar = this.getActionBar();
    actionBar.setDisplayHomeAsUpEnabled(true);
}

之后,活动应当在用户单击图标时进行响应,在onOptionsItemSelected()中,通过监听ID android.R.id.home(如上所示)。在这种情况下,当向上导航时,在Intent中使用FLAG_ACTIVITY_CLEAR_TOP旗标是更为重要的,这样才能不在上级活动的实例已经存在时还再次创建一个新的。

添加一个动作视图(Action View)

动作视图是在Action Bar上出现的一个控件,作为动作项目的一种替代。例如,如果在Option Menu中有一个项目是“搜索”,那么当该项目作为动作项目使用时可以在Action Bar中为该项目添加一个提供SearchView控件的项目。

当为一个菜单项目添加动作视图时,允许该项目在没有出现于Action Bar时仍能作为一个通常的菜单项目执行指令是很重要的。例如,一个执行搜索工作的菜单项目应该默认弹出Android搜索对话框,但如果该项目被置于Action Bar的话,动作视图将以SearchView控件的形式显示。图4展示了一个动作视图的SearchView控件的范例。

图4. 带有一个SearchView控件的动作视图。

为一个项目声明动作视图最好的方式是在菜单资源中使用android:actionLayout或是android:actionViewClass属性:

  • android:actionLayout的值必须是指向一个布局文件的资源指针。例如:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_search"
        android:title="Search"
        android:icon="@drawable/ic_menu_search"
        android:showAsAction="ifRoom"
        android:actionLayout="@layout/searchview" />
</menu>
  • android:actionViewClass的值必须是一个所要使用的View的完整类名。例如:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_search"
        android:title="Search"
        android:icon="@drawable/ic_menu_search"
        android:showAsAction="ifRoom"
        android:actionViewClass="android.widget.SearchView" />
</menu>

必须包含android:showAsAction=”ifRoom”以使项目在空间足够时作为动作视图显示。不过,在必要时,可以通过设置android:showAsAction为“always”来强制该项目以动作视图显示。

现在,当菜单项目作为一个动作项目显示时,它将是一个动作视图而不是图标和/或标题文本。不过,如果在Action Bar中没有足够的空间的话,该项目将在浮动式菜单中以一个通常菜单项目的形式显示,必须在onOptionsItemSelected()回馈方法中响应该项目。

当活动首次启动时,系统通过调用onCreateOptionsMenu()生成Action Bar和浮动式菜单。当菜单在该方法中被展开之后,可以通过以菜单项目的ID调用findItem()来获取动作视图的元素(比如为了绑定监听器),而后对所返回的MenuItem调用getActionView()。例如,上面的例子里的搜索控件可以像这样获得:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.options, menu);
  SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
  // Set appropriate listeners for searchView
  ...
  return super.onCreateOptionsMenu(menu);
}

关于使用搜索控件的更多信息,请参见“创建一个搜索接口”。

 添加标签(tab)

动作栏可以显示标签以使用户在活动内的不同片段之间导航。每一个标签可以包含一个标题和/或一个图标。

图 6. 在Honeycomb版本的图库范例程序中的Action Bar中的标签的截图。

首先,布局必须在每一个和显示的表现相关联的Fragment中包含一个View。要确保该视图有一个能在代码中引用的ID。

要添加标签至Action Bar:

1. 创建一个ActionBar.TabListener的实现来处理Action Bar标签的交互事件。比如实现所有的方法:onTabSelected()、onTabUnselected()和onTabReselected()。

每一个回馈方法都将传递收到了事件的ActionBar.Tab和一个FragmentTransaction用以执行片段事务(添加或是移除片段)。

例如:

private class MyTabListener implements ActionBar.TabListener {
    private TabContentFragment mFragment;

    // Called to create an instance of the listener when adding a new tab
    public MyTabListener(TabContentFragment fragment) {
        mFragment = fragment;
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        ft.add(R.id.fragment_content, mFragment, null);
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        ft.remove(mFragment);
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // do nothing
    }

}

这种ActionBar.TabListener的实现添加了一个保存与标签关联的Fragment的构造函数以使每一个回馈函数可以添加或是移除片段。

2. 在onCreate()时通过在Activity中调用getActionBar()来获取活动的ActionBar(不过要注意需要在调用了setContentView()之后才这么做)。

3. 调用setNavigationMode(NAVIGATION_MODE_TABS)来开启ActionBar的标签模式。

4. 为Action Bar创建所有的标签:

1. 通过对ActionBar调用newTab()来创建新的ActionBar.Tab。

2. 通过调用setText()和/或setIcon()来为标签添加文字和/或图标。

提示:这些方法返回相同的ActionBar.Tab实例,所以可以将这些调用一起使用。

3. 声明ActionBar.TabListener,并通过向setTabListener()传递其实现的一个实例来使用它。

5. 通过对ActionBar调用addTab()来添加所有的ActionBar.Tab,并传递它们。

例如,下面的代码结合了步骤2-5来创建了两个标签并将它们添加至了Action Bar:

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

    // setup Action Bar for tabs
    final ActionBar actionBar = getActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    // remove the activity title to make space for tabs
    actionBar.setDisplayShowTitleEnabled(false);

    // instantiate fragment for the tab
    Fragment artistsFragment = new ArtistsFragment();
    // add a new tab and set its title text and tab listener
    actionBar.addTab(actionBar.newTab().setText(R.string.tab_artists)
            .setTabListener(new TabListener(artistsFragment)));

    Fragment albumsFragment = new AlbumsFragment();
    actionBar.addTab(actionBar.newTab().setText(R.string.tab_albums)
            .setTabListener(new TabListener(albumsFragment)));
}

标签被选中时所有需要进行的行为都必须在ActionBar.TabListener回馈方法中被定义。当一个标签被选中,它将收到一个onTabSelected()的调用,应当在这里通过使用add()及所提供的FragmentTransaction来向布局中指定的视图添加正确的片段。类似的,当一个标签被取消选中时(因为另一个标签被选中了),则应当使用remove()来从布局中移除该片段。

注意:不能为这些事务调用commit()——系统会调用它,如果手动调用的话将会抛出一个例外。同时也不能将这些片段事务添加至返回栈。

如果活动被中止,应当保存当前选中的标签,这样当用户返回应用程序时,可以打开该保存的标签。在保存状态时,可以通过getSelectedNavigationIndex()获取当前选中的标签。这将返回所选标签的索引位置。

注意:应该在必要时保存每一个片段的状态,这样用户在切换标签并返回前一个片段时能保持之前的样子。关于保存片段状态的更过信息,请参见“片段”开发者指南。

添加下拉式导航

 

由于官方对文档进行了更新,有较大的改动。因此对于本文的翻译暂时中止,将在之后进行重新翻译。

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

片段(Fragment)

一个片段代表了一个Activity的一种行为或是其用户界面的一个区域。可以在一个单独的活动中组合多个分片来组建一个多面板UI,并在不同的活动中多次利用同一个分片。可以把片段理解为一个活动的一个模块化部分,有其自己的生命周期,接收其自己的输入事件,并且可以在活动运行过程中添加或移除一个片段。(译注:网络上也有“碎片”的译法,不过目前还并没有统一的名称。在这里姑且称为“片段”,个人感觉比“碎片”听起来舒服一些 :)  )

一个片段必须被嵌在一个活动中,它的生命周期与该活动的有着紧密联系。例如,当活动被暂停(pause),该活动中所有的片段也会暂停,当活动被销毁(destroy),其中所有的片段也会被销毁。不过,当活动在运行时(处于resumed生命周期状态),可以单独改变每一个片段,如添加或是删除它们。当进行这样的片段处理时,还能将片段加入一个由该活动管理的返回栈——每个活动中的返回栈条目是一段发生过的片段处理的记录。返回栈允许用户通过按下BACK键撤销一个片段事务(反向导航)。

在添加一个片段作为活动布局的一部分时,它将存在于活动的视图层级的ViewGroup中,并定义其自有的视图布局。可以通过在活动布局文件中以<fragment>元素声明片段,或是在程序代码中添加至已有的ViewGroup)来将一个片段插入活动的布局之中。不过,片段并不一定要是活动布局的一部分;还可以将片段作为一个活动的不可见部分使用。

本文档描述了如何使用片段来构建应用程序,包括片段如何在被加入活动的返回栈时维护其状态,与活动及活动内的其他片段共享事件,以及和活动的动作条相结合等。

设计理念

Android在Android 3.0(API级别“Honeycomb”蜂巢)中引入了片段,用以在平板等大屏幕上支持更为动态而灵活的UI设计。由于平板等屏幕比手机的要大许多,因此有更多的空间来交互组合UI组件。片段可以在不必考虑视图层级的复杂操作的情况下实现这种设计。通过把活动的布局分成一个个片段,就可以在运行时改变活动的外观并在该活动所管理的返回栈中保存这些变化。

例如,一个新闻程序可以在左侧用一个片段来展示条目列表,在另一边的片段中显示一条条目——两个片段同时显示在一个活动中的两边,且都有自己的生命周期回馈方法并处理其自有的用户输入事件。因此,相比一个活动选择条目另一个活动阅读条目,现在用户可以在同一个活动中选择条目并阅读,就像图1所示的那样。

图1 一个演示了如何通过片段将两个独立的UI模块并入一个活动的范例

一个片段应该是程序中的一个模块化的可重用的组件。也就是说,因为片段定义了其自有的布局以及使用了自有生命周期回馈方法的自有行为,所以可以将一个片段用在多个活动之中。这是很重要的一点,它使得能够在不同屏幕尺寸上提供不同的用户体验。例如,可以只在屏幕尺寸足够大时才在一个活动中包含多个片段,否则,就将不同片段分在不同的活动中使用。

例如——还是那个新闻程序的例子——程序可以在运行于一个超大屏幕设备(extra large screen,比如平板电脑)时将两个片段嵌入在一个Activity A中。不过,在普通尺寸屏幕的设备(例如,手机)上,就没有足够的空间放下两个片段,所以Activity A仅包含了条目列表功能的片段,而当用户选择了一个条目时,它将启动包含了阅读条目的片段的Activity B。因此,如图1所示程序将同时支持两种设计模式。

创建一个片段

要创建一个片段,必须创建一个Fragment(或它的一个已有的子类)的子类。Fragment类的代码看起来和一个Activity很相似。它包含了和一个活动类似的回馈方法,例如onCreate()、onStart()、onPause()和onStop()。事实上,如果要把一个已有的Android程序改为通过片段来实现,只需简单地把代码中的活动的回馈方法改为相应的片段的回馈方法。

通常,至少需要实现以下的生命周期方法:

onCreate()

系统在创建片段时将调用这个方法。在其实现中,应当初始化那些希望在该片段暂停或停止时被保留的必要组件以供之后继续使用。

onCreateView()

系统在片段第一次绘制其用户界面时将调用这个方法。要绘制片段的UI,就必须从这个方法返回一个片段布局的根View。如果片段不提供UI,可以只返回一个null。

onPause()

系统将调用该方法作为用户将要离开该片段的第一个标志(尽管这不意味着片段一定就会被销毁)。通常应当在这里保存当前用户进行的操作(因为用户之后或许不会返回该片段了)。

大部分的程序应当为每一个片段至少实现以上三个方法,不过还有一些其他的回馈方法可以用来处理片段生命周期的不同阶段。所有的生命周期回馈方法会在之后的“处理片段生命周期”一节再讨论。

图2. 一个片段的生命周期(当其所属的活动处于运行状态时)

除了Fragment基类之外,还有一些可供继承的子类:

DialogFragment

显示一个浮动对话框。可以用该类来创建对话框而不是用Acitivity类中的对话框辅助方法来创建,这样就可以将一个片段对话框加入由活动所管理的片段的返回栈,令用户可以返回到一个已被舍弃的片段。

ListFragment

显示一个由某一适配器(例如SimpleCursorAdapter)管理的项目列表,类似于ListActivity。它提供了好几种管理列表视图的方法,例如onListItemClick()回馈方法以处理点击事件。

PreferenceFragment(偏好设置片段)

以列表方式显示一个Preference对象的层级,类似于PreferenceActivity。当为程序创建“设置”活动时这是很有用的。

添加用户界面

一个片段通常被用于一个活动的用户界面,其自有的布局将成为该活动的一部分。

要为一个片段提供布局,就必须实现onCreateView()回馈方法,Android系统将在该片段绘制其布局时调用它。该方法的实现必须返回一个片段布局的根View。

注意:如果片段是一个ListFragment的子片段,那么默认的实现将从onCreateView()返回一个ListView,因此不必另外去实现该方法。

要从onCreateView()返回一个布局,可以从一个定义于XML中的布局资源中生成它。为此,onCreateView()提供了一个LayoutInflater对象。

例如,这里有一个Fragment的子类从example_fragment.xml文件中读取了一个布局:

public static class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.example_fragment, container, false);
    }
}

被传递给onCreateView()的container参数是上一级的ViewGroup(来自于该活动的布局),片段的布局将被插入其中。savedInstanceState参数是在返回片段时提供之前片段实例数据的一个Bundle(将在“处理片段生命周期”一节中更深入地讨论状态恢复)。

创建一个布局

在上面的范例中,R.layout.example_fragment是指向保存于程序资源中的一个名为example_fragment.xml布局资源的引用。关于如何在XML中创建一个布局的更多信息,请参见“用户界面”文档。

inflate()方法需要三个参数:

  • 希望生成的布局的资源ID
  • 要生成的布局的父级ViewGroup。为了使系统将布局参数传递给生成的布局的根视图,就需要传递该container。这将由当前正在运行的父视图来决定。
  • 一个表明了生成的布局在生成过程中是否应该和ViewGroup(即第二个参数)相关联的布尔值。(在这个例子里,因为系统已经将生成的布局插入到了container内所以值为false——如果是true则会在最终的布局中产生一个冗余的视图组。)

现在已经了解了如何创建一个提供了布局的片段。接下来,需要将该片段添加到活动中去。

向活动添加一个片段

通常,片段作为宿主活动的UI的一部分嵌于该活动整体视图层级之中。有两种方式可以将一个片段添加到活动的布局:

  • 在活动的布局文件里声明该片段。

这种情况下,如果片段是一个视图,可以为其指定布局属性。例如,这里有一个含有两个片段的活动的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment android:name="com.example.news.ArticleListFragment"
            android:id="@+id/list"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
    <fragment android:name="com.example.news.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

 <fragment>中的android:name属性指定了用来实例化布局的Fragment类。

当系统创建该活动布局时,将实例化在布局中指定的每一个片段并为其调用onCreateView()方法来检索每一个片段的布局。系统将在<fragment>元素处直接插入由该片段返回的View。

注意:每一个片段需要在系统中有一个唯一的标识用以在活动重启时还原该片段(并且用于获取该片段以执行如移除该片段之类的操作)。有三种方式提供一个片段的ID:

    • 为android:id属性提供唯一的ID。
    • 为android:tag属性提供唯一的字符串。
    • 如果没有提供以上两种标识,系统将使用其容器视图的ID。

 

  • 或者,在程序中将片段添加至已有的ViewGroup。

在活动正在运行中的任意时刻,都可以将片段添加至活动的布局。只需要指定这一需要放置片段的ViewGroup即可。

要在活动中进行片段事务(例如添加、移除或替换一个片段),必须使用FragmentTransaction所提供的API。可以像这样在Activity中获取一个FragmentTransaction的实例:

FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

之后可以用add()方法添加一个片段,指定要添加的片段以及要插入该片段的视图。例如:

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

传递给add()的第一个参数是该片段应当被放置的ViewGroup,通过资源ID来指定,第二个参数则是要添加的片段。

一旦通过FragmentTransaction进行了变更,比如调用commit()来使变更生效。

添加没有UI的片段

上面的范例演示了如何添加提供了UI的片段至活动中。不过,也可以通过使用一个片段来在活动中执行后台行为而不显示额外的UI。

要添加没有UI的片段,需要在活动中通过使用add(Fragment, String)来完成(为片段提供一个唯一的”tag”字符串而非一个视图ID)。这样就能添加该片段,但是,因为它没有和活动布局中的某个视图相关联,所以将不会收到onCreateView()的调用。因此不需要去实现这个方法。

为片段提供一个字符串标签(tag)对于无UI片段来说并不是很严格的要求——也能为包含UI的片段提供标签——但如果片段不含UI,那么字符串标签就是唯一能识别该片段的方式了。如果希望之后能从活动中获取该片段,需要使用findFragmentByTag()。

关于将片段作为后台工作部件而不含UI的活动的范例,请参见FragmentRetainInstance.java范例。

管理片段

要在活动中管理片段,需要使用FragmentManager。可以通过在活动中调用getFragmentManager()来获取它。

借助FragmentManager可以做到包括以下这些事:

  • 通过findFragmentById()(适用于在活动布局中提供了UI的片段)或findFragmentByTag()(对于提供了或没有提供UI的片段都适用)来获取已存在于活动之中的片段。
  • 通过popBackStack()将片段从返回栈中弹出(模拟了一次用户按下BACK键的指令)。
  • 通过addOnBackStackChangedListener()为返回栈的变化注册一个监听器。

关于这些方法的更多信息,请参见FragmentManager类的文档。

正如在之前一节中所描述的,还可以使用FragmentManager来打开一个FragmentTransaction,以能够执行例如添加、移除片段等事务。

执行片段事务(Fragment Transaction)

在活动中使用片段的一大特点是可以根据用户交互对这些片段进行添加、移除、替换或是执行其他操作。对活动进行的每一组改变都被称为是一次事务(transaction),可以通过FragmentTransaction提供的API执行事务。还可以把每一个事务都保存至活动所管理的返回栈,使得用户可以撤销片段的改变(和返回上一个活动类似)。

可以像这样从FragmentManager获取一个FragmentTransaction的实例:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

每一个事务都是一组能同时执行的变更。可以通过对给定操作使用如add()、remove()和replace()等方法在设置所有希望执行的变更。之后,必须对活动调用commit()来应用该事务。

不过,在调用commit()之前,需要调用addToBackStack(),以将该事务加入片段事务的返回栈中。该返回栈由活动所管理,允许用户通过按下返回键来返回到之前的片段状态。

例如,下面展示了如何替换一个片段,并将之前的状态保存于返回栈中:

// 创建新的片段和事务
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// 用该片段替换fragment_container视图中所含有的任意片段,
// 并将该事务添加至返回栈
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// 执行该事务
transaction.commit();

在这个范例里,newFragment替代了当前在由ID R.id.fragment_container定义的布局容器中的(任意的)片段。通过调用addToBackStack(),替换事务被保存于返回栈中,因此用户可以通过按下返回键来回滚事务,回到之前的片段。

如果将多次变更添加至事务(例如第二个add()或是remove())并调用addToBackStack()的话,那么所有在调用commit()之前被应用的变更都将作为一个单独的事务被加入返回栈中,BACK键将会恢复所有这些变更。

向FragmentTransaction中添加变更的顺序是无所谓的,不过:

  • 必须在最后调用commit()
  • 如果将多个片段加入同一个容器中,那么添加的顺序将决定它们在视图层级中的顺序

如果在执行移除片段的事务时没有调用addToBackStack(),那么该片段将在事务被执行(commit)后被销毁,用户无法再次返回它。反之,如果在移除片段时调用了addToBackStack(),那么该片段将被中止(stop),并在用户返回时被继续(resume)。

提示:对于每个片段事务,都可以通过在执行(commit)之前调用setTranstition()以应用一个切换动画。

调用commit()并不会立即执行事务。它只是作了在活动的UI线程(“主”线程)准备好之时运行该事务的调度。不过,如有必要,可以在UI线程中调用executePendingTransactions()来立即执行由commit()提交的事务。只有在该事务是其他线程工作的组成部分时才有必要这么做。

注意:可以仅在活动保存其状态之前(当用户离开活动时)使用commit()来执行一次事务。如果在此之后执行,将抛出一个异常。这是因为如果活动需要被储存的话,执行之后的状态将无法被保存。对于丢失状态也无妨的情况,则需使用commitAllowingStateLoss()。

和活动进行通信

尽管一个Fragment被作为一个独立于Activity的对象使用,且可以被用于多个活动之中,一个给定的片段实例可以与包含它的活动直接关联。

特别要注意的是,片段可以以getActivity()来获取Activity的实例,并能够在活动的布局中很容易地进行寻找视图之类的任务:

View listView = getActivity().findViewById(R.id.list);

类似的,活动可以通过findFragmentById()或findFragmentByTag()从FragmentManager获取一个Fragment的引用来调用片段内的方法。例如:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

创建活动接受的事件回馈

在有些情况下,可能会需要片段与活动共享事件。这可以通过在片段定义一个回馈接口并在宿主活动中实现它来实现。当活动通过该接口接收到了一个回馈时,如有需要,它可以与布局中的其他片段共享信息。

例如,如果一个新的应用程序在一个活动中有两个片段——一个用来显示一列文章(片段A)而另一个用来显示某一篇文章(片段B)——那么片段A必须在某一个列表项目被选中时告知活动,使活动能够告知片段B来显示该文章。在这种情况下,OnArticleSelectedListener接口被声明于片段A内:

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

之后持有该片段的活动实现了OnArticleSelectedListener接口并覆盖了onArticleSelected()来将来自于片段A的事件通知给片段B。为确保宿主活动实现了该接口,片段A的onAttach()回馈方法(当把片段添加至活动时系统将会自动调用该方法)通过将传递过来的Activity强制转换为了onAttach()以实例化了一个OnArticleSelectedListener的实例:

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnArticleSelectedListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
        }
    }
    ...
}

如果该活动没有实现这个接口,那么片段将会抛出一个ClassCastException。一旦成功实现了该接口,mListener成员将持有一个活动的OnArticleSelectedListener实现的引用,因此片段A将可以通过调用由OnArticleSelectedListener接口定义的方法和活动共享事件。例如,如果片段A是ListFragment的一个继承,那么每次用户点击一个列表项目时,系统将调用片段的onListItemClick(),它将之后调用onArticleSelected()来与活动共享该事件:

public static class FragmentA extends ListFragment {
    OnArticleSelectedListener mListener;
    ...
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        // Append the clicked item's row ID with the content provider Uri
        Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
        // Send the event and Uri to the host activity
        mListener.onArticleSelected(noteUri);
    }
    ...
}

传递给onListItemClick()的id参数是被点击项目的行ID,它被活动(或其他片段)用来从应用程序的ContentProvider中获取文章。

关于使用内容提供器的更多信息请参见“Content Providers文档”。

添加项目至动作条(Action Bar)

片段可以通过实现onCreateOptionsMenu()方法来和活动的选项菜单(Options Menu)项目相结合使用(因此也就可以和动作条一起使用)。不过,为了使该方法能够接受调用,必须在onCreate()中调用setHasOptionsMenu()来标识该片段将被添加至选项菜单(否则,该片段将不会接收到对onCreateOptionsMenu()的调用)

之后从片段添加至选项菜单的任何项目都将被增加到已有菜单项目之后。该片段还将在某一菜单项目被选中时收到onOptionsItemSelected()的回馈。

也可以在片段布局中注册一个视图,通过调用registerForContextMenu()来提供上下文菜单。当用户打开上下文菜单时,该片段将收到一个对onCreateContextMenu()的调用。当用户选中一个项目时,片段将收到一个对onContextItemSelected()的调用。

注意:尽管片段对每一个添加过的项目都能收到一个项目选中回馈,但活动本身将在用户选择菜单项目时首先收到相应的回馈。如果活动的项目选中回馈的实现没有处理该选中的项目,那么该事件才会被传递给片段的回馈方法。对于选项菜单和上下文菜单来说都是如此。

关于菜单的更多信息,请参见“菜单”和“动作条”开发者指南。

处理片段生命周期

管理一个片段的生命周期和管理一个活动的生命周期十分的相像。如同活动一样,一个片段可以以三种状态存在:

Resumed

该片段在运行中的活动里是可见的。

Paused

另一个活动正处于前台且获得了焦点,不过含有该片段的活动依然是可见的(前台的活动是半透明的或没有覆盖整个屏幕 )。

Stopped

片段不可见。或是宿主活动被停止了,或是片段被从活动中移除后加入了返回栈。一个被停止的片段依然是存在着的(所有的状态和成员信息由系统保持着)。不过,它对于用户不再可见,在活动被杀除后也将被杀除。

和一个活动一样,可以通过一个Bundle以在活动的进程被杀除而又需要在该活动被重建时还原该片段的情况下保存片段的状态。可以在活动的onSaveInstanceState()回馈方法中保存状态,在onCreate()、onCreateView()或onActivityCreated()中还原状态。关于保存状态的更多信息,请参见“活动”文档。

图 3. 活动生命周期对于片段生命周期的影响

一个活动和一个片段的生命周期之间最大的区别在于它们是如何被保存于各自的返回栈中的。一个活动默认为在其被停止时被放入由系统所管理的一个返回栈中(这样用户就能通过BACK键返回该活动,就像“任务和返回栈”中所说的那样)。然而, 一个片段只有在进行片段移除的事务中调用addToBackStack()来显式地请求保存片段的实例时才会被放入由宿主活动所管理的返回栈中。

除此之外,管理片段的生命周期和管理活动的生命周期十分相似。因此,管理活动生命周期时的做法同样可以应用于管理片段。所需要理解的就仅仅是活动的生命周期将会地片段的生命周期有着怎样的影响。

范例

为了总结本文档中所说的,下面有一个范例来演示一个使用了两个片段的活动是如何创建一个双面板布局的。下面的这个活动包含了一个显示一列莎士比亚戏剧标题的片段和另一个在从列表中选择项目后显示该戏剧的摘要的片段。它还演示了如何基于不同的屏幕配置来提供不同的片段配置。

注意:该活动的完整源代码可以在FragmentLayout.java中得到。

主活动按照常规的方式在onCreate()中应用布局:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.fragment_layout);
}

被应用的布局是fragment_layout.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

使用该布局,系统将在活动载入该布局时实例化TitlesFragment(它列出了戏剧标题),而FrameLayout(它显示戏剧的摘要)将占用屏幕的右侧(不过最初它是空着的)。可以发现,直到用户从列表中选择了一个项目之后某一个片段才会被放入FrameLayout。

不过,不过所有的屏幕配置都足够宽来在两边显示戏剧列表和摘要。所以上面的布局仅用于横屏模式,需要保存为res/layout-land/fragment_layout.xml。

因此,当屏幕是竖向时,系统将应用下面的保存于res/layout/fragment_layout.xml的布局:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

该布局仅包含了TitlesFragment。这意味着当设备处于竖向状态时,只有戏剧标题列表是可见的。因此,当用户在这时点击了列表项目的话,程序会启动一个新的活动来显示摘要,而不是载入第二个片段。

接下来,看一下如何在片段类中完成整个过程。首先是TitlesFragment,它显示了一列莎士比亚戏剧标题。该片段继承于ListFragment,通过它来处理大部分的列表视图工作。

正如代码中所表明的,需要注意当用户点击一个列表项目时有两种可能的行为:这取决于两种布局中的哪一种使处于激活状态的。它既可以在同一个活动中创建并表示一个新的片段来显示详细信息(将片段加入FrameLayout),也可以启动一个新的活动(并在那里显示片段)。

public static class TitlesFragment extends ListFragment {
    boolean mDualPane;
    int mCurCheckPosition = 0;

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

        // Populate list with our static array of titles.
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
        }

        if (mDualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            // Make sure our UI is in the correct state.
            showDetails(mCurCheckPosition);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("curChoice", mCurCheckPosition);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    void showDetails(int index) {
        mCurCheckPosition = index;

        if (mDualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            getListView().setItemChecked(index, true);

            // Check what fragment is currently shown, replace if needed.
            DetailsFragment details = (DetailsFragment)
                    getFragmentManager().findFragmentById(R.id.details);
            if (details == null || details.getShownIndex() != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index);

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                ft.replace(R.id.details, details);
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            Intent intent = new Intent();
            intent.setClass(getActivity(), DetailsActivity.class);
            intent.putExtra("index", index);
            startActivity(intent);
        }
    }
}

第二个片段,DetailsFragment显示了从TitlesFragment列表中所选择的戏剧项目的摘要:

public static class DetailsFragment extends Fragment {
    /**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */
    public static DetailsFragment newInstance(int index) {
        DetailsFragment f = new DetailsFragment();

        // Supply index input as an argument.
        Bundle args = new Bundle();
        args.putInt("index", index);
        f.setArguments(args);

        return f;
    }

    public int getShownIndex() {
        return getArguments().getInt("index", 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (container == null) {
            // We have different layouts, and in one of them this
            // fragment's containing frame doesn't exist.  The fragment
            // may still be created from its saved state, but there is
            // no reason to try to create its view hierarchy because it
            // won't be displayed.  Note this is not needed -- we could
            // just run the code below, where we would create and return
            // the view hierarchy; it would just never be used.
            return null;
        }

        ScrollView scroller = new ScrollView(getActivity());
        TextView text = new TextView(getActivity());
        int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics());
        text.setPadding(padding, padding, padding, padding);
        scroller.addView(text);
        text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
        return scroller;
    }
}

回忆一下TitlesFragment类,如果用户点击了一个列表项目时当前布局没有包含R.id.details视图的话(DetailsFragment属于该视图),应用程序将启动DetailsActivity活动来显示项目的内容。

下面是DetailsActivity,它仅仅内嵌了DetailsFragment以在屏幕处于竖屏模式时显示所选择的戏剧摘要:

public static class DetailsActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}

注意这个活动在横屏模式时将会自动结束,改为由主活动同时显示DetailsFragment和TitlesFragment。而在用户以竖屏模式启动DetailsActivity之后旋转屏幕至横屏模式的情况下也会如此(此时将重启当前活动)。

关于使用片段的更多范例(以及本范例的完整源文件),请参见ApiDemos里的范例代码(可以从范例SDK组件中下载)。

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

构造自定义组件

Android提供了一种高级而强大的自定义模型来构建UI,这一模型基于布局类View和ViewGroup来实现。首先,Android平台包含了一系列的预构建View和ViewGroup子类——被称为控件(widget)和布局(layout)——用以构造UI。

可用的控件包括但不限于Button、TextView、EditText、ListView、CheckBox、RadioButton、Gallery、Spinner及一些特殊用途的AutoCompleteTextView、ImageSwitcher和TextSwitecher。

可用的布局包括LinearLayout、FrameLayout、RelativeLayout等。范例请参见“常见布局对象”。

如果预构建的控件或布局无法满足需要,可以创建自有的View子类。如果只是要对现有的控件或布局作小调整,可以继承该控件或布局并覆盖其方法。

通过创建自有的View子类,将可以对于屏幕元素的外观和功能进行精确控制。要了解能够对自定义视图进行哪些控制,可以参考下面的例子:

  • 可以创建一个完全自定义渲染的View类型,例如使用2D图像渲染的有些类似于模拟电路控制的“音量控制”按键。(自己也不太清楚analog electronic control是什么,总之这个例子是说可以自定义按键的外观)
  • 可以将一组View组件结合为一个新的单独的组件,比如类似于ComboBox(一种弹出式列表和文本输入栏的结合体)的东西,或是双栏选择器(一种有左右两栏的列表,可以重新分配其中的某个项目应该属于哪一栏),等等。
  • 可以覆盖EditText组件在屏幕上的渲染方式(Notepad教程中通过这种方式创建了具有划线的记事本页面,取得了良好效果)
  • 可以捕捉例如按键等的其他事件并以自定义方式处理它们(例如在游戏中)。

下面的小节阐述了如何创建自定义View并将其用于程序之中。更为详细的参考信息,请参见View类。

基本方式

这里是对要创建自有View组件所需了解的内容的一个高度预览:

  •  用自有类继承一个View类或其子类。
  • 覆盖其中的一些方法。需要覆盖的是那些父类中以“on”开头的方法,例如,onDraw(),onMeasure()和onKeyDown()。这和在Activity或是ListActivity中覆盖那些和生命周期与功能相关on…事件相类似。
  • 使用该新类。一旦完成,自有的新类可以被用于其父类能被用于的地方。

 提示:继承的类可以在使用它们的活动中被定义。这样就限制了对其不必要的使用,很有用(因为可能会为了更广泛的用途而需要在程序中创建另一个新的公用View)。

 完全自定义的组件

完全自定义的组件可以被用来创建任何希望呈现的图形组件。也许是一个看起来像是老式类比指示条的音量指示器,或是卡拉OK机上唱歌时会有一个随着歌曲的进度弹跳着的小球的一长条的文本视图。总之,有时无论怎样组合预置组件也无法达到某种效果,因而需要另外的自定义组件。

幸运地,可以轻松地创建具有所希望的外观和行为的组件,仅有的限制只是想象力,屏幕尺寸,可用的处理器能力(要记得最终程序不得不运行于比桌面工作站性能弱很多的设备上)。

要创建一个完全自定义的组件:

  1. 也许有些出乎意料,通常要继承的视图正是View,因此创建新的组件往往以继承View类开始。
  2. 可以提供一个构造函数以从XML中获取属性和参数,也可以自己定义这些属性和参数(比如音量指示器的色彩和范围,指针的宽度和阻尼等。)
  3. 有可能需要为该组件创建自有的事件监听器,属性继承器和修改器,或是其他更为深入的功能。
  4. 通常都需要覆盖onMeasure()方法,如果希望组件显示内容,常常onDraw()方法也要覆盖。这两者都具有其默认行为,默认来说onDraw()什么也不做,而onMeasure()则总是设置尺寸为100×100——这可能不是所希望的属性。
  5. 如有需要其他的on…方法也应该被覆盖。

 继承onDraw()和onMeasure()

onDraw()方法将传递一个Canvas,在该Canvas上可以实现任何所需内容:2D图形、其他自定义组件、个性化的文本内容或其他所能想到的。

注意:这并不适用于3D图形。如果希望使用3D图形,必须继承SurfaceView而不是View,并在另一个线程中绘制。参见GLSurfaceViewActivity范例以了解更多。

onMeasure()则稍微复杂一些。onMeasure()是组件与其容器之间的一种渲染约束。它应该被覆盖以高效而准确地报告其所包含内容的尺寸度量。由于其父级(它被传递给onMeasure()方法)限制的要求,又由于一旦完成计算后需要以测量的宽度和高度来调用setMeasuredDimension(),整件事变得有些复杂。如果没能通过一个被覆盖的onMeasure()方法来调用它的话,结果将会是在测量时抛出一个例外。

从整体上来看,实现onMeasure()大致是这样的:

  1. 被覆盖的onMeasure()方法被以宽度和高度的测量值(即widthMeasureSpec以及heightMeasureSpec参数,它们都是用来表示维度的整型值)调用,这是对于要处理的宽度和高度的测量值的一种限制。这些参数能够要求的所有限制的参考资料请见View.onMeasure(int, int)相关的文档(该参考文档同时很好的解释了整个测量操作)。
  2. 组件的onMeasure()方法应当计算出渲染该组件所需的宽度和高度的测量值。它通常应当和传入的参数相匹配,不过有时也可以选择扩大(这时,父类可以根据不同的测量值选择包括切割、滚动、抛出例外或请求onMeasure()重试等一系列操作。)
  3. 一旦宽度和高度被计算出来,就必须以它们去调用setMeasuredDimension(int width, int height)。否则将导致抛出例外。

下面是关于视图的框架可能调用的其他标准方法的一些总结:

类别 方法 描述
创建 构造函数 在通过代码创建视图时和在视图通过布局文件生成时都会需要一些构造函数。第二种情况下布局文件中定义的任何属性都需要被读取并应用。
onFinishInflate() 当一个视图及其所有子视图都通过XML生成后被调用。
布局 onMeasure(int, int) 调用以决定该视图及其所有子视图所需的尺寸。
onLayout(boolean, int, int, int, int) 当该视图需要指定其所有子视图的尺寸和位置时被调用。
onSizeChanged(int, int, int, int) 当该视图的尺寸发生变化时被调用。
绘制 onDraw(Canvas) 当该视图需要渲染其内容时被调用。
事件处理 onKeyDown(int, KeyEvent) 当一个新的按键事件发生时被调用。
onKeyUp(int, KeyEvent) 当一个按键释放事件发生时被调用。
onTrackballEvent(MotionEvent) 当一个轨迹球移动事件发生时被调用。
onTouchEvent(MotionEvent) 当一个触摸屏动作事件发生时被调用。
焦点 onFocusChanged(boolean, int, Rect) 当视图获得或是失去焦点时被调用。
onWindowFocusChanged(boolean) 当包含了该视图的窗口获得或是失去焦点时被调用。
关联(Attaching) onAttachedToWindow() 当该视图与一个窗口相关联后被调用。
onDetachedFromWindow() 当该视图与窗口接触关联后被调用。
onWindowVisibilityChanged(int) 当包含了该视图的窗口的可见性发生改变时被调用。

 一个自定义视图的范例

在API Demos中的CustomView范例提供了一个自定义View的范例。该自定义View被定义在LabelView类中。

这个LabelView范例对自定义组件的各方面进行了演示:

  • 继承View类来创建一个完全自定义的组件。
  • 支持参数并可以接受视图生成参数(在XML中定义的参数)的构造函数。其中一些参数将被传递至父类View,不过更重要的是,一部分参数将被用于在LabelView中定义的属性。
  • 一些该标签组件所需的标准公有方法,比如setText(),setTextSize(),setTextColor()等等。
  • 被覆盖的onMeasure()方法以决定并设置组件的渲染尺寸。(注意在LabelView中,实际的工作是由私有的measureWidth()方法完成的。)
  • 被覆盖的onDraw()方法以将标签绘制于所提供的画布(canvas)上。

可以在范例中的custom_view_1.xml里看到一些LaberView自定义视图的用例。通常,可以看到android: namespace参数和自定义的app: namespace参数被混合使用。app: parameters是LabelView进行组织并使用的参数,被定义于该范例的R资源定义类中的风格化内部类中。

复合控制器(Compound Control)

如果不想创建一个完全自定义的组件,而是希望将一些可重用的组件组合在一起以获得一组已存在的控制功能的话,就创建一个复合组件(或称为复合控制器)吧。简言之,就是将一些更为基本的控制器(或是视图)装入一个逻辑组使之被看作为是一个单独的组件。例如,一个Combo Box可以被认为是一条单独的EditText域加上一个具有弹出式列表(PopupList)的调节按钮。如果按下按钮并从列表中进行选择,就会弹出一条EditText域,当然用户也可以直接在EditText中键入一些内容。

在Android中,其实已经有两种View可以做到这点了:Spinner和AutoCompleteTextView,不过Combo Box的概念还是可以用来做为一个简单易懂的范例的。

要创建一个复合组件:

  1. 这通常是从某种类型的Layout开始,所以需要创建一个继承了某种Layout的类。在Combo box的例子中或许应该使用一个水平方向的LinearLayout。请记得其他的布局是可以嵌套于其中的,所以复合组件可以做得相当地结构复杂。需要注意的是还可以像Activity所使用的那种方式一样通过声明式(基于XML)的方式来创建所包含的组件,或是在代码中对其进行嵌套。
  2. 在新类的构造函数中,获取父类所需的所有参数,并首先将其传递给父类的构造函数。之后可以设置其他在新的组件中所要使用的视图;在这里将创建EditText域和PopupList。请注意可能还需要向XML引入一些自有属性和参数用以被提取并使用于构造函数。
  3. 还可以为所包含的视图可能会触发的事件创建监听器,例如,一个当进行了列表选择后对EditText的内容进行更新的列表项目点击监听器(List Item Click Listener)的监听器方法。
  4. 还可以通过访问器(accessor)和修改器(modifier)创建自有的属性,例如,允许EditText的值在组件中被初始化并在需要时可以查询获取其内容。
  5. 在继承一个Layout时,不必覆盖其onDraw()和onMeasure()方法,因为该布局的默认行为就能很好的实现效果。不过,如有需要还是可以对其进行覆盖。
  6. 可以覆盖其他的on…方法,例如onKeyDown(),用于某个键被按下时从一个combo box的弹出列表中选择某一个默认值。

总之,使用Layout作为自定义控制器的基础将带来一系列的好处,包括:

  • 可以通过和活动一样的声明式的XML文件来指定布局,或是可以在代码中创建视图并将其嵌入布局中。
  • onDraw()和onMeasure() 方法(加上大部分其他的on…方法)的默认行为能正常工作因而不必进行覆盖。
  • 最后,可以很快捷地构建非常复杂的复合视图并重复利用它们。

复合控制器的范例

在SDK附带的API Demos项目中,有两个List范例——Views/Lists下的范例4和范例6演示了一个继承了LinearLayout的SpeechView,它构建了一个显示Speech引用的组件。范例代码中所用的相关的类是List4.java和List6.java。

修改一个已有的View类型

在某些情况下可以用一种更简单的创建自定义View的方法。如果已经有了一个和所希望创建的组件非常相似的组件,可以继承该组件并覆盖那些希望改变的行为。可以像对完全自定义的组件那样对这个继承的组件进行任何处理,只不过是需要从View层级中一个更为特殊化的类开始罢了。与此同时还可以额外获得其他很多所需要的功能。

例如,SDK在范例中包含了一个NotePad程序。它演示了使用Android平台的许多方面,包括继承一个EditText View来实现带有划线的记事本。这并不是一个完美的范例,且实现这个效果的API相对这个比较早期的版本可能会发生改变,不过它确实展示了一些使用原则。

如果还没有尝试过,那就将NotePad范例导入到Eclipse(或只是查看所提供的链接中的源代码)。特别是查看NoteEditor.java文件中MyEditText的定义。

其中一些要点如下。

1. 定义

该类由以下代码定义:

public static class MyEditText extends EditText

  • 它被定义为NoteEditor活动的一个内部类,不过因为它是公有的所以可以在需要时从NoteEditor外部以NoteEditor.MyEditText被使用。
  • 它是静态的,意味着它不会生成所谓的“合成方法(synthetic method)”以允许它读取其父类的数据。这同时也意味着它是一个具有独立行为的类,并没有和NoteEditor保持着高度相关性。这是创建不需要读取外部类的数据的内部类时一种比较条理清楚的做法,它使得生成的类较小且能被其他类所使用。
  • 在这里EditText被选作需要自定义的View来被它继承。当完成后,一个新的类可以用来替代通常的EditText视图。

2. 类初始化

和往常一样,super被首先调用。另外,这不是一个默认的构造函数,而是包含了一些的参数。EditText在通过XML布局文件生成时将用到这些参数,因此,新的构造函数也要获取这些参数并将其传递给父类的构造函数。

3. 覆盖方法

在该范例中,只覆盖了一个方法:onDraw() —— 不过当创建自己的自定义组件时很可能需要覆盖更多方法。

对于NotePad范例来书,覆盖onDraw()方法使得能够在EditText视图的画布(canvas)上绘制蓝线(画布被传递给了被覆盖的onDraw()方法)。super.onDraw()方法在该方法结束前会被调用。这一父类方法的调用是必需的,不过在这里,它在完成了所希望的直线的绘制后才被最后执行。

4. 使用自定义组件

现在已经有了自定义的组件,不过该如何使用呢?在NotePad范例中,自定义组件被直接从声明式的布局中使用,所以现在来看下res/layout文件夹下的note_editor.xml。

<view
  class="com.android.notepad.NoteEditor$MyEditText" 
  id="@+id/note"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:background="@android:drawable/empty"
  android:padding="10dip"
  android:scrollbars="vertical"
  android:fadingEdge="vertical" />

*该自定义组件作为通用视图在XML中被创建,且该类被指定为使用整个包。还要注意所定义的内部类通过NoteEditor$MyEditText标识来引用,这是Java编程语言中引用内部类的标准方式。

如果自定义的View组件没有作为内部类被定义,那么还可以用XML元素名称来声明该View组件而不包括class属性。例如:

<com.android.notepad.MyEditText
  id="@+id/note"
  ... />

注意现在的MyEditText类是一个单独的类文件。当这个类被嵌套于NoteEditor类中时,这样的做法将会有问题。

*定义中的其他属性和参数将被传递至自定义组件的构造函数,之后被传入EditText的构造函数,即这些也是EditText所需要使用的参数。注意还可以增加自有参数,之后还会涉及这一问题。

该范例的说明至此。确实这只是个简单的例子,但它演示了关键点——可以根据需求创建任何复杂的自定义组件。

一个更为复杂的组件可能需要覆盖更多的on…方法并且引入一些自有的方法以完全自定义其属性和行为。唯一的限制就仅仅是想象力和实际需要组件去完成怎样的工作。

 返回用户界面

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

意图和意图过滤器

一个程序的三个核心组件——活动、服务和广播接收者——通过被称为意图(intent)的消息来被激活。意图消息是一种在同一个或不同程序的组件间的运行后绑定机制。意图自身,即一个Intent对象,是包含了对于要执行的操作的抽象描述的被动数据结构——或者,对于广播来说,是对于已发生正在被播报的事件的描述。对于向不同类型的组建传递意图,有不同的机制:

  • 一个Intent对象被传递给Context.startActivity()或Activity.startActivityForResult()以启动一个活动,或是让一个已存在的活动进行新的工作。(它也可以被传递给Activity.setResult()使其向调用了startActivityForResult()的活动返回信息。)
  • 一个Intent对象被传递给Context.startService()以初始化一个服务或向正在运行的服务发出新的指令。同样的,一个意图可以被传递给Context.bindService()以在进行调用的组件和目标服务间建立联系。如果服务不在运行,还可以选择初始化该服务。
  • 被传递给任何广播方法(例如Context.sendBroadcast()、Context.sendOrderedBroadcast()或Context.sendStickyBroadcast())的Intent对象将被发送给所有相关的广播接收者。很多种类的广播在系统代码中被创建。

在任何一种情况下,Android系统将寻找合适的活动、服务或广播接收者来响应意图,在必要时对它们进行初始化。这些消息系统之间没有交集:广播意图仅被发送给广播接收者,而不会给活动或是服务。一个传递给startActivity()的意图将仅被发送给一个活动,而不会是服务或广播接收者,依此类推。

本文档将首先对Intent对象进行描述。之后介绍Android用以映射意图至组建的规则——它如何了解哪一个组建应当接收意图消息。对于没有显式地命名目标组件的意图,这个过程将包括使用与可能对象关联的意图过滤器(intent filter)来测试Intent对象。

Intent对象

Intent对象是一捆数据。它包含了接受该意图的组件所感兴趣的信息(例如要采取的操作及操作的对象数据)及Android系统所需的信息(例如要处理该意图的组件类别及如何启动目标活动的指令)。原则上,它包含以下内容:

组件名称

应当处理该意图的组件名称。这部分是一个ComponentName对象——一种目标组件的完整类名(例如“com.example.project.app.FreneticActivity”)和组件所在程序的manifest文件中的包名称组(例如“com.example.project”)的组合。组件名称的包部分和manifest中的包名称组不一定要相同。

组件名称是可选的。如果被设定,Intent对象将被发送给指定类的实例。如果没有设定,Android使用Intent对象中的其他信息来选择合适的目标——参见本文档中之后的“意图解决方案”。

组件名称通过setComponent()、setClass()或setClassName()来设定,通过getComponent()来读取。

行为

命名了要被执行的行为的字符串——或者,对于广播的意图来说将要发生并被报告的行为。Intent类定义了很多行为常量,包括:

常量 目标组件 行为
ACTION_CALL 活 动 初始化一次电话呼叫。
ACTION_EDIT 活 动 向用户显示数据以供编辑。
ACTION_MAIN 活 动 作为一个任务(task)的首活动启动,不包含数据输入且没有返回的输出。
ACTION_SYNC 活 动 以移动设备上的数据同步服务器上的数据。
ACTION_BATTERY_LOW 广播 接收者 电量低的警告。
ACTION_HEADSET_PLUG 广播接收者 耳机插入或是拔出设备。
ACTION_SCREEN_ON 广播接收者 屏幕被打开。
ACTION_TIMEZONE_CHANGED 广播接收者 时区设定被改变。

参见Intent类的描述以获知通用行为的预定义常量列表。其他的行为在Android API的其他部分被定义。也可以为激活自己的程序中的组件而定义自有的行为字符串。它们应当包含作为前缀的程序包——例如:“com.example.project.SHOW_COLOR”。

行为很大程度上决定了意图剩余部分的结构——特别是数据和额外信息——就好像是一个方法的名称决定了一组参数和返回值。因此,最好尽可能具体地使用行为名称,将它们紧密地和意图的其他部分配对。换言之,不要单独定义一个行为,而是要为组件可以处理的Intent对象定义一整套的协议。

一个Intent对象的行为通过setAction()方法设置,通过getAction()来读取。

数据

将被执行操作的数据的URI及数据的MIME类型。不同的行为和不同的数据规格相对应。例如,如果行为域是ACTION_EDIT,那数据域就需要包含要被显示以编辑的文档的URI。如果行为是ACTION_CALL,那数据域需要是一个要拨打的号码的tel: URI。同样地,如果行为是ACTION_VIEW且数据域是http: URI,那接收方活动将会下载或显示该URI指向的任何数据。

在配对一个意图和一个有能力处理其数据的组件时,知道数据的类型(即它的MIME类型)以及URI是很重要的。例如,一个可以显示图像数据的组建不应被调用来播放音频文件。

在许多情况下,数据类型可以由URI获知——特别是content: URI,它表明数据位于设备中并由一个内容提供者所控制(参见另外的关于内容提供者的解说)。不过也可以在Intent对象内显式地设定类型。setData()方法以仅URI来指定数据,setType()仅以MIME类型来指定数据,setDataAndType()同时以URI和MIME类型来指定数据。URI通过getData()读取,类型由getType()读取。

类别

一个包含了应该由哪一种类型的组件来处理意图的额外信息的字符串。Intent对象中可以存放任何数量的类别描述。和行为一样,Intent类定义了一些类别常量,其中包括:

常量 意义
CATEGORY_BROWSABLE 目标活动可以安全地被浏览器用于显示某一链接指向的数据——例如,一幅图像或是一条电子邮件消息。
CATEGORY_GADGET 该活动可以被另一个持有小部件(gadget)的活动嵌于内部。
CATEGORY_HOME 该活动显示主界面(home screen)这一用户启动设备或按下HOME键后首先看到的画面。
CATEGORY_LAUNCHER 该活动是一个任务中的首活动,被列于应用程序启动器的首级。
CATEGORY_PREFERENCE 目标活动是一个偏好设置面板。

 参见Intent类的描述以获知完整的类别列表。

addCategory()方法将一个类别存入一个Intent对象,removeCategory()则删除之前添加的一个类别,getCategories()用于获取当前在意图对象内的整个类别集。

额外信息

关于需要被传递给要处理意图的组件的其他信息的键值对(key-value pair)。如同一些行为与特定类型的数据URI相配对,另一些会与特定的额外信息相配对。例如,一个ACTION_TIMEZONE_CHANGED意图有一个额外信息“time-zone”来识别新的时区,ACTION_HEADSET_PLUG则有一个额外信息“state”来识别耳机是被插入还是拔出,另外还有额外信息“name”来识别耳机的类型。如果想要设计一个SHOW_COLOR行为,颜色的值将被设置于一个额外的键值对之中。

Intent对象有一系列的put…()方法来插入不同类型的额外数据,类似地有一组get…()方法来读取这些值。这些方法和Bundle对象中的相类似。事实上,额外信息可以如同一个Bundle那样通过putExtras()和getExtras()方法来被设置和读取。

旗标

各种类型的旗标。其中很多指示了Android系统如何去启动一个活动(例如,该活动应该属于哪一个任务)以及在启动后应该如何处理它(例如,它是否应该属于最近活动列表)。所有的这些旗标在Intent类中被定义。

Android系统和该平台的应用程序使用Intent对象来发出系统创建的广播及激活系统定义的组件。要了解如果构建一个意图以激活一个系统组件,参见附录中的意图列表。

意图的解决方案

意图可以被分为两类:

  • 显式意图通过其名称指定目标组件(前面提到过,该组件的名称域有一个值集)。因为组件的名称通常不会被其他程序的开发者知道,显式的意图往往被用于程序内部消息——例如一个活动启动一个附属的服务或是一个平级的活动。
  • 隐式意图不会命名目标(组件的名称域是空白的)。隐式意图常用于激活其他活动中的组件。

Android将一个显式意图传递给所指定的目标类的一个实例。只有Intent对象中的组件名称会影响到意图最终将被传送给哪一个组件。

对于隐式意图需要一种不同的策略。由于没有指定的目标,Android系统必须找到最为合适的那一组件(或那一些组件)——一个单独的执行所请求的行动的活动或服务,或是一组广播接收者来响应广播通告。系统通过比较Intent对象的内容和意图过滤器(和组件相关联的用以接收潜在意图的结构)来实现这一目的。过滤器告知了一个组件的功能并界定了它可以处理的意图。它们使得组件可能接收所告知的类型的隐式意图。如果一个组件没有任何的意图过滤器,它将只能接收显示意图。一个拥有意图过滤器的组件可以同时接收显式和隐式意图。

当一个Intent对象被意图过滤器检查时只有三个方面会被考虑:

行为

数据(同时包括URI和数据类型)

类别

额外信息和旗标不会影响到决定由哪个组件接收意图。

意图过滤器(Intent filter)

为了告知系统它可以处理哪些隐式意图,活动、服务和广播接收者都可以拥有一个或多个意图过滤器。每一个过滤器将描述组件的某一种功能以及该组件要接收的意图集。它有效地过滤下所需的类型,排除不需要的意图——不过仅能排除不需要的(未命名目标类的)隐式意图。显式意图将始终被传递给其目标而无论它包含什么内容;过滤器不会起作用。然而一个隐式意图就只有在通过了组件的过滤器之后才能够被传递给组件。

一个组件对每一类它可以处理的工作及用户所能看见的每一种形式分别有着不同的过滤器。例如,范例中Note Pad程序的NoteEditor活动有两个过滤器——一个用于创建用户可以查看并编辑的特定便筏,另一个用于启动一个空的用户可以输入并保存的新的便筏。(Note Pad的所有的过滤器将会在之后的Note Pad范例一节中详细说明。)

一个意图过滤器是IntentFilter类的一个实例。不过因为Android系统必须在启动一个组件之前知道它所具有的功能,所以意图过滤器一般不在Java代码中使用,而是作为程序的manifest文件中(AndroidManifest.xml)的<intent-filter>元素。(不过有一个例外是广播接收者的过滤器是通过调用Context.registerReceiver()动态地在代码中注册的;它们被作为IntentFilter对象直接创建。)

一个过滤器有着和Intent对象中的行为、数据和类别域相对应的域。一个隐式意图将检测过滤器的所有这三个内容。要被传递给某个组件,一个意图必须通过该组件的过滤器的所有三项检测。即使只有一项不满足,Android系统也不会将该意图传递给组件——至少不会是基于过滤器传递给组件。不过,由于一个组件可以有多个意图过滤器,一个意图如果无法通过其中一个过滤器还是有可能能通过其他的的。

过滤器和安全

意图过滤器不能确保安全性。尽管它只让一个组件接受特定类型的隐式意图,它无法阻止显式意图指向目标组件。即使过滤器约束了意图使其只能使用将会被请求处理特定的行为和数据源的组件,一些人仍然可以将一个显式意图与不同的行为和数据源结合在一起,并将目标以组件的名称来命名。

下面将详细描述三种检测是如何进行的:

行为检测

manifest文件中的一个<intent-filter>元素以<action>子元素的形式列出了行为。例如:

<intent-filter . . . >
    <action android:name="com.example.project.SHOW_CURRENT" />
    <action android:name="com.example.project.SHOW_RECENT" />
    <action android:name="com.example.project.SHOW_PENDING" />
    . . .
</intent-filter>

如同示例所展示的,一个Intent对象只命名一个单独的行为,而一个过滤器可以列出多个行为。列表不能为空;一个过滤器至少要包含一个<action>元素,否则它将屏蔽所有的意图。

要通过这个检测,在Intent对象中指定的行为必须与过滤器所列出的行为之一相匹配。如果该对象或是过滤器没有指定一个行为,就会有下面的结果:

  • 如果过滤器没有列出任何行为,就没有行为可以与意图相匹配,所有的意图都无法通过检测。没有意图可以通过过滤器。
  • 另一方面,如果Intent对象没有指定任何的行为,它将自动通过检测——只要过滤器含有一个或以上的行为。

类别检测

<intent-filter>也会将类别作为子元素列出。例如:

<intent-filter . . . >
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    . . .
</intent-filter>

注意之前描述过的表示行为和类型的常量不在mainfest文件中被使用。而是使用完整的字符串值。例如,在范例中的“android.intent.category.BROWSABLE”字符串和文档之前提到的CATEGORY_BROWSABLE常量相对应。类似地,字符串“android.intent.action.EDIT”和ACTION_EDIT常量相对应。

一个意图要通过类别检测,Intent对象中的每一个类别都必须与过滤器中的某个类别相匹配。过滤器可以列出其他更多的类别,但不能省略任何一个意图中含有的类别。

原则上,所以说,一个不含有类别的Intent对象,无论过滤器中有哪些类别,总是能够通过这项检测。这通常是正确的。不过,有一个例外,Android把所有传递给startActivity()的隐式意图视为它们包含了至少这样一个类别:“android.intent.category.DEFAULT”(CATEGORY_DEFAULT常量)。因此,要接受隐式意图的活动必须在其意图过滤器中包含有“android.intent.category.DEFAULT”。(设有“android.intent.action.MAIN”和“android.intent.category.LAUNCHER”的过滤器不在此范围之中。它们将活动标记为了新任务的开始,并显示在应用启动器(launcher)屏幕上。它们可以在类别列表中包含“android.intent.category.DEFAULT”,不过这并不是必须的。)参见之后的“使用意图匹配”来进一步了解这些过滤器。

数据检测

如同行为和类别,意图过滤器的的数据类型也是作为子元素保存的。而且和它们一样,这些子元素可以出现多次或完全不出现。例如:

<intent-filter . . . >
    <data android:mimeType="video/mpeg" android:scheme="http" . . . /> 
    <data android:mimeType="audio/mpeg" android:scheme="http" . . . />
    . . .
</intent-filter>

每一个<data>元素可以指定一个URI和数据类型(MINE媒体类型)。URI的每一个部分由不同的属性——模式(scheme)、主机(host)、接口(port)和路径(path)。

scheme://host:port/path

例如,下面的URI,

content://com.example.project:200/folder/subfolder/etc

其模式是“content”,主机是“com.example.project”,接口是“200”,路径是“folder/subfolder/etc”。主机和接口共同构成了URI的授权;如果没有指定主机,那么接口将被忽略。

每一个属性都是可选的,但并非与其他属性相互独立:要让一个授权变得有意义,必须指定一个模式。要让路径具有意义,必须指定一个模式和一个授权。

当一个Intent对象中的URI和过滤器中的URI相比较时,仅比较过滤器中所包含的URI部分。例如,如果一个过滤器仅指定了一个模式,那所有具有这个模式的URI就与过滤器相匹配。如果过滤器指定了一个模式及一个授权但是没有指定路径,那么无论是什么路径,具有相同模式和授权的URI将得到匹配。如果过滤器指定了模式、授权和路径,那就只有具有相同模式、授权和路径的URI得到匹配。不过,过滤器中指定的路径可以包含通配符以仅仅限定部分路径。

一个<data>元素的类型(type)属性指定了数据的MINE类型。在过滤器中这比URI更为常见。Intent对象和过滤器都可以用”*”通配符作为子类别域来标识子类别匹配,例如“text/*”或“audio/*”。

数据检测同时将Intent对象中的URI和数据类型与过滤器中的URI和数据类型相比较。该过程遵循以下规则:

  • 不包含URI和数据类型的Intent对象只有在过滤器也不指定任何URI及数据类型时才能通过检测。
  • 包含URI但不包含数据类型(且数据类型不能通过URI推断出来)的Intent对象只有在其URI与过滤器中的URI相匹配且过滤器同样没有指定类型时才能通过检测。这是仅仅在类似mailto:和tel:这样没有指向实际数据的URI时才会出现的情况。
  • 包含数据类型但不包含URI的Intent对象只有在过滤器列出了相同的数据类型而没有指定URI时才能通过检测。
  • 同时包含有URI和数据类型(或URI可以推导出数据类型)的Intent对象在其数据类型与过滤器中列出的类型相匹配时通过检测的数据类型部分。当其URI与过滤器中的URI相匹配,或,其具有一个content:或file:URI并且过滤器没有指定URI时,通过测试的URI部分。换言之,如果过滤器仅列出了数据类型,一个组件将被假定支持content:和file:数据。

如果一个意图可以通过不止一个活动或服务的过滤器,用户将被询问要激活哪一个组件。如果没有一个目标能被找到,则会抛出一个例外。

常见情况

上面数据检测中的最后一条规则,说明了组件可以从一个文件或是内容提供者处获取本地数据。因此,它们的过滤器可以仅仅列出数据类型而不需要显式地命名content:和file:模式。这是一种典型的情况。比如说,一个如下的<data>元素,告诉Android组件可以从内容提供者处获取图像数据并显示:

<data android:mimeType="image/*" />

因为大部分可用的数据由数据提供者分发,所以指定了数据类型而没有指定URI的过滤器也许是最为常见的了。

另一种常见的情况是过滤器指定了模式和数据类型。例如,如下的一个<data>元素告诉Android组件可以从网络获取视频数据并播放:

<data android:scheme="http" android:type="video/*" />

试想,例如,浏览器类应用在用户打开网页上的一个链接后将做什么。它首先尝试显示数据(如果该链接是一个HTML页面的话)。如果它无法显示数据,它将生成一个包含模式和数据类型的隐式意图并试图启动一个可以处理该工作的活动。如果没有合适的活动,它将请求下载管理器下载该数据。这处于内容提供者的控制之下,所以有大量的可能合适的活动(它们的过滤器仅指定了数据类型)将能够做出响应。

大部分程序还有一种不需要任何指定数据引用的开始刷新的方法。能够初始化一个程序的活动有着将行为指定为”android.intent.action.MAIN”的过滤器。如果它们需要被显示在应用程序启动器中的话,它们还需要被指定为”android.intent.category.LAUNCHER”类别:

<intent-filter . . . >
    <action android:name="code android.intent.action.MAIN" />
    <category android:name="code android.intent.category.LAUNCHER" />
</intent-filter>

使用意图匹配

将意图与意图过滤器相匹配不仅仅是为了寻找要激活的某一目标组件,同时也是要获取设备上的某一组组件的某些属性。例如,Android系统通过查找所有具有指定了“android.intent.action.MAIN”行为和“android.intent.category.LAUNCHER”类别的过滤器的活动(如前一节所示)来弹出应用程序启动器这一列出了所有可供用户启动的程序的顶层屏幕内容。之后它在启动器中显示那些活动的图标(icon)和标签(label)。同样地,它通过查找具有“android.intent.category.HOME”类别的过滤器的活动来显示主界面。

程序可以以一种类似的方式来使用意图匹配。PackageManager拥有一组query…()方法来返回所有可以接受某一特定意图的组件,以及一系列的resolve…()方法来决定响应意图最合适的组件。例如,queryIntentActivities()将返回一个列出了所有可以将意图作为参数传递的活动的列表,queryIntentServices()将返回一个类似作用的所有服务的列表。这两种方法都不会激活组件;它们只是列出了那些可以做出响应的。对于广播接收者,还有一个类似的queryBroadcastReceivers()方法。

记事本(Note Pad)范例

记事本范例允许用户浏览笔记列表、查看列表中项目的详细内容、编辑项目、向列表增加新项目。本节将分析在其manifest文件中声明的意图过滤器(如果正在离线使用SDK,可以在<sdk>/samples/NotePad/index.html下找到包括manifest文件在内的该范例程序的所有源文件。如果是在在线查看文档,源文件则是在这里的“教程和范例代码”一节。

在manifest文件中,记事本程序声明了三个活动,每一个都有至少一个意图过滤器。它还声明了一个内容提供者用以管理笔记数据。以下是整个manifest文件的内容:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.android.notepad">
    <application android:icon="@drawable/app_notes"
                 android:label="@string/app_name" >

        <provider android:name="NotePadProvider"
                  android:authorities="com.google.provider.NotePad" />

        <activity android:name="NotesList" android:label="@string/title_notes_list">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.EDIT" />
                <action android:name="android.intent.action.PICK" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.GET_CONTENT" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
            </intent-filter>
        </activity>

        <activity android:name="NoteEditor"
                  android:theme="@android:style/Theme.Light"
                  android:label="@string/title_note" >
            <intent-filter android:label="@string/resolve_edit">
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.EDIT" />
                <action android:name="com.android.notepad.action.EDIT_NOTE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.INSERT" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
            </intent-filter>
        </activity>

        <activity android:name="TitleEditor" 
                  android:label="@string/title_edit_title"
                  android:theme="@android:style/Theme.Dialog">
            <intent-filter android:label="@string/resolve_title">
                <action android:name="com.android.notepad.action.EDIT_TITLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.ALTERNATIVE" />
                <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
                <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
            </intent-filter>
        </activity>

    </application>
</manifest>

第一个活动,NoteList,和其他的活动不同,它要对一系列的笔记(笔记列表)进行操作而不是某条单独的笔记。它通常将作为用户进入程序的初始界面。如其三个意图过滤器所描述的,它能执行三项工作:

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

该过滤器声明了Note Pad程序的主入口。标准MAIN行为是一种不需要Intent中的其他信息(比如不需要数据类型)的入口(entry point),LAUNCHER类别则说明该入口应该被列于应用程序启动器中。

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.EDIT" />
    <action android:name="android.intent.action.PICK" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
</intent-filter>

该过滤器声明了这个活动可以对一列笔记进行何种操作。它可以允许用户查看或编辑笔记(通过VIEW和EDIT行为),或是从列表中选取一条特定笔记(通过PICK行为)。

<data>元素的mimeType属性指定了这些行为要执行操作的数据类型。它表明了这个活动可以从一个持有Note Pad数据的内容提供器(vnd.google.note)中获取一个指向零个或更多个项目(vnd.android.cursor.dir)的Cursor。启动该活动的Intent对象包含一个content: URI用以指定活动应该打开的确切数据类型。

也请注意该过滤器中提供的DEFAULT类别,因为Context.startActivity()和Activity.startActivityForResult()方法在意图包含DEFAULT类别时将会对其进行处理——只有两种例外:

  • 显式地命名了目标活动的Intent
  • 包含了MAIN行为和LAUNCHER类别的Intent

因此,所有的过滤器都需要DEFAULT类别——除非有MAIN行为和LAUNCHER类别。(意图过滤器不过滤显式意图)

<intent-filter>
    <action android:name="android.intent.action.GET_CONTENT" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
</intent-filter>

该过滤器说明了这个活动可以返回一条用户选中的笔记而不需要这条笔记所在的列表的相关信息。GET_CONTENT行为类似于PICK行为。两种情况下活动都将返回用户所选择的笔记的URI。(将返回给调用了startActivityForResult()来启动NoteList活动的那个活动。)不过现在,调用者指定了所需的数据类型而不是用户要进行选择的数据列表的类型。

数据类型,vnd.android.cursor.item/vnd.google.note,标明了该活动可以返回的数据的类型——一条单笔记的URI。通过所返回的URI,调用者可以从持有Note Pad数据(vnd.google.note)的内容提供其中获取某一特定条目(vnd.android.cursor.item)的Cursor。

也就是说,前一个过滤器中的PICK行为,数据类型标识出了活动可以显示给用户的数据的类型。而GET_CONTENT过滤器则标识出了活动可以返回给它的调用者的数据的类型。

有了这些能力,下面的意图可以实现活动NotesList的功能:

行为:android.intent.action.MAIN

不需要特定的数据就能启动活动

行为:android.intent.action.MAIN 

类别:android.intent.category.LAUNCHER

不需要特定的被选择的数据就能启动活动。这是实际上应用程序启动器用来填充其最高层列表(top-level list)的意图。所有具有匹配该行为和类别的过滤器的活动将被添加至列表。

行为:android.intent.action.VIEW 

数据:content://com.google.provider.NotePad/notes

请求活动显示在content://com.google.provider.NotePad/notes下的所有笔记的列表。之后用户可以浏览该列表并获取列表中项目的信息。

行为:android.intent.action.PICK 

数据:content://com.google.provider.NotePad/notes

请求活动显示在content://com.google.provider.NotePad/notes下的笔记的列表。之后用户可以从列表中选取一条笔记,活动将返回该条目的URI给启动了NoteList活动的那个活动。

行为:android.intent.action.GET_CONTENT 

数据类型:vnd.android.cursor.item/vnd.google.note

请求活动提供一条单独的Note Pad数据。

第二个活动,NoteEditor,向用户显示一条单独的笔记条目并允许对其进行编辑。根据其两个意图过滤器的描述,它可以进行两项任务:

<intent-filter android:label="@string/resolve_edit">
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.EDIT" />
    <action android:name="com.android.notepad.action.EDIT_NOTE" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
</intent-filter>

这个活动的第一个,也是主要的目的是使用户与一条笔记交互,VIEW这条笔记或是EDIT它。(类别EDIT_NOTE与EDIT的意义相同。)该意图将包含匹配MIME类型vnd.android.cursor.item/vnd.google.note的数据的URI——也就是说,这条指定的笔记的URI。它通常是被NoteList活动的PICK或GET_CONTENT行为所返回的URI。

和之前一样,该过滤器列出了DEFAULT类别因此这个活动可以被其他没有显式地指定NoteEditor类的意图启动。

<intent-filter>
    <action android:name="android.intent.action.INSERT" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
</intent-filter>

这个活动的第二个目的是使用户创建一条新的笔记并INSERT至现有的笔记列表中。该意图包含了匹配MIME类型vnd.android.cursor.dir/vnd.google.note的数据的URI——即笔记将被插入的笔记列表的URI。

有了这些能力,下面的意图可以实现NoteEditor活动的功能:

行为:android.intent.action.VIEW 

数据:content://com.google.provider.NotePad/notes/ID

请求活动显示由ID确定的笔记的内容。(关于content: URIs是如何指定一个群组内的个别成员的,请参见“内容提供器”。)

行为:android.intent.action.EDIT 

数据:content://com.google.provider.NotePad/notes/ID

请求活动显示由ID确定的笔记的内容,并允许用户对其进行编辑。如果用户保存了修改,该活动将更新内容提供器中的笔记数据。

行为:android.intent.action.INSERT 

数据:content://com.google.provider.NotePad/notes

请求活动在content://com.google.provider.NotePad/note的笔记列表中创建一个新的空笔记并允许用户对其进行编辑。如果用户保存了笔记,其URI将被返回给调用者。

最后一个活动,TitleEditor,允许用户编辑笔记的标题。这可以通过直接调用该活动来实现(通过显式地在Intent中设定该组件名称),而不需要通过一个意图过滤器。不过这里主要关注如何对现存的数据进行其他操作:

<intent-filter android:label="@string/resolve_title">
    <action android:name="com.android.notepad.action.EDIT_TITLE" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.ALTERNATIVE" />
    <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
    <data android:mimeType="vnd.android.cursor.item/vnd.google.note" />
</intent-filter>

这个活动的唯一的意图过滤器使用了一个名为”com.android.notepad.action.EDIT_TITLE“的自定义行为。它必须像之前的VIEW和EDIT行为一样通过一条指定的笔记调用(数据类型vnd.android.cursor.item/vnd.google.note)。不过,这个活动将显示包含于笔记数据中的标题,而不是笔记内容自身。

另外为了支持常见的DEFAULT类别,这个标题编辑器也支持其他两种常见类别:ALTERNATIVE和SELECTED_ALTERNATIVE。这两个类别表明了活动可以在一个选项菜单中显示给用户(就好象LAUNCHER类别表明了活动应当在应用程序启动器中显示给用户一样)。注意过滤器也支持一个显式标签(通过android:label=”@string/resolve_title”)以更好地控制用户能够看到当前被这个活动以其他行为方式查看的数据的哪些内容。(关于这些类别和创建选项菜单的更多信息,请参见PackageManager.queryIntentActivityOptions()和Menu.addIntentOptions()方法。)

有了这些能力,下面的意图就可以实现TitleEditor活动的功能:

行为:com.android.notepad.action.EDIT_TITLE 

数据:content://com.google.provider.NotePad/notes/ID

请求活动显示和笔记ID相关联的标题,并允许用户对其进行编辑。

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

媒体

Android平台为多种常见媒体类型提供了内建的编码/解码支持,因而可以简单地向程序整合音频、视频和图像。要使用平台的媒体功能非常简单——同样只需使用意图-活动的机制,之后Android会处理其余的工作。

Android可以从多种数据源类型中播放音频和视频。可以播放存储在程序资源(原始资源 raw resource)中的音频或视频媒体文件,可以播放文件系统中的某一文件,可以播放经由网络的流数据。要让程序播放视频或音频,需使用MediaPlayer类。

如果移动设备硬件支持的话,平台也支持录制音频和视频。要录制音频或视频,需使用MediaRecorder类。注意,模拟器没有获取音频或视频的硬件,不过实际的移动设备通常会提供这些支持,可以通过MediaRecorder类使用。

要了解Android提供内建支持的媒体格式列表,参见附录“Android媒体格式”。

音频和视频播放

可以播放任意来源的媒体:原始资源、系统中的文件或是可用的网络(URL)。

仅可以通过标准输出设备播放音频数据;目前来说,就是至移动设备的扬声器或是蓝牙耳机。现在不能在通话音频中播放声音文件。

播放原始资源

最为常见的需求或许就是从自有程序中播放媒体(一般是声音)了吧。这很容易做到:

  1. 将声音(或其他媒体资源)文件放入工程的res/raw文件夹,Eclipse插件(或aapt)将会发现它并将其识别为R类可以引用的资源。
  2. 创建一个MediaPlayer的实例,用MediaPlayer.create引用该资源,之后调用实例的start()方法:
  MediaPlayer mp = MediaPlayer.create(context, R.raw.sound_file_1);
    mp.start();

要停止播放,调用stop()。如果希望之后重放该媒体,就必须在再次调用start()之前reset()及prepare()该MediaPlayer对象。(create()在第一次使用时会调用prepare()。)

要暂停播放,调用pause()。在想要继续播放的时候调用start()。

播放文件或流

可以播放储存于文件系统中或位于网络URL的媒体文件:

  1. 使用new来创建一个新的MediaPlayer实例
  2. 以一个包含了要播放的文件的路径(文件地址或是URL)的字符串调用setDataSource()
  3. 首先prepare()之后start()该实例:
   MediaPlayer mp = new MediaPlayer();
   mp.setDataSource(PATH_TO_FILE);
    mp.prepare();
    mp.start();

stop()和pause()的工作方式同上。

注意:当引用的文件不存在时,IllegalArgumentException 和IOException 可能会在使用setDataSource()时被接收或传递。

注意:如果传递的是在线媒体文件的URL,该文件必须可以被逐步下载。

播放JET内容

Android平台包含了一个JET引擎,它允许在程序中添加与JET音频内容的播放控制交互。可以使用SDK附带的JetCreator授权程序创建JET内容的播放控制交互。要在程序中播放并管理JET内容,需要使用JetPlayer类。

关于JET理念的描述及使用JetCreator授权工具的使用说明,参见“JetCreator用户手册”。该工具在OS X和Windows平台下全功能可用,在Linux下所有的内容创建功能也都被支持,只是输入内容审查功能不被支持。

这里有一个如何从一个储存于SD卡的.jet文件中建立JET播放的例子:

JetPlayer myJet = JetPlayer.getJetPlayer();
myJet.loadJetFile("/sdcard/level1.jet");
byte segmentId = 0;

// queue segment 5, repeat once, use General MIDI, transpose by -1 octave
myJet.queueJetSegment(5, -1, 1, -1, 0, segmentId++);
// queue segment 2
myJet.queueJetSegment(2, -1, 0, 0, 0, segmentId++);

myJet.play();

SDK包含了一个程序范例——JetBoy——演示了如何使用JetPlayer创建游戏中的音乐音轨播放交互。它同时也展示了如何使用JET事件来同步音乐播放和游戏逻辑。该程序位于<sdk>/platforms/android-1.5/samples/JetBoy。

音频录制

通过设备录制音频要比播放音频/视频稍微复杂一些,不过其实也是相当简单:

  1. 使用new来创建android.media.MediaRecorder的一个新的实例
  2. 使用MediaRecorder.setAudioSource()来设置音频源。通常会使用MediaRecorder.AudioSource.MIC
  3. 使用MediaRecorder.setOutputFormat()来设置输出文件格式
  4. 使用MediaRecorder.setOutputFile()来设置输出文件名
  5. 使用MediaRecorder.setAudioEncoder()来设置音频编码
  6. 对MediaRecorder实例调用MediaRecorder.prepare()
  7. 调用MediaRecorder.start()以开始音频录制
  8. 调用MediaRecorder.stop()以结束音频录制
  9. 当不再需要MediaRecorder实例时,对其调用MediaRecorder.release()。建议调用MediaRecorder.release()来立即释放资源
范例:录制音频并播放录制的音频

下面的这个范例类演示了如何建立、开始及终止音频录制,之后播放录制的音频文件。

/*
 * The application needs to have the permission to write to external storage
 * if the output file is written to the external storage, and also the
 * permission to record audio. These permissions must be set in the
 * application's AndroidManifest.xml file, with something like:
 *
 * <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 * <uses-permission android:name="android.permission.RECORD_AUDIO" />
 *
 */
package com.android.audiorecordtest;

import android.app.Activity;
import android.widget.LinearLayout;
import android.os.Bundle;
import android.os.Environment;
import android.view.ViewGroup;
import android.widget.Button;
import android.view.View;
import android.view.View.OnClickListener;
import android.content.Context;
import android.util.Log;
import android.media.MediaRecorder;
import android.media.MediaPlayer;

import java.io.IOException;

public class AudioRecordTest extends Activity
{
    private static final String LOG_TAG = "AudioRecordTest";
    private static String mFileName = null;

    private RecordButton mRecordButton = null;
    private MediaRecorder mRecorder = null;

    private PlayButton   mPlayButton = null;
    private MediaPlayer   mPlayer = null;

    private void onRecord(boolean start) {
        if (start) {
            startRecording();
        } else {
            stopRecording();
        }
    }

    private void onPlay(boolean start) {
        if (start) {
            startPlaying();
        } else {
            stopPlaying();
        }
    }

    private void startPlaying() {
        mPlayer = new MediaPlayer();
        try {
            mPlayer.setDataSource(mFileName);
            mPlayer.prepare();
            mPlayer.start();
        } catch (IOException e) {
            Log.e(LOG_TAG, "prepare() failed");
        }
    }

    private void stopPlaying() {
        mPlayer.release();
        mPlayer = null;
    }

    private void startRecording() {
        mRecorder = new MediaRecorder();
        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        mRecorder.setOutputFile(mFileName);
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

        try {
            mRecorder.prepare();
        } catch (IOException e) {
            Log.e(LOG_TAG, "prepare() failed");
        }

        mRecorder.start();
    }

    private void stopRecording() {
        mRecorder.stop();
        mRecorder.release();
        mRecorder = null;
    }

    class RecordButton extends Button {
        boolean mStartRecording = true;

        OnClickListener clicker = new OnClickListener() {
            public void onClick(View v) {
                onRecord(mStartRecording);
                if (mStartRecording) {
                    setText("Stop recording");
                } else {
                    setText("Start recording");
                }
                mStartRecording = !mStartRecording;
            }
        };

        public RecordButton(Context ctx) {
            super(ctx);
            setText("Start recording");
            setOnClickListener(clicker);
        }
    }

    class PlayButton extends Button {
        boolean mStartPlaying = true;

        OnClickListener clicker = new OnClickListener() {
            public void onClick(View v) {
                onPlay(mStartPlaying);
                if (mStartPlaying) {
                    setText("Stop playing");
                } else {
                    setText("Start playing");
                }
                mStartPlaying = !mStartPlaying;
            }
        };

        public PlayButton(Context ctx) {
            super(ctx);
            setText("Start playing");
            setOnClickListener(clicker);
        }
    }

    public AudioRecordTest() {
        mFileName = Environment.getExternalStorageDirectory().getAbsolutePath();
        mFileName += "/audiorecordtest.3gp";
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        LinearLayout ll = new LinearLayout(this);
        mRecordButton = new RecordButton(this);
        ll.addView(mRecordButton,
            new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                0));
        mPlayButton = new PlayButton(this);
        ll.addView(mPlayButton,
            new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                0));
        setContentView(ll);
    }

    @Override
    public void onPause() {
        super.onPause();
        if (mRecorder != null) {
            mRecorder.release();
            mRecorder = null;
        }

        if (mPlayer != null) {
            mPlayer.release();
            mPlayer = null;
        }
    }
}

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

安全性和许可

Android是一个权限分离(privilege-separated)的操作系统,其中的每一个运行的程序有一个单独的系统识别(identity)(Linux 用户ID及组ID)。此外系统的不同部分也被分入了不同的识别。通过这种方式Linux使不同的程序和系统之间相互独立。

其他的高级安全特性也由一种称为“权限”的机制所提供,它强行规定了一个进程可以执行哪些特定操作,同时URI级别的权限(per-URI permission)担保了对于特定数据片段的ad-hoc访问。

安全构架

Android安全构架的一个中心设计要点就是默认来说没有程序有进行任何可能对其他程序、操作系统或是用户产生负面影响的任何操作的许可(permission)。这包括了读写用户私人数据(例如通讯录或是电子邮件),读写其他程序的文件,执行网络接入或保持设备不进入睡眠状态等。

因为内核将程序通过沙盒分离,所以程序必须显式地共享资源及数据。它们通过声明许可来获取它们所需的没有通过基本沙盒提供的额外功能。程序静态地声明它们所需的许可,Android系统将在程序安装时向用户征求许可。Android没有动态(在运行时)担保许可的机制,因为这将使安全性受损,使得用户体验变糟。

只有内核负责将程序通过沙盒与其他程序分离。尤其要注意Dalvik VM以及任何可以运行原生代码(native code)的程序(参见Android NDK)并不是安全边界。所有类型的程序——Java,原生(native)或是混合(hybrid)——都以同样方式被沙盒化,相互之间拥有相同的级别的安全性。

应用程序签名

所有的Android应用程序(.apk文件)必须用一个开发者拥有其私钥的认证(certificate)来进行签名。该认证用于识别应用程序的权限。一个认证不需要由认证权威来进行签名:Android程序完全可以,而且通常也会,使用自签名的认证。Android的认证的目的是为了识别应用程序作者。这使得系统可以允许或是拒绝程序获取签名级别(signature-level)的许可以及程序希望获取“相同作者程序”的Linux识别的请求。

用户ID与文件读取

在安装时,Android分给每一个包(package)一个独自的Linux用户ID。该识别将在这个包在设备上的整个周期内保持不变。在不同的设备上,同一个包可能会有不同的UID;不过重要的是在给定的某一个设备上每一个包都有一个独自的UID。

因为安全强化实在进程级别进行的,所以两个不同包的代码无法正常运行于同一个进程,他们需要作为不同的Linux用户运行。可以在每一个包的AndroidManifest.xml的manifest标签中使用sharedUserId属性来将其标为相同的用户ID。这样一来,这两个包就被认为是同一个程序,有同样的用户ID和文件权限了。注意为了保持安全性,只有两个有同样签名(且请求了同样的sharedUserId)的程序才会被分配同样的用户ID。

一个程序存储的任何数据会被分配以这个程序的用户ID,而无法被其他包正常访问。当使用getSharedPreferences(String, int),openFileOutput(String, int)或openOrCreateDatabase(String, int, SQliteDatabase.CursorFactory)来创建新文件时,可以用MODE_WORLD_READABLE和/或MODE_WORLD_WRITEABLE 旗标来允许其他包读/写该文件。当设置了这些旗标后,文件仍然属于原来的程序,但是因为设置了全局读/写许可,所以对任何其他程序都可见。

使用许可

一个基本的Android程序没有与之关联的许可,这意味着它无法进行任何可能影响用户体验或是影响设备上任何数据的操作。要利用这些设备上受保护的特性,就必须在AndroidManifest.xml文件中包含一个或更多的<uses-permission>标签来申明程序所需的许可。

例如,一个需要监视收到的短信的程序应该指定:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.app.myapp" >
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

在程序安装时,基于用户对声明了许可和/或交互的程序签名的检查,包安装器(package installer)将会认可程序所请求的许可。在程序运行时不会进行任何检查:要么在安装时许可被允许而可以如所希望的那样使用该特性,要么许可不被认可,任何使用该特性的操作将不经过用户判断而直接拒绝。

通常许可错误会导致一个SecurityException被抛回程序。然而,这也不是在所有情况下都会发生的事。例如,sendBroadcast(Intent)方法会在方法调用被返回之后检查数据被传递至每一个接收者的许可,因此即使有许可错误,也不会收到异常。不过,在绝大多数情况下,许可失败都将会被打印于系统日志上。

Android系统所提供的许可可以在Manifest.permission中找到。任何程序也可以定义并执行其自有许可,因此那并非是所有可用许可的完整列表。

一个特定的许可可以在程序执行时被执行于多个地方:

  • 在系统执行一个调用时,阻止应用程序执行某一功能。
  • 在启动一个活动时,阻止应用程序启动属于其他程序的活动。
  • 在发送和接收广播时,控制广播的接收方和发送方。
  • 在读取并操作内容提供者时。
  • 在绑定或启动一个服务时。

声明并执行许可

要执行自有许可,就必须首先在AndroidManifest.xml中通过一个或多个<permission>标签来声明它们。

例如,如果一个程序希望可以控制谁能启动它的活动,它可以像这样为这个操作声明一个许可:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.me.app.myapp" >
    <permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
        android:label="@string/permlab_deadlyActivity"
        android:description="@string/permdesc_deadlyActivity"
        android:permissionGroup="android.permission-group.COST_MONEY"
        android:protectionLevel="dangerous" />
    ...
</manifest>

<protectionLevel>属性是必需的,它会按链接文档中描述的那样告诉系统用户将如何被告知程序需要该许可,或是谁将被允许拥有这个许可。

<permissionGroup>属性是可选的,仅仅被用来帮助系统向用户显示该许可。通常会对一个标准系统组(standard system group)(列于android.Manifest.permission_group)或在某些特殊情况下对自己定义的系统组设置该属性。推荐使用一个已有的组,以简化向用户显示的许可界面。

注意必须同时向许可提供标签(label)和描述(description)。它们是用户在查看一列许可(android:label)或某一许可的详细内容(android:description)时将被显示的字符串资源。标签应当简短,用几个词描述该许可所保护的主要功能。描述可以是说明该许可允许持有者能做什么事的几句句子。习惯上用两句话来描述,第一句描述该许可,第二句告知用户当程序获得该许可后将有哪些可能的不良后果。

这是一个CALL_PHONE许可的标签和描述的例子:

   <string name="permlab_callPhone">directly call phone numbers</string>
    <string name="permdesc_callPhone">Allows the application to call
        phone numbers without your intervention. Malicious applications may
        cause unexpected calls on your phone bill. Note that this does not
        allow the application to call emergency numbers.</string>

可以通过shell指令adb shell pm list permissions来查看当前系统中所定义的许可。一般来说,’-s’选项将会像展示给用户看到的那样展示这些许可。

$ adb shell pm list permissions -s      
All Permissions:

Network communication: view Wi-Fi state, create Bluetooth connections, full
Internet access, view network state

Your location: access extra location provider commands, fine (GPS) location,
mock location sources for testing, coarse (network-based) location

Services that cost you money: send SMS messages, directly call phone numbers

...

在AndroidManifest.xml中执行许可

限制系统或程序的整体组件接入的高级别许可可以在AndroidManifest.xml中被应用。所需要做的只是将android:permission属性包含于所希望的组件之中,并为将被用于控制接入的许可命名。

Activity许可(应用于<activity>标签)限制了谁可以启动相关的活动。该许可在Context.startActivity()和Activity.startActivityForResult()的过程中会被检查;如果调用者没有所需的许可,该调用将抛出SecurityException。

Service许可(应用于<service>标签)限制了谁可以启动或是绑定相关的活动。该许可在Context.startService()、Context.stopService()和Context.bindService()的过程中会被检查;如果调用者没有所需的许可,该调用将抛出SecurityException。

BroadcastReceiver许可(应用于<receiver>标签)限制了谁可以向关联的接收者发送广播。该许可在Context.sendBroadcast()返回后系统试图向给定接收者传递所请求的广播时被检查。因此,许可错误不会向调用者抛出例外;它只是不会传递该意图。同样地,该许可可以用于Context.registerReceiver()控制谁可以向一个在代码中注册的接收者发送广播。另一方面,该许可也可以用于在调用Context.sendBroadcast()时限制哪一个BroadcastReceiver对象可以被允许接收广播(见下文)。

ContentProvider许可(应用于<provider>标签)限制了谁可以读取内容提供者内的数据。(内容提供者拥有一项重要的额外安全机制,允许它们调用URI许可。这将在之后说明。)和其他组件不同,内容提供者可以设置两个不同的属性:android:readPermission限制了谁可以读取该提供者,而android:writePermission限制了谁可以进行写操作。注意,如果一个提供者同时被读和写许可所保护,仅持有写许可并不意味着就可以读取提供者的内容。许可将在第一次检索一个提供者时和对提供者执行操作时被检查(如果没有持有任何一个许可,则将会抛出一个SecurityException)。使用ContentResolver.query()来请求持有读许可;使用ContentResolver.insert()、ContentResolver.update()和ContentResolver.delete()来请求写许可。在所有这些情况下,未持有所需的许可将导致调用抛出SecurityException 。

在发送广播时执行许可

另外,对于限制谁可以向一个注册的BroadcastReceiver发送Intent的许可(如同之前所描述的),还可以在发送广播时指定一个必需的许可。通过调用Context.sendBroadcast()及一个许可字符串,可以要求一个接收者程序必须持有该许可以接收广播。

注意,接收者和广播者都可以请求许可。在这时,Intent必须通过对两种许可的检查才能被传送至相关联的目标。

其他许可执行

强制的细化许可可以对任意服务调用执行。这通过Context.checkCallingPermission()方法执行。以一段所需的许可字符串调用后它将会返回一个整型值,显示该许可是否已被当前调用的进程所许可。注意这只能在执行来自另一进程中的调用时使用(这通常要通过一个由服务发布的IDL接口或是某些其他方法来实现)。

还有许多其他有用的检查许可的方法。如果拥有另一进程的pid,就可以用Context方法的Context.checkPermission(String, int, int)来检查违反该pid的许可。如有有另一个程序的包名,就可以直接用PackageManager方法PackageManager.checkPermission(String, String)来查看特定的包是否已经被某一指定许可所认可。

URI许可

在使用内容提供者时,至今所描述的标准许可系统常常是不足以胜任的。内容提供者可能需要读写许可以保护自己,同时它的直接客户端也需要处理其他程序的特殊URI以供执行操作。典型的例子是邮件程序中的附件。应当通过许可来保护对邮件的访问,因为这是用户敏感数据。不过,如果一个指向图像附件的URI被交给了图像查看器,该查看器将不会有打开附件的许可,因为它没有理由持有访问所有电子邮件的许可。

这个问题的解决方案是单URI许可(per-URI permission):当启动一个活动或是向一个活动返回结果时,调用者可以设置Intent.FLAG_GRANT_READ_URI_PERMISSION和/或Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这将许可接收方访问Intent内的指定数据URI,而不论它是否拥有任何访问和Intent相对应的内容提供者中数据的许可。

这一机制使得常见的能力-风格模型(capability-style model)得以实现,用户交互操作(打开附件,从列表中选择联系人等)可以创建临时的细化许可权限。这是减少程序所需许可至仅有和其行为直接相关许可的工具。

然而,对细化URI许可的授权也需要一些拥有那些URI的内容提供者的配合。强烈建议内容提供者应用这一工具,通过android:grantUriPermissions属性或是<grant-uri-permissions>标签来声明它们支持这一特性。

更为详细的信息可以在Context.grantUriPermission()、Context.revokeUriPermission()和Context.checkUriPermission()方法中找到。

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

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)。
// Get a Spinner and bind it to an ArrayAdapter that 
// references a String array.
Spinner s1 = (Spinner) findViewById(R.id.spinner1);
ArrayAdapter adapter = ArrayAdapter.createFromResource(
    this, R.array.colors, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
s1.setAdapter(adapter);

// Load a Spinner and bind it to a data query.
private static String[] PROJECTION = new String[] {
        People._ID, People.NAME
    };

Spinner s2 = (Spinner) findViewById(R.id.spinner2);
Cursor cur = managedQuery(People.CONTENT_URI, PROJECTION, null, null);

SimpleCursorAdapter adapter2 = new SimpleCursorAdapter(this,
    android.R.layout.simple_spinner_item, // Use a template
                                          // that displays a
                                          // text view
    cur, // Give the cursor to the list adapter
    new String[] {People.NAME}, // Map the NAME column in the
                                         // people database to...
    new int[] {android.R.id.text1}); // The "text1" view defined in
                                     // the XML template

adapter2.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
s2.setAdapter(adapter2);

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

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

处理用户选择

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

// Create a message handling object as an anonymous class.
private OnItemClickListener mMessageClickedHandler = new OnItemClickListener() {
    public void onItemClick(AdapterView parent, View v, int position, long id)
    {
        // Display a messagebox.
        Toast.makeText(mContext,"You've got an event",Toast.LENGTH_SHORT).show();
    }
};

// Now hook into our object and set its onItemClickListener member
// to our class handler object.
mHistoryView = (ListView)findViewById(R.id.history);
mHistoryView.setOnItemClickListener(mMessageClickedHandler); 

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

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

进程和线程

在一个程序的仅有的一个应用程序组件启动时(除此之外没有其他组件在运行),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中:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

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

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

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

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

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

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

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

使用AsyncTask

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

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

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

public void onClick(View v) {
    new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    /** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    }

    /** The system calls this to perform work in the UI thread and delivers
      * the result from doInBackground() */
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}

现在不但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许可协议进行许可。