Action Bar(操作栏)

操作栏

操作栏是一种能标识用户位置、提供用户操作和导航模式的窗口特征。使用操作栏能让你的系统优雅的适配不同的屏幕配置,给你的用户在整个应用中提供熟悉的界面。


图1. 一个包含了 [1] 应用图标,[2] 两个功能项,[3] 更多操作的操作栏。

操作栏提供了一些关键功能:

  • 分配一个专用空间为你的应用提供一致性并标识用户在应用中的位置。
  • 用可预测的方式突出重要的操作并且容易访问(比如搜索框)。
  • 在应用中提供一致的导航和视图切换效果(使用选项卡和下拉列表)。

更多关于操作栏的交互模式和设计指南,请参考 Action Bar 设计指南。

虽然 ActionBar API直到Android 3.0(API等级11)才被引入,但是为了兼容Android 2.1(API等级7)和以上的系统,提供了可用的 Support Library

这篇指南主要讲述如何使用支持库中的操作栏,但是如果你的应用只支持Android 3.0或更高的版本,你应该使用框架提供的 ActionBar。他们大部分的API都是一样的,但是属于不同的包命名空间,还有些例外,比如下面的章节中提到的方法名和签名。

注意: 请确定你从合适的包中引入了正确的 ActionBar 类 (和相关的API):

  • 如果支持的API等级小于11:
    import android.support.v7.app.ActionBar
  • 如果仅支持API等级11或更高的:
    import android.app.ActionBar

注解:如果你想知道关于上下文操作栏显示上下文操作项的信息,请参考 Menu 指南。

添加操作栏


就像上面提到的,这篇指南主要讲述如何使用支持库中 ActionBar 的API。所以在你添加操作栏之前,你必须按照 Support Library Setup 中说明的为你的项目设置 appcompat v7 支持库。

一旦你的项目设置好支持库,下面介绍如何添加操作栏:

  1. 通过继承 ActionBarActivity 来创建你的activity。
  2. 对你的activity使用或继承 Theme.AppCompat 主题中的一种。例如:
    <activity android:theme="@style/Theme.AppCompat.Light" ... >

现在当你的应用运行在Android 2.1(API等级7)或以上的系统时,你的activity就包含了操作栏。

API等级11或更高时

当 targetSdkVersion 或 minSdkVersion 属性被设置为 "11" 或更高时,activity默认情况下使用 Theme.Holo 主题,任何使用这种主题(或他们的子类)的activity都拥有操作栏。

移除操作栏

你可以在运行时调用 hide()方法来隐藏操作栏。例如:

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

API等级11或更高时

调用 getActionBar() 方法来获取 ActionBar

当操作栏隐藏时,系统会调整你的布局去填充可用的屏幕空间。你可以调用 show() 方法重新显示操作栏。

请注意当隐藏或移除操作栏时会导致activity为了填充操作栏占用的空间去重新排版布局。如果你的activity经常会隐藏或显示操作栏,你可能希望去开启overlay 模式。Overlay模式模糊化activity布局的顶部位置并在activity布局前面绘制操作栏。这样,你的布局不管操作栏隐藏或重现都是固定的。为你的activity创建一个自定义的主题并且把 windowActionBarOverlay 属性置为 true 就能启用overlay模式了。更多详情,请参考下面关于 Styling the Action Bar 的章节。

使用商标而不是图标

默认情况下,系统在操作栏里使用的是在 <application> or <activity> 元素里 icon 属性指定的图标。然而,如果你同时指定了 logo 属性,那么操作栏将会使用商标而不是图标。

商标通常应该比图标宽,除此之外不应该包括不必要的文字。你通常应该在使用用户可以识别的传统方式展示你的品牌时才使用商标。一个很好的事例是YouTube应用的商标,商标代表了用户期望的品牌,然而应用的图标被修改成方形的以符合启动图标的要求。

添加功能项


图2. 包含了三个功能项和更多功能按钮的操作栏。

操作栏为用户提供了应用当前上下文相关的最重要功能项的访问方式。那些在操作栏里直接显示的带或不带文字的图标被叫做功能按钮。无法安装在操作栏或不重要的功能被隐藏在更多操作选项中。用户可以按下右边的更多操作按钮(如果设备有菜单按钮的话,也可以按下菜单按钮)显示其他的功能列表。

当你的activity启动时,系统通过调用activity的 onCreateOptionsMenu() 方法填入功能项。使用这个方法去扩充一个定义了所有功能项的 menu resource。例如,下面是一个定义了一些菜单项的菜单资源:

res/menu/main_activity_actions.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/action_search"
          android:icon="@drawable/ic_action_search"
          android:title="@string/action_search"/>
    <item android:id="@+id/action_compose"
          android:icon="@drawable/ic_action_compose"
          android:title="@string/action_compose" />
</menu>

然后在activity的 onCreateOptionsMenu() 方法里把菜单资源扩充到给定的 Menu 中以便把每一个功能项添加到操作栏中:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu items for use in the action bar
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main_activity_actions, menu);
    return super.onCreateOptionsMenu(menu);
}

