9.1 菜单的基本用法

源代码目录:src/ch09/Menu

菜单是Android系统中重要的用户接口之一。在Android系统中提供了丰富多彩的菜单,例如,系统的主菜单,也可称为选项菜单;带图像、复选框、选项按钮的菜单;上下文菜单。本节将对这些菜单的实现方法进行详细讲解。

9.1.1 创建选项菜单(Options Menu)

Activity.onCreateOptionsMenu方法用来创建选项菜单,该方法的原型如下:

public boolean onCreateOptionsMenu(Menu menu);

通常需要将创建选项菜单的代码放在onCreateOptionsMenu方法中。调用Menu.add方法可以添加一个选项菜单项。该方法有4种重载形式,它们的原型如下:

public MenuItem add(int titleRes);

public MenuItem add(CharSequence title);

public MenuItem add(int groupId, int itemId, int order, int titleRes);

public MenuItem add(int groupId, int itemId, int order, CharSequence title);

add方法最多有4个参数,这些参数的含义如下。

groupId:菜单项的分组ID,该参数一般用于带选项按钮的菜单(将在后面详细介绍)。参数值可以是负整数、0和正整数。

itemId:当前添加的菜单项的ID。该参数值可以是负整数、0和正整数。

order:菜单显示顺序。Android系统在显示菜单项时,根据order参数的值按升序从左到右、从上到下显示菜单项。参数值必须是0和正整数,不能为负整数。

titleRes或title:菜单项标题的字符串资源ID或字符串。

如果使用add方法的前两种重载形式,groupId、itemId和order三个参数的值都为0。这时菜单项的显示顺序就是菜单项的添加顺序。下面的代码添加了3个选项菜单项:

public boolean onCreateOptionsMenu(Menu menu)

{

  menu.add(1, 1, 1, "菜单项1");

  menu.add(1, 2, 2, "菜单项2");

  menu.add(1, 3, 3, "菜单项3");

  return true;

}

如果想为菜单项设置图像,可以使用MenuItem.setIcon方法,代码如下:

// 通过图像资源ID装载图像

public MenuItem setIcon(int iconRes);

// 通过Drawable对象装载图像

public MenuItem setIcon(Drawable icon);

下面的代码设置了“删除”菜单项的图像:

MenuItem deleteMenuItem = menu.add(1, 1, "删除");

deleteMenuItem.setIcon(R.drawable.delete); // 设置“删除”菜单项的图像

选项菜单的显示效果根据Android版本不同分为如下两个阶段。

Android 1.x、2.x

选项菜单最多显示6个菜单项,如果不足6个菜单项,可根据实际情况来排列,例如,在有5个菜单项的情况下,第1行会显示两个菜单项,第2行会显示3个菜单项,如图9-1所示。如果菜单项超过6个,系统会显示前5个菜单项,而最后一个菜单项的文本是“更多”或“More”,如图9-2所示。单击该菜单项后,会显示其余的菜单项。如果菜单项的文本过长,系统会显示三行两列的选项菜单,而不是如图9-2所示的两行三列的选项菜单,而且过长的标题会从左到右移动显示。

 

▲图9-1 有5 个菜单项的选项菜单

 

▲图9-2 超过6 个菜单项的Activity 菜单

Android 3.x、Android 4.x

由于从Android 3.x开始,Android开始支持ActionBar关于ActionBar的内容会在后面的章节详细介绍。,也就是将菜单、按钮放到窗口顶端的一种界面风格。ActionBar的出现取代了标题栏的位置,并改变了选项菜单的风格的同时与选项菜单紧密结合。在本节并不需要了解ActionBar的细节,只要知道可以很容易地将选项菜单项移到ActionBar上作为一个按钮显示即可,并且该选项菜单项会从选项菜单中消失。当然,即使菜单项显示在选项菜单中,也不会再显示图像,而只会显示文本,并且也不会是图9-1的菜单风格,实际的菜单风格是从窗口底端弹出一个如图9-3所示的单列菜单,如果菜单项太多一屏显示不下,该菜单可以上下滚动。

 

▲图9-3 新版Android选项菜单的效果

不同Android版本的选项菜单只是风格不同,但使用方法是完全一样的,因此从代码角度是完全兼容的。

注意

如果想在Android 3.x或Android 4.x中使用旧版选项菜单的效果,可以将AndroidManifest.xml文件中<application>标签的android:theme属性去掉即可,因为该主题设置了与选项菜单和ActionBar相关的属性。如果去掉了该主题,虽然选项菜单恢复了旧貌,但ActionBar则消失了。

9.1.2 关联Activity