要把一个菜单项作为一个功能按钮直接显示在操作栏上,请在 <item> 标签里包含showAsAction="ifRoom"。 例如:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_search"
          android:icon="@drawable/ic_action_search"
          android:title="@string/action_search"
          yourapp:showAsAction="ifRoom"  />
    ...
</menu>

如果在操作栏中没有足够的空间显示项目的话,它将在更多操作中显示。

使用支持库中的XML属性

注意上面的showAsAction属性使用的定义在<menu>标签中的自定义命名空间。因为这些属性在旧设备的Android框架中并不存在,那么在使用支持库里定义的任何XML属性时这么做还是很有必要的。所以你必须为所有支持库定义的属性使用你自己的命名空间作为前缀。

如果你的菜单项同时拥有标题和图标属性,那么默认情况下功能项只显示图标。如果你想显示文本标题,把"withText"添加到showAsAction属性里即可。例如:

<item yourapp:showAsAction="ifRoom|withText" ... />

注解: 对于操作栏来说"withText"意味着文本标题应该显示出来,操作栏会尽可能的显示出标题,但是当图标可用并且操作栏没有剩余空间时不会显示标题。

尽管你不想功能项带着标题显示出来,你还是应该总是为每一个功能项定义标题,原因如下:

  • 如果在操作栏里无法为功能项提供足够的空间,那么菜单项将会放在更多操作里并且只会显示标题。
  • 视力障碍读者可以读出菜单项的标题。
  • 如果功能项仅显示图标的话,那么当用户长按功能项时会显示描述这个功能项的短语。

虽然图标是可选的,但是我们建议使用。关于图标设计建议,请参考 Iconography 设计指南。你也可以从这个 Downloads 页面下载一些标准操作栏图标合集(比如搜索栏和放弃键)。

你也可以使用"always"来声明一个功能项始终作为功能按钮来展示。然而,通过这种方式你并不能使一个功能项强制出现在操作栏里。 在拥有狭窄屏幕的设备上这么做会产生一些布局问题。最好使用"ifRoom"去请求系统把一个功能项显示在操作栏中,这样可以允许系统在空间不足的情况下把功能项移动到更多操作选项里。然而,如果功能项包含一个不能收缩的 action view 并且这个功能项为了提供关键功能的入口必须总是可见的话,使用"always"就很必要了。

处理功能项的点击

当用户按下一个功能项时,系统将会调用activity里的 onOptionsItemSelected() 方法。系统会传递 MenuItem 对象给这个方法,这样你可以通过调用它的 getItemId() 方法来区分功能项。它会返回<item>标签的id属性指定了的唯一的ID,所以你能执行合适的操作。例如:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // 处理操作栏功能项的点击
    switch (item.getItemId()) {
        case R.id.action_search:
            openSearch();
            return true;
        case R.id.action_compose:
            composeMessage();
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

注解:如果你在fragment中通过 Fragment 类的 onCreateOptionsMenu() 方法扩展菜单项,系统会在用户选中其中菜单项时调用 onOptionsItemSelected() 回调。然而,activity拥有优先处理事件的机会, 所以系统会在fragment调用 onOptionsItemSelected() 前,优先调用activity里相同名字的回调。为了确保activity里的任何frament都同样拥有处理回调的机会,当你不想处理这个项目时请总是把调用传递给它的父类作为默认的处理方式而不是返回false 。


图3. 实物模型展示了带选项卡的操作栏(左),分离操作栏(中)和不带应用图标和标题的操作栏(右)。

使用分离操作栏

当activity运行在狭窄的屏幕上时(比如垂直方向的手机),分离操作栏可以在屏幕底部提供一个能显示所有功能项的分离栏。

这样分离操作栏可以确保在狭窄的屏幕上用合理的空间去显示所有的功能项,顶部剩余的空间可以显示导航和标题元素。

当使用支持库时想开启分离操作栏,你必须做下面两件事:

  1. 在 <application> 元素或每个 <activity> 元素里添加uiOptions="splitActionBarWhenNarrow"。这个属性仅在API等级14或更高的系统上支持(旧版本会忽略这个属性)。
  2. 为了支持旧版本,在每个 <activity> 元素里添加 <meta-data> 子元素,并且声明"android.support.UI_OPTIONS"的值为splitActionBarWhenNarrow

例如:

<manifest ...>
    <activity uiOptions="splitActionBarWhenNarrow" ... >
        <meta-data android:name="android.support.UI_OPTIONS"
                   android:value="splitActionBarWhenNarrow" />
    </activity>
</manifest>

如果你移除了操作栏的图标和标题(如图3右侧所示),使用分离操作栏可以允许 navigation tabs 收缩在主操作栏中。要达到这种效果,请分别调用 setDisplayShowHomeEnabled(false) 和 setDisplayShowTitleEnabled(false) 方法使操作栏的图标和标题不可用。

使用应用图标向上导航


图4. Gmail中的向上按钮

使用应用图标作为向上导航按钮可以使用户基于界面间的层次关系操作你的应用。举个例子,如果界面A显示一个项目列表并且选择一个项目时会通向界面B,那么界面B应该包含用来返回界面A的向上的按钮。

注解:向上导航与系统回退键提供的后退导航功能不同。回退键被用来在用户最近使用过的历史界面里以倒序的方式进行导航,这是基于界面间的临时关系而不是应用的层级结构(向上导航的基础正是应用的层级结构)。

调用 setDisplayHomeAsUpEnabled() 可以将应用的图标作为向上按钮使用。例如:

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

    ActionBar actionBar = getSupportActionBar();
    actionBar.setDisplayHomeAsUpEnabled(true);
    ...
}

现在操作栏里的图标旁会出现一个向上的符号(如图4所示)。然而,默认它不会有任何效果。要指定当用户按下向上按钮时应该打开哪个activity的话,你有下面两种选择:

  • 在清单文件中指定它的父级activity。

    父级activity总是一样时这种是最好的选择。通过在清单文件中声明父级activity,操作栏可以在用户按下向上按钮时自动执行正确的操作。

    从Android 4.1(API等级16)开始,你可以在 <activity> 元素中使用 parentActivityName 属性来指明父级activity。

    要使用支持库去支持旧设备,同样要包含一个用来指定指定父级activity的 <meta-data> 元素,并且指定的父级activity用来作为android.support.PARENT_ACTIVITY的值。例如:

    <application ... >
        ...
        <!-- 主activity(没有父级activity) -->
        <activity
            android:name="com.example.myfirstapp.MainActivity" ...>
            ...
        </activity>
        <!-- 主activity的子级activity -->
        <activity
            android:name="com.example.myfirstapp.DisplayMessageActivity"
            android:label="@string/title_activity_display_message"
            android:parentActivityName="com.example.myfirstapp.MainActivity" >
            <!-- 父级activity元数据用来支持API等级7或以上的版本 -->
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.myfirstapp.MainActivity" />
        </activity>
    </application>

    一旦像这样在清单文件中指定了父级activity并且通过 setDisplayHomeAsUpEnabled() 启用了向上按钮后,你的工作就完成了,操作栏会正确的向上导航。

  • 或者,重写activity中的 getSupportParentActivityIntent() 和 onCreateSupportNavigateUpTaskStack()
  • 取决于用户如何到达当前界面的,父级activity也可能不同,那么这么做就很合适了。也就是说,如果用户有许多可以到达当前界面的路径,那么向上按钮应该按照用户实际到达的路劲回退导航。

    当用户按下向上按钮时系统会调用 getSupportParentActivityIntent() 去导航你的应用(在你应用自己的任务栈内)。取决于用户如何到达当前位置的,向上导航上应该打开的activity也会不同,所以你应该重写这个方法以返回可以打开合适父级activity的 Intent

    当用户按下向上按钮并且你的activity也运行在不属于你应用的任务栈中时,系统会为你的activity调用 onCreateSupportNavigateUpTaskStack()。因此,当用户向上导航时你必须使用 TaskStackBuilder 传递到这个方法去建立那个应该合并的合适的回退栈。

    虽然你重写了 getSupportParentActivityIntent() 来具体指定用户在应用中的具体导航,但是你不想实现 onCreateSupportNavigateUpTaskStack(),那么你可以像上面所示在清单文件中声明“默认”父级activity达到同样的效果。然后 onCreateSupportNavigateUpTaskStack() 的默认实现会基于清单中声明的父级activity去合并回退栈。

注解:如果你是使用大量的fragment而不是activity去构建你应用层级结构,那么上面的选项都不会生效。要在fragment里向上导航,你应该改为重写 onSupportNavigateUp(),通常通过调用 popBackStack() 方法把当前fragment从回退栈里出栈去来执行合适的fragment事务。

更多关于实现向上导航的信息,请阅读 Providing Up Navigation

添加操作视图


图5. 带可收缩 SearchView 的操作栏。

操作视图是一种显示在操作栏中并可以作为操作按钮替代品的小部件。操作视图可以在不替换操作栏、不改变activity和fragment的的情况下提供快速访问富操作入口。例如,如果你有搜索的操作,你可以如图5所示在操作栏里把一个  SearchView 控件嵌入到操作视图中。

可以使用actionLayoutactionViewClass属性去分别指定使用的布局资源或控件类。例如,下面介绍如何添加 SearchView 控件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_search"
          android:title="@string/action_search"
          android:icon="@drawable/ic_action_search"
          yourapp:showAsAction="ifRoom|collapseActionView"
          yourapp:actionViewClass="android.support.v7.widget.SearchView" />
</menu>

注意上面的showAsAction属性里也包含了"collapseActionView"值。这是用来声明操作视图应该被收缩到一个按钮中的可选操作。(在后面关于 Handling collapsible action views 的章节里会进一步解释这种用法。)

如果你需要配置操作视图(例如添加事件监听器),你可以在 onCreateOptionsMenu() 回调里处理。你可以传递相应的 MenuItem 给静态方法 MenuItemCompat.getActionView() 去获取操作视图的对象。例如,可以这样获取上面例子的搜索控件:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_activity_actions, menu);
    MenuItem searchItem = menu.findItem(R.id.action_search);
    SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
    // 配置搜索框和添加事件监听器
    ...
    return super.onCreateOptionsMenu(menu);
}