虽然可以通过代码显示一个Activity,但我们还有更简单的方法,就是直接将Activity与菜单项关联。做法非常简单,只需要使用MenuItem.setIntent方法指定一个Intent对象即可。setIntent方法的原型如下:

public MenuItem setIntent(Intent intent);

将一个Activity与菜单项关联后,单击该菜单项,系统会调用startActivity方法显示与菜单项关联的Activity。下面的代码将名为AddActivity的窗口类与“添加”菜单项关联,单击“添加”菜单项,系统就会显示AddActivity窗口。

MenuItem addMenuItem = menu.add(1, 1, 1, "添加");

// 将AddActivity与“添加”菜单项进行关联

addMenuItem.setIntent(new Intent(this, AddActivity.class));

注意

如果设置了菜单项的单击事件,并且单击事件返回true,则与菜单项关联的Activity将失效。也就是说,系统调用单击事件方法后,就不会再调用startActivity方法显示与菜单项关联的Activity了。

9.1.3 响应菜单的单击动作

调用MenuItem.etOnMenuItemClickListener方法可以设置菜单项的单击监听对象,该方法有一个OnMenuItemClickListener类型的参数,处理菜单项的单击事件类必须实现OnMenuItemClickListener接口。下面的代码为“删除”菜单项设置了单击事件。

源代码文件:ch09/Menu/src/mobile/android/menu/Main.java

public class Main extends Activity implements OnMenuItemClickListener

{

  // 菜单项单击事件方法

  @Override

  public boolean onMenuItemClick(MenuItem item)

  {

    // 在这里编写菜单项单击事件的代码,可根据item参数的getItemId方法来判断单击的是哪个菜单项

    return true;

  }

  @Override

  public boolean onCreateOptionsMenu(Menu menu)

  {

    MenuItem deleteMenuItem = menu.add(1, 2, 2, "删除");

    deleteMenuItem.setIcon(R.drawable.delete);

    // 设置“删除”菜单项单击事件的监听器,因为当前窗口类已经实现了

    // OnMenuItemClickListener接口

    deleteMenuItem.setOnMenuItemClickListener(this);  

  }

}

除了设置菜单项的单击事件外,还可以使用Activity.onOptionsItemSelected和Activity.onMenu ItemSelected方法响应菜单项的单击事件。这两个方法的原型如下:

public boolean onOptionsItemSelected(MenuItem item);

public boolean onMenuItemSelected(int featureId, MenuItem item);

这两个方法都有一个item参数,用于传递被单击的菜单项的MenuItem对象。可以根据MenuItem接口的相应方法(例如,getTitle和getItemId方法)判断单击的是哪个菜单项。

既然有3种响应菜单项单击事件的方法,就会产生一个问题:如果同时使用这3种方法,它们都会起作用吗?如果都起作用,那么调用顺序如何呢?实际上,当onMenuItemClick方法返回true时,另两种单击事件的响应方式都会失效,也就是说,单击菜单项时,系统不会再调用onOptionsItemSelected和onMenuItemSelected方法了。如果未设置菜单项的单击事件(onMenuItemClick方法),而同时使用了另外两种响应单击事件的方式,系统会根据在onMenuItemSelected方法中调用父类(Activity类)的onMenuItemSelected方法的位置来决定先调用onOptionsItemSelected方法还是先调用onMenuItemSelected方法。

// 如果将super.onMenuItemSelected(...)放在Log.d(...)后面调用,

// 系统会在执行完onMenuItemSelected方法中的代码后再调用onOptionsItemSelected方法

@Override

public boolean onMenuItemSelected(int featureId, MenuItem item)

{

  super.onMenuItemSelected(featureId, item);// 这条语句调用了onOptionsItemSelected方法

  Log.d("onMenuItemSelected:itemId=", String.valueOf(item.getItemId()));

  return true;

}

9.1.4 动态添加、修改和删除选项菜单

在很多Android应用中,需要在程序的运行过程中根据具体情况动态地对选项菜单进行处理,例如,增加菜单项、修改菜单项的标题和图像。实现这个功能的关键是获得描述选项菜单的Menu对象。

Activity类中的很多方法都可以获得Menu对象。例如, onCreateOptionsMenu方法的menu参数就是Menu类型,我们要做的就是在onCreateOptionsMenu方法中将Menu对象保存在类成员变量中。下面的代码动态地向选项菜单中添加了5个菜单项:

源代码文件:ch09/Menu/src/mobile/android/menu/Main.java

public class Main extends Activity implements OnMenuItemClickListener,

    OnClickListener