API等级11或更高

调用对应 MenuItem 的 getActionView() 方法获取操作视图:

menu.findItem(R.id.action_search).getActionView()

更多关于使用搜索控件的信息,请参考 Creating a Search Interface

处理可收缩的操作视图

为了维护操作栏的空间,你可以把你的操作视图收缩到一个操作按钮中。当收缩时,系统可能会把操作按钮放到更多操作内,但是当用户选择操作时操作视图还是会在操作栏中显示。如上面XML中所示,你可以通过在showAsAction属性里添加"collapseActionView"来收缩你的操作视图。

因为系统在用户选择操作时会扩展操作视图,所以你不需要在 onOptionsItemSelected() 回调中响应这个操作。系统还是会调用 onOptionsItemSelected(),但是如果你返回了true(表明你已经处理了事件)那么操作视图就不会扩展了。

当用户按下向上按钮或回退按钮时系统同样会收缩你的操作视图。

如果你需要基于操作视图的可见性来更新activity,可以通过定义一个 OnActionExpandListener 并把它传递给 setOnActionExpandListener() 方法来在操作视图扩展和收缩时接收到回调。例如:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.options, menu);
    MenuItem menuItem = menu.findItem(R.id.actionItem);
    ...

    // 当使用支持库时,setOnActionExpandListener()方法是静态的并且接受MenuItem对象作为参数
    MenuItemCompat.setOnActionExpandListener(menuItem, new OnActionExpandListener() {
        @Override
        public boolean onMenuItemActionCollapse(MenuItem item) {
            // 当收缩时执行
            return true;  // 返回ture去收缩操作视图
        }

        @Override
        public boolean onMenuItemActionExpand(MenuItem item) {
            //当扩展时执行
            return true;  // 返回true去扩展操作视图
        }
    });
}

添加操作提供者


图6.  操作栏被  ShareActionProvider  扩充来展示分享目标。

与 action view 相似,操作提供者使用自定义的布局去替换操作按钮。然而,不像操作视图,操作提供者控制所有的操作行为并且可以在按下时显示子菜单。

要想声明操作提供者,在菜单<item>标签里使用 ActionProvider 类的完全限定名填充到actionProviderClass属性里。

你可以继承 ActionProvider 类来构建你自己的操作提供者,不过Android已经提供了一些预构建的操作提供者,例如 ShareActionProvider,通过直接在操作栏里(如图6所示)展示可以分享的应用列表来提供分享操作。

因为每个 ActionProvider 类已经定义过它自己的操作行为了,所以你不必在 onOptionsItemSelected() 内监听这些操作。如果需要的话,比如你需要同时执行另外的操作,你还是可以在 onOptionsItemSelected() 中监听这些点击事件。但是为了使操作提供者依旧能接收到 onPerformDefaultAction() 回调来执行它的预定操作请确保返回值的是false。

然而,如果操作提供者提供的是操作子菜单,那么你的activity在用户打开这个列表或选择子菜单中的一项时不会接收到 onOptionsItemSelected() 的调用。

使用ShareActionProvider

使用 ShareActionProvider 类去定义<item>里的一个名为actionProviderClass的标签,这样你就能用 ShareActionProvider 去添加一个“分享”操作了。例如:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_share"
          android:title="@string/share"
          yourapp:showAsAction="ifRoom"
          yourapp:actionProviderClass="android.support.v7.widget.ShareActionProvider"
          />
    ...
</menu>

现在操作提供者控制着操作项并且处理它的外观与行为。除此之外你必须为每一个将要在更多操作中用到的操作项提供一个标题。

剩下唯一一件要做的事是定义你想要用来分享的 Intent。编辑 onCreateOptionsMenu() 方法,传递一个持有操作提供者的 MenuItem 对象给 MenuItemCompat.getActionProvider()方法,然后传递一个带有合适内容的在返回的 ACTION_SEND intent给 ShareActionProvider 上的 setShareIntent() 方法。

你应该在 onCreateOptionsMenu() 内调用一次 setShareIntent() 去初始化分享操作。但是因为用户上下文可能会变,所以一旦分享上下文改变时,你必须重新调用 setShareIntent() 去更新intent。

例如:

private ShareActionProvider mShareActionProvider;

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_activity_actions, menu);

    // 设置ShareActionProvider的默认分享intent
    MenuItem shareItem = menu.findItem(R.id.action_share);
    mShareActionProvider = (ShareActionProvider)
            MenuItemCompat.getActionProvider(shareItem);
    mShareActionProvider.setShareIntent(getDefaultIntent());

    return super.onCreateOptionsMenu(menu);
}

/** 定义一个默认的(虚拟的)分享intent去初始化操作提供者。
  * 然而,一旦在intent里实际使用的上下文变化了,你必须再次调用mShareActionProvider.setShareIntent()去更新分享intent
  */
private Intent getDefaultIntent() {
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("image/*");
    return intent;
}

现在 ShareActionProvider 处理所有与操作项的用户交互并且你不必要在 onOptionsItemSelected() 回调方法里处理点击事件。