{

  private Menu menu;

  private int menuItemId = Menu.FIRST;    // Menu.FIRST的值是1

  @Override

  public void onClick(View view)

  {

    // 只有单击手机上的“Menu”按钮,onCreateOptionsMenu方法才会被调用,

    // 因此,如果不按“Menu”按钮,Main类的menu变量值是null

    if (menu == null) return;

    // 向Activity菜单添加5个菜单项,菜单项的id从10开始

    for (int i = 10; i <15; i++)

    {

      int id = menuItemId++;

      menu.add(1, id, id, "菜单" + i);

    }

  }

  @Override

public boolean onCreateOptionsMenu(Menu menu)

  {

    this.menu = menu;      // 保存Menu变量

    return super.onCreateOptionsMenu(menu);

  }

  ... ...

}

运行程序后,单击模拟器上的“Menu”按钮(为了调用onCreateOptionsMenu方法以获得Menu对象),然后单击“动态添加5个菜单项”按钮,再次单击模拟器上的“Menu”按钮,将显示如图9-4所示的效果。

 

▲图9-4 动态添加的5 个菜单项

既然有了Menu对象,修改和删除指定的菜单项就变得非常容易了,读者可以使用Menu对象的相应方法来完成这些工作。

9.1.5 带复选框和选项按钮的子菜单

传统的子菜单是以层次结构显示的,而Android中的子菜单采用了弹出式的显示方式。也就是当单击带有子菜单的菜单项后,父菜单会关闭,而在屏幕上会单独显示子菜单。

Menu.addSubMenu方法用来添加子菜单。该方法有4种重载形式,它们的原型如下:

SubMenu addSubMenu(final CharSequence title);

SubMenu addSubMenu(final int titleRes);

SubMenu addSubMenu(final int groupId, final int itemId, int order, final CharSequence title);

SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes);

addSubMenu方法和add方法的参数个数、数据类型和含义完全相同,所不同的是它们的返回值类型。addSubMenu方法返回了一个SubMenu对象(SubMenu是Menu的子接口),可以通过SubMenu.add方法添加子菜单项。SubMenu.add方法与Menu.add方法在功能和使用方法上完全相同,这两个add方法都会返回一个MenuItem对象。

在子菜单项上不能显示图像,但可以在子菜单头上显示图像,不过子菜单项可以带复选框和选项按钮。例如,下面的代码向“文件”菜单项添加了3个子菜单项,并将第1个子菜单项设置成复选框类型,将后两个子菜单项设置成选项按钮类型,同时为子菜单头设置了图像。

源代码文件:ch09/Menu/src/mobile/android/menu/Main.java

public boolean onCreateOptionsMenu(Menu menu)

{

  // 添加子菜单

  SubMenu fileSubMenu = menu.addSubMenu(1, 1, 2, "文件");

  fileSubMenu.setIcon(R.drawable.file);       // 设置在选项菜单中显示的图像

  fileSubMenu.setHeaderIcon(R.drawable.headerfile); // 设置子菜单头的图像

  MenuItem newMenuItem = fileSubMenu.add(1, 2, 2, "新建");

  newMenuItem.setCheckable(true);      // 将第1个子菜单项设置成复选框类型

  newMenuItem.setChecked(true);         // 选中第1个子菜单项中的复选框

  MenuItem openMenuItem = fileSubMenu.add(2, 3, 3, "打开");

  MenuItem exitMenuItem = fileSubMenu.add(2, 4, 4, "退出");

  exitMenuItem.setChecked(true);         // 将第3个子菜单项的选项按钮设为选中状态

  fileSubMenu.setGroupCheckable(2, true, true); // 将后两个子菜单项设置成选项按钮类型

}

在编写上面代码时应注意如下几点。

添加子菜单并不是直接在MenuItem下添加菜单项,而需要使用addSubMenu方法创建一个SubMenu对象,并在SubMenu下添加子菜单项。SubMenu和MenuItem是平级的,这一点在添加子菜单时要注意。

将子菜单项设置成复选框类型,需要使用MenuItem.setCheckable方法。但设置成选项按钮类型,不需要使用setCheckable方法,而要将同一组的选项按钮(子菜单项)的groupId设置成相同的值,同时使用setGroupCheckable方法来设置这个groupId。该方法的第1个参数指定子菜单项的groupId,第2个参数必须为true。如果第3个参数为true,相同groupId的子菜单项会被设置成选项按钮效果;如果为false,相同groupId的子菜单项会被设置成复选框效果。根据相同groupId设置的选项按钮和复选框除了显示效果,并没有什么其他的不同。在菜单项上加选项按钮或复选框主要是为了标志菜单项当前的状态,至于选中或未选中状态代表什么,完全由开发人员自己决定。

使用setChecked方法可以将复选框或选项按钮设置成选中状态。

选项菜单不支持嵌套子菜单,也就是说,不能在子菜单项下再建立子菜单,否则系统将抛出异常。