默认情况下,ShareActionProvider 基于用户选择的频率对每个分享目标项进行排名。越常用的分享目标项出现在下拉列表越靠近顶端的位置,并且最常用的直接作为默认分享目标出现在操作栏上。默认情况下,排名信息被保存在 DEFAULT_SHARE_HISTORY_FILE_NAME 指定文件名的私有文件中。如果你仅仅为一种操作类型使用 ShareActionProvider 或它的扩展,那么你应该继续使用默认的历史文件而不用做任何事。然而,如果你为语义不同的多样的操作类型使用 ShareActionProvider 或它额扩展,那么为了维持他们各自的历史需要为每一种 ShareActionProvider 指定它们自己的历史文件。调用 setShareHistoryFileName() 并传递一个XML文件名(例如,"custom_share_history.xml")就能为 ShareActionProvider 指定一个不同的历史文件。

注解:尽管 ShareActionProvider 是基于使用分享目标项的频率进行排名,这种处理行为还是可以扩展的,ShareActionProvider 的扩展可以执行不同的处理方式并基于历史文件进行排名(在适当情况下)。

创建自定义操作提供者

创建你自己的操作提供者可以使你在独立的模块里重用和管理动态的操作项,而不是在activity或fragment代码里处理操作项的转变和行为。就像在前面章节中展示的,Android已经为分享操作提供了一个 ActionProvider 的扩展:ShareActionProvider

为不同的操作创建你自己的操作提供者,你只需要继承 ActionProvider 类并实现适当的回调方法即可。最重要的是,你应该继承下列方法:

ActionProvider()
这个构造方法传递给你应用的 Context,你应该把它保存在成员变量里以便在其他回调方法里使用。
onCreateActionView(MenuItem)
在这里为这个项目定义操作视图。使用从构造方法里获得的 Context 去实例化 LayoutInflater 并使用XML资源去扩充你的操作视图布局,然后挂载事件监听器。例如:
public View onCreateActionView(MenuItem forItem) {
    // 扩充要在操作栏展示的操作视图。
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    View view = layoutInflater.inflate(R.layout.action_provider, null);
    ImageButton button = (ImageButton) view.findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 一些处理...
        }
    });
    return view;
}
onPerformDefaultAction()
当用户选择了更多操作里的菜单项时系统将会调用这个方法,并且操作提供者应该为菜单项执行默认的操作。

然而,如果你的操作提供者支持子菜单,那么通过  onPrepareSubMenu() 回调,当操作提供者被放置在更多操作内时,子菜单实际上还是显示的。因此,当有子菜单时 onPerformDefaultAction() 绝对不会被调用。

注解:实现 onOptionsItemSelected() 的activity或fragment可以通过处理菜单项被选中事件(并且返回true)来重写操作提供者的默认行为(除非它使用了子菜单),在这种情况下,系统不会调用 onPerformDefaultAction()

ActionProvider 扩展的例子,请参考 ActionBarSettingsActionProviderActivity

添加导航选项卡


图7. 宽屏上的操作栏选项卡

图 8. 窄屏上的选项卡

操作栏上的选项卡可以使用户很方便的在你应用里不同的页面间浏览与切换。ActionBar 支持选项卡是非常明智的,因为他们可以适配不同的屏幕尺寸。例如,当屏幕足够宽时,选项卡出现在操作栏里的操作按钮旁边(如图7所示,比如在平板上),然而当在一个狭窄的屏幕上时,他们出现在一个分离的操作栏上(如图8所示,被称为“堆叠的操作栏”) 。在某些情况下,Android系统为了保证选项卡在操作栏上的最佳摆放会以下拉列表形式去展示选项卡。

首先,你的布局必须包含一个 ViewGroup,用来放置选项卡相关的 Fragment。确保 ViewGroup 拥有一个资源ID以便你能在代码里引用它并在里面切换选项卡。另一种情况,如果选项卡的内容将会填充整个activity布局,那么activity完全不需要设置布局(你甚至不需要调用 setContentView()), 也就是说你可以把每个fragment放置在默认根视图(通过资源IDandroid.R.id.content可以获取跟视图的引用)中。

一旦你决定了fragment在布局文件中的位置,那么添加选项卡的基本过程就是下面这些步骤:

  1. 继承 ActionBar.TabListener 接口。这个接口提供选项卡事件的回调,比如用户按下时你可以切换选项卡。
  2. 为每个你想添加的选项卡,实例化一个 ActionBar.Tab 实例并且调用 setTabListener() 去设置  ActionBar.TabListener,同时别忘了使用 setText() 设置选项卡的标题(视需要也可以使用 setIcon() 设置图标)。
  3. 然后调用 addTab() 把每个选项卡添加到操作栏中。

注意,ActionBar.TabListener 回调方法没有说明哪个fragment与这个选项卡关联,仅仅指出了哪个 ActionBar.Tab 被选中。你必须自己把每个 ActionBar.Tab 与它代表的合适的 Fragment 关联起来。取决于你的设计,你可以使用很多方式去关联。

例如,下面的例子介绍了如何继承  ActionBar.TabListener 使每个选项卡使用它自己的监听器实例:

public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    /** 每次调用构造方法会创建一个新的选项卡
      * @param activity  宿主Activity,用来实例化fragment
      * @param tag  fragment的标识符
      * @param clz  fragment的类,被用来实例化frament
      */
    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    }

    /* 下面是 ActionBar.TabListener 的回调 */

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        // 检查fragment是否已经实例化
        if (mFragment == null) {
            // 如果没有,实例化并且添加到activity中
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            // 如果已经存在,为了显示出来只需把fragment依附到activity上
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            // 取消fragment的依附,因为另外一个需要依附上
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // 用户重新选择已经选择过的标签栏,通常不需要做任何事
    }
}

警告:这这些回调里你绝对不能调用 commit() 去提交fragment事务—因为系统会为你调用它并且如果你自己调用时可能会抛出异常。同样你也不能把这些fragment事务添加到回退栈中。

在这个事例里,当各自的选项卡被选中时,监听器仅仅把fragment依附(attach())到activity布局中,或者如果fragment没有实例化,创建fragment并把它添加(add())到布局中(作为android.R.id.content视图组的一个子视图),当选项卡取消选中时取消fragment的依附(detach())。

剩下需要做的就是创建每个 ActionBar.Tab 并添加到 ActionBar 中。另外,你必须调用 setNavigationMode(NAVIGATION_MODE_TABS) 使选项卡可见。

例如,下面的代码使用上面定义的监听器添加两个选项卡:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 注意没用使用setContentView(),因为我们使用根节点android.R.id.content作为fragment的容器

    // 设置操作栏上的选项卡
    ActionBar actionBar = getSupportActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    actionBar.setDisplayShowTitleEnabled(false);

    Tab tab = actionBar.newTab()
                       .setText(R.string.artist)
                       .setTabListener(new TabListener<ArtistFragment>(
                               this, "artist", ArtistFragment.class));
    actionBar.addTab(tab);

    tab = actionBar.newTab()
                   .setText(R.string.album)
                   .setTabListener(new TabListener<AlbumFragment>(
                           this, "album", AlbumFragment.class));
    actionBar.addTab(tab);
}

如果activity停止了,你应该在 saved instance state 中保存当前选中的选项卡信息以便在用户返回时可以打开合适的选项卡。当保存状态时,你可以使用 getSelectedNavigationIndex() 查询当前选中的选项卡,它会返回选中选项卡的索引位置。

注意: 当用户使用选项卡在fragment之间切换,为了能返回到前一个fragment的状态,保存每个fragment的状态就显得非常重要了,这样它们看起来还像它们离开时那样。虽然某些状态默认会被保存了,但是你可能需要手动保存自定义视图的状态。更多关于保存fragment状态的信息,请参考 Fragments API指南。

注解:上面继承 ActionBar.TabListener 的方式只是许多合适的技巧中的一种。另外一种常用的方式是使用 ViewPager 去管理fragment,这样用户可以使用滑动手势去切换选项卡。在这种情况下,你只需在 onTabSelected() 回调里告诉 ViewPager 当前选项卡的索引位置。更多详细的信息,请阅读 Creating Swipe Views with Tabs

添加下拉导航


图9. 操作栏中的下拉导航列表。

activity还有另外一种导航模式(或称为过滤),在操作栏中嵌入下拉列表(也被称为“spinner”)。例如,下拉列表可以根据activity中内容的分类提供不同的模式。

当改变内容的很重要但是又不频繁发生时使用下拉列表就非常有用了。假如切换内容会更频繁的话,你应该使用 navigation tabs 来替代。

开启下拉导航的基本过程:

  1. 创建一个 SpinnerAdapter 为下拉列表提供可选项目列表和布局以便在绘制列表里的项目时使用。
  2. 继承 ActionBar.OnNavigationListener 来定义用户选中列表里项目时的操作。
  3. 在activity的 onCreate() 方法里调用 setNavigationMode(NAVIGATION_MODE_LIST) 启用操作栏的下拉列表。
  4. 使用 setListNavigationCallbacks() 设置下拉列表的回调。例如:
    actionBar.setListNavigationCallbacks(mSpinnerAdapter, mNavigationCallback);

    这个方法需要传入 SpinnerAdapter 和 ActionBar.OnNavigationListener

这个过程是非常简短的,但是实现 SpinnerAdapter 和 ActionBar.OnNavigationListener 需要做大量的工作。在这个文档的范围外还有有许多方式可以实现它们来为你的下拉导航定义功能并实现不同类别的 SpinnerAdapter(你应该查阅 SpinnerAdapter 类获取更多信息)。下面只是个让你 入手 SpinnerAdapter 和 ActionBar.OnNavigationListener 的简单事例。

 SpinnerAdapter 和 OnNavigationListener

SpinnerAdapter 是为spinner控件提供数据的适配器,例如操作栏中的下拉列表。SpinnerAdapter 是一个你可以继承的接口,然而Android提供了一些有用的实现你可以直接拿来扩展,比如 ArrayAdapter 和 SimpleCursorAdapter。例如,通过使用 ArrayAdapter 实现可以很轻松的创建一个使用字符数组作为数据源的  SpinnerAdapter 实例:

SpinnerAdapter mSpinnerAdapter = ArrayAdapter.createFromResource(this, R.array.action_list,
          android.R.layout.simple_spinner_dropdown_item);

createFromResource() 方法接收3个参数:应用的 Context,字符数组的资源ID和每个列表项目使用的布局。

资源文件中的字符输入定义如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="action_list">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
    </string-array>
</pre>

通过 createFromResource() 方法返回 ArrayAdapter 后你就可以直接把他传递给 setListNavigationCallbacks()(上面的步骤4)。在这么做之前,你需要创建 OnNavigationListener

你可以实现 ActionBar.OnNavigationListener 在用户选择下拉列表里的项目时来处理fragment的变化或activity的修改。继承这个监听器只需要实现一个回调方法: onNavigationItemSelected()

onNavigationItemSelected() 方法接受列表中项目的位置和 SpinnerAdapter 提供的项目唯一ID作为参数。

下面是实例化一个 OnNavigationListener 匿名实现的事例,把一个 Fragment 插入到 R.id.fragment_container 标识的布局容器中:

mOnNavigationListener = new OnNavigationListener() {
  // 获取字符串给下拉列表的适配器使用
  String[] strings = getResources().getStringArray(R.array.action_list);

  @Override
  public boolean onNavigationItemSelected(int position, long itemId) {
    // 创建我们定制的fragment事例
    ListContentFragment newFragment = new ListContentFragment();
    FragmentTransaction ft = openFragmentTransaction();
    // 不管fragment是否在fragment容器里都直接替换掉,并且用选中位置的字符串作为fragment的标签名
    ft.replace(R.id.fragment_container, newFragment, strings[position]);
    // 提交改变
    ft.commit();
    return true;
  }
};

OnNavigationListener 实例化后你就可以把 ArrayAdapter 和这个 OnNavigationListener 传递给 setListNavigationCallbacks()(步骤4)并调用。

在这个事例中,当用户选中下拉列表中的项目时,把一个fragment添加到布局中(替换 R.id.fragment_container 视图中的当前fragment)。被添加的fragment被指定一个标签来作为它的唯一标识,并且标签名与下拉列表中标识fragment的字符串一样。

在这个事例中,ListContentFragment 类定义了每个fragment:

public class ListContentFragment extends Fragment {
    private String mText;

    @Override
    public void onAttach(Activity activity) {
      // 这是接收到的第一个回调;我们可以在fragment事务期间在此用指定的标签名为fragment设置文本内容
      super.onAttach(activity);
      mText = getTag();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // 这里为fragment定义布局
        // 我们只创建一个文本并且文本内容设置为fragment标签名
        TextView text = new TextView(getActivity());
        text.setText(mText);
        return text;
    }
}

设置操作栏的样式


如果你想创造可以代表你应用品牌的视觉设计,那么你可以通过定制操作栏的每个显示细节达到效果,包括操作栏的颜色,文本的颜色,按钮的样式等等。这么做的话,你需要使用Android的 style and theme 框架的专门的样式属性为操作栏重塑样式。

注意:确保所有你使用到的背景图都是可以拉伸的 Nine-Patch drawables。Nine-Patch图的高度应该小于40dp,宽小于30dp。

大体外观

actionBarStyle
指定一个定义操作栏不同样式属性的样式资源。

默认样式是 Widget.AppCompat.ActionBar,你应该把它作为父样式来使用。

支持的样式包括:

background
为操作栏的背景定义图片资源。
backgroundStacked
为堆叠的操作栏(标签栏)定义图片资源。
backgroundSplit
为 split action bar 定义图片资源。
actionButtonStyle
为操作按钮定义样式资源。

默认样式是 Widget.AppCompat.ActionButton,你应该把它作为父样式来使用。

actionOverflowButtonStyle
为更多操作里的操作项定义样式资源。

默认样式是 Widget.AppCompat.ActionButton.Overflow, 你应该把它当做父样式来使用。

displayOptions
定义一个或多个操作栏显示选项,比如是否使用应用商标,显示activity标题或开启向上操作。参考 displayOptions 了解所有可能的值。
divider
为操作项间的分割线定义图片资源。
titleTextStyle
为操作栏标题定义样式资源。

默认样式是 TextAppearance.AppCompat.Widget.ActionBar.Title,你应该把它当做父样式来使用。

windowActionBarOverlay
声明操作栏是否应该覆盖在activity布局上,而不是平移activity布局的位置(例如:Gallery应用使用的是覆盖模式)。默认是false

通常操作栏在屏幕上有自己的空间且activity布局填充剩下的空间。当操作栏是覆盖模式时,activity布局使用所有可用的空间,然后系统在顶部绘制出操作栏。如果你想在操作栏隐藏和显示时使内容保持固定的大小和位置,使用覆盖模式就很有用了。因为你可以为操作栏设置半透明背景以便用户始终可以看到操作栏后的某些activity布局,所以你也可以纯粹想把它作为一种视觉效果来使用。

注解:Holo 主题家族默认使用半透明背景绘制操作栏。然而,你可以在你的样式里修改它并且在不同设备上的 DeviceDefault 主题可能默认使用的是不透明的背景。

当覆盖模式开启时,你的activity布局不会意识到操作栏在它的顶部趴着。所以你必须注意不要去替换操作栏覆盖区域里重要的信息或UI组建。在适当情况下,你可以在XML里重新指定 actionBarSize 在平台上的值来决定操作栏的高度。例如:

<SomeView
    ...
    android:layout_marginTop="?android:attr/actionBarSize" />

你也可以在运行时通过  getHeight() 获取操作栏的高度。 在它被调用时可以反射出操作栏的高度,可是如果调用发生在activity的早期生命周期里可能获取的高度不包含堆叠操作栏(导航标签栏)。要了解如何在运行时确定包含堆叠操作栏的总高度,请参考 Honeycomb Gallery 事例应用中的  TitlesFragment 类。

操作项

actionButtonStyle
为操作项按钮定义样式资源。

默认样式是 Widget.AppCompat.ActionButton,你应该把它当做父样式来使用。

actionBarItemBackground
为每个操作项的背景定义图片资源。为了标明不同选中状态应该使用 state-list drawable 。
itemBackground
为每个更多操作里的操作项背景定义图片资源。为了标明不同选中状态应该使用 state-list drawable 。
actionBarDivider
为操作项之间的分割线定义图片资源。
actionMenuTextColor
为操作项里显示的文本定义颜色。
actionMenuTextAppearance
为操作项里显示的文本定义样式资源。
actionBarWidgetTheme
为扩充到操作栏里的 action views 组件定义主题资源。

导航标签栏

actionBarTabStyle
为操作栏里的标签栏定义样式资源。

默认样式是 Widget.AppCompat.ActionBar.TabView,你应该把它当做父样式来使用。

actionBarTabBarStyle
为导航标签栏下面的细条定义样式资源。

默认样式是 Widget.AppCompat.ActionBar.TabBar,你应该把它当做父样式来使用。

actionBarTabTextStyle
为导航标签栏中的文本定义样式资源。

默认样式是 Widget.AppCompat.ActionBar.TabText, 你应该把它当做父样式来使用。

下拉列表

actionDropDownStyle
为下拉导航定义样式(比如背景和文本样式)。

默认样式是 Widget.AppCompat.Spinner.DropDown.ActionBar, 你应该把它当做父样式来使用。

主题事例

下面是关于为activity定义自定义主题的事例,CustomActivityTheme包含了自定义操作栏的一些样式。

注意每个操作栏的样式属性有两种版本。第一种版本在属性名里包含了android:前缀的可以支持API等级11或更高框架里的所有属性。第二种版本不包含android:前缀的是用来支持旧版本平台的,系统使用的支持库里的样式属性。每种效果都是一样的。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 应用或activity应用的主题 -->
    <style name="CustomActionBarTheme"
           parent="@style/Theme.AppCompat.Light">
        <item name="android:actionBarStyle">@style/MyActionBar</item>
        <item name="android:actionBarTabTextStyle">@style/TabTextStyle</item>
        <item name="android:actionMenuTextColor">@color/actionbar_text</item>

        <!-- 支持库兼容模式 -->
        <item name="actionBarStyle">@style/MyActionBar</item>
        <item name="actionBarTabTextStyle">@style/TabTextStyle</item>
        <item name="actionMenuTextColor">@color/actionbar_text</item>
    </style>

    <!-- 操作栏的大体样式 -->
    <style name="MyActionBar"
           parent="@style/Widget.AppCompat.ActionBar">
        <item name="android:titleTextStyle">@style/TitleTextStyle</item>
        <item name="android:background">@drawable/actionbar_background</item>
        <item name="android:backgroundStacked">@drawable/actionbar_background</item>
        <item name="android:backgroundSplit">@drawable/actionbar_background</item>

        <!-- 支持库兼容模式 -->
        <item name="titleTextStyle">@style/TitleTextStyle</item>
        <item name="background">@drawable/actionbar_background</item>
        <item name="backgroundStacked">@drawable/actionbar_background</item>
        <item name="backgroundSplit">@drawable/actionbar_background</item>
    </style>

    <!-- 操作栏标题文本 -->
    <style name="TitleTextStyle"
           parent="@style/TextAppearance.AppCompat.Widget.ActionBar.Title">
        <item name="android:textColor">@color/actionbar_text</item>
    </style>

    <!-- 操作栏标签栏文本 -->
    <style name="TabTextStyle"
           parent="@style/Widget.AppCompat.ActionBar.TabText">
        <item name="android:textColor">@color/actionbar_text</item>
    </style>
</resources>

在清单文件中,你可以为整个应用应用这个主题:

<application android:theme="@style/CustomActionBarTheme" ... />

或者只为个别的activity应用:

<activity android:theme="@style/CustomActionBarTheme" ... />

注意:请确保每个在<style>标签里的主题和样式都声明了父主题,它从中继承了所有你的主题里没有明确声明的样式。在修改操作栏时使用父主题是很重要的,这样你就只需重写你想要改变的操作栏样式,而不用重写那些你不想修改的样式(比如操作项中的文本大小或边距)。

更多关于在应用中使用主题和样式资源的信息,请阅读 Styles and Themes

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值