运行程序后,单击选项菜单中的“文件”菜单项,根据setGroupCheckable方法的第3个参数值是true或false,显示的选项效果或复选框效果如图9-5和图9-6所示。

 

▲图9-5 选项按钮效果

 

▲图9-6 复选框按钮效果

9.1.6 上下文菜单

上下文菜单可以和任意View对象进行关联,例如,TextView、EditText、Button等控件都可以关联上下文菜单。上下文菜单的显示效果和子菜单有些类似,也分为菜单头和菜单项。

要想创建上下文菜单,需要实现Activity.onCreateContextMenu方法,该方法的原型如下:

public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo);

可以使用ContextMenu.setHeaderTitle和ContextMenu.setHeaderIcon方法设置上下文菜单头的标题和图像。上下文菜单项不能带图像,但可以带复选框或选项按钮(这一点和子菜单相同)。上下文菜单与选项菜单一样,也不支持嵌套子菜单。下面的代码创建一个包含4个菜单项的上下文菜单,其中最后一个菜单项包含两个子菜单项。

源代码文件:ch09/Menu/src/mobile/android/menu/Main.java

public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)

{    

  super.onCreateContextMenu(menu, view, menuInfo);

  menu.setHeaderTitle("上下文菜单");

  menu.setHeaderIcon(R.drawable.face);

  // 添加3个上下文菜单项,Menu.NONE的值是0

  menu.add(0, menuItemId++, Menu.NONE, "菜单项1").setCheckable(true).setChecked(true);

  menu.add(20, menuItemId++, Menu.NONE, "菜单项2");

  // 选中第2个选项按钮

  menu.add(20, menuItemId++, Menu.NONE, "菜单项3").setChecked(true);

  menu.setGroupCheckable(20, true, true);  

  // 添加带子菜单的上下文菜单项

  SubMenu sub = menu.addSubMenu(0, menuItemId++, Menu.NONE, "子菜单");

  sub.add("子菜单项1");

  sub.add("子菜单项2");

}

上下文菜单与其他菜单不同的是必须注册到指定的View上才能显示。注册上下文菜单可以使用Activity.registerForContextMenu方法。下面的代码将当前窗口的上下文菜单注册到Button、EditText和TextView上。

Button button = (Button) findViewById(R.id.btnAddMenu);

EditText editText = (EditText) findViewById(R.id.edittext);

TextView textView = (TextView)findViewById(R.id.textview);

// 注册上下文菜单

registerForContextMenu(button);

registerForContextMenu(editText);

registerForContextMenu(textView);

当一个控件关联上下文菜单后,长按该控件,等一会就会显示上下文菜单,如运行程序后,长按TextView控件,会显示如图9-7所示的上下文菜单。

 

▲图9-7 TextView 控件的上下文菜单

上下文菜单项的单击事件也可以使用OnMenuItemClickListener.onMenuItemSelected方法来响应,这和选项菜单、子菜单的响应方法相同。但对于上下文菜单来说,第3种响应单击事件的方式需要实现Activity.onContextItemSelected方法,该方法的原型如下:

public boolean onContextItemSelected(MenuItem item);

9.1.7 菜单事件

Activity类还有一些与菜单相关的事件方法,这些方法的原型如下:

public boolean onPrepareOptionsMenu(Menu menu);

public void onOptionsMenuClosed(Menu menu);

public void onContextMenuClosed(Menu menu);

public boolean onMenuOpened(int featureId, Menu menu);

这些方法的含义如下:

onPrepareOptionsMenu:在显示选项菜单之前被调用。一般可用来修改即将显示的选项菜单。

onOptionsMenuClosed:在关闭选项菜单时被调用。

onContextMenuClosed:在关闭上下文菜单时被调用。

onMenuOpened:在显示选项菜单之前被调用。该方法在onPrepareOptionsMenu方法之后调用。

9.1.8 从菜单资源中装载菜单

前面介绍的各种类型的菜单都是通过代码添加的,Android SDK还允许我们从菜单资源中装载菜单。Android工程中的所有菜单资源都在res/menu目录中,例如,下面就是一个包含3个菜单项的菜单资源文件。

file_menu.xml

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:title="新建" />

  <item android:title="打开" />

  <item android:title="退出" />

</menu>

编写完了菜单资源,还必须在onCreateOptionsMenu或onCreateContextMenu方法中使用如下的代码来装载这个菜单资源。本例在onCreateContextMenu方法中将file_menu.xml文件中的内容作为上下文菜单添加。相当于调用3次Menu.add方法添加3个菜单项。

getMenuInflater().inflate(R.menu.file_menu, menu);