从Android3.0开始,安卓设备不再提供菜单按钮。这样,我们就从原先的六选项菜单转移到现在使用控制条(action bar)显示用户的常用操作。
尽管用户使用习惯有所改变,但定义一些控制和选择的用户行为仍然是基于Menu AP接口的。本篇介绍三种最基本的菜单类型。
Options menu and action bar
Options menu是activity的最基本容器。这里是放置一些对整个app有全局影响的地方。例如"Search","Compose email","Settings"。Android3.0之后,options menu里的选项被转移到了action bar作为屏幕之上控制操作和溢出选项的结合。并且之后不再使用Menu button。
Context menu and contextual action mode
context menu是当用户点击某一元素之后出现的悬浮菜单。它提供影响选择的选中的内容或文本框架的操作。Android3.0之后,我们应该使用contextual action mode进行选中内容的操作。这一模式表示的是对显示在屏幕顶部的选项条中对选中内容进行操作的选项并允许用户同时选择多个选项。
Popup menu
popup menu显示的是一列选项展示在视图的一个竖形列表中。这对于提供指定内容的溢出操作或者一个命令的第二部分选择是个不错的选择。popup menu中的操作不应该直接影响相关内容——因为这是contextual操作的工作。popup menu的目的是对activity的内容区域相关。
Defining a Menu in XML
对于所有的菜单类型,Android提供了标准的XML格式定义菜单选项。在XML中定义一个菜单和菜单选项而不要在activity代码中进行此项操作。然后在activity或fragment代码中填充菜单资源(作为菜单对象)。这样做有以下好处:
- 在XML中容易组织菜单结构
- 把菜单定义和菜单操作分开
- 允许我们根据不同的版本,屏幕尺寸创建不同的菜单配置。
菜单XML文件放置在res/menu/文件夹下,使用如下标签:
<menu>:必须是根节点,同时在它之下还可以有多个<menu>和<group>标签。
<item>:对应于MenuItem类对象。
<group>:一个可选项,通常表示一组属性类似的菜单比如active state和visibility。
如下是game_menu.xml示例:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/new_game"
android:icon="@drawable/ic_new_game"
android:title="@string/new_game"
android:showAsAction="ifRoom"/>
<item android:id="@+id/help"
android:icon="@drawable/ic_help"
android:title="@string/help" />
</menu>
同时菜单之下还可以有菜单,如下:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/file"
android:title="@string/file" >
<!-- "file" submenu -->
<menu>
<item android:id="@+id/create_new"
android:title="@string/create_new" />
<item android:id="@+id/open"
android:title="@string/open" />
</menu>
</item>
</menu>
在代码中填充菜单XML文件使用MenuInflater.inflate(),这个方法把XML菜单转换为Menu类对象。
Creating an Options Menu
Options menu在屏幕上的位置取决于Android版本:
- Android 2.3.x(API level 10)或更低版本,用户点击菜单按钮是出现在屏幕底部。如下图左。
- 之后版本中,则出现在action bar中。缺省情况下,系统放置所有选项在溢出操作中,用户点击action bar右侧的溢出图标显示这些菜单项。支持快捷操作,我们可以在某些菜单项中设置android:showAsAction="ifRoom"属性,如下图右。
我们可以为activity和fragment申明菜单项。如果同时申明了,那么它们会同时声明了options menu,它们结合在UI里。先显示activity的选项,接着显示fragment选项。如果需要,可以使用android:orderInCategory属性重排相应的<item>选项。在activity和fragment使用各自的onCreateOptionsMenu()回调指定options menu。例如:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater()
;
inflater.inflate(R.menu.game_menu, menu);
return true;
}
更多菜单项的操作,查看MenuItem的API。在Android 2.3.x之前,系统在用户初次打开菜单项的时候调用onCreateOptionsMenu()。Android3.0之后,系统在启动activity的时候调用onCreateOptionsMenu()。
Handling click events
当用户点击某个菜单选项时,用户调用onOptionsItemSelected()函数,它传递选中的MenuItem对象。通过getItemId()可以指定选项(它由android:id定义)例如:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.new_game:
newGame();
return true;
case R.id.help:
showHelp();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
当成功处理了一个菜单选项时,返回true。如果没有,需要调用父类的onOptionsItemSelected()(缺省情况下返回false)。
如果activity包括fragments,系统首先调用activity的onOptionsItemSelected(),然后调用每个fragment(按照fragment各自加到activity的顺序)直到返回true值或者所有fragments都被调用。
Tips:Android3.0在XML中添加了android:onClick属性。该属性值必须是一个使用该菜单的activity中的某个方法的名字。该方法必须是public的并且接收一个单独的MenuItem参数——当系统调用这个方法时,它传递选中的菜单选项给它。更多信息,查看Menu Resource文件。
Tips:如果应用包含多个activities都提供相同的options menu,我们可以考虑创建一个activity专门实现onCreateOptionsMenu()和onOptionsItemSelected()方法。然后让其它activity继承这个activity即可。如果各个activities对菜单项操作不同我们只需要各自再复写相关菜单方法即可。
Changing menu items at runtime
Android3.0之后,系统创建activity时调用onCreateOptionsMenu(),之后就保持菜单不变,如果我们想根据事件修改菜单项,可以在onPrepareOptionsMenu()方法中实现。当出现某个事件时,我们需要先调用invalidateOptionsMenu()要求系统调用onPrepareOptionsMenu()。
Note:永远不要根据当前处于焦点的视图改变菜单项。因为在触屏模式下(用户没有使用轨迹球或d-pad),视图永远无法获取焦点,因此永远不要根据这一点修改options menu。如果菜单跟视图相关,最好使用Context Menu
Creating Contextual Menus
文本菜单提供指定选项或UI文本框架的操作。可以为任何视图控件提供文本菜单,但通常使用在ListView,GridView或其它用户可以操作每个选项的视图容器中。有两种提供文本按钮的方法:
- 悬浮文本菜单。当用户长按一个视图时出现一个悬浮列表。用户一次能处理一个文本操作。
- 文本操作模式。这是ActionMode类的一个系统级实现,ActionMode展示了一个文本控制条,显示在屏幕顶部。当激活这个模式时,用户可以一次对多个选项进行操作。
Note:Android3.0之后更倾向于使用文本操作模式而不是悬浮文本菜单。
Creating a floating context menu
1. 调用registerForContextMenu()注册文本菜单传递给某个view视图。
如果activity使用了ListView或GridView,我们希望其中的每个选项都使用同样的文本菜单,我们调用registerForContextMenu()传递文本菜单给ListView或GridView为每个选项注册文本菜单。
2. 在Activity或Fragment中实现onCreateContextMenu()方法。当注册的视图接收到长按事件时,系统调用onCreateContextMenu()方法。这是我们定义菜单选项的地方,通常是填充菜单资源。例如:
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.context_menu, menu);
}
MenuInflater对象允许用户从Menu XML资源填充文本菜单。回调参数包括用户点击的View视图和一个ContextMenu.ContextMenuInfo对象提供选中项的额外信息。如果activity有几个视图提供哦你过了不同的文本视图,我们可能需要这些参数来决定填充那个文本菜单。
3. 实现onContextItemSelected()
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
switch (item.getItemId()) {
case R.id.edit:
editNote(info.id);
return true;
case R.id.delete:
deleteNote(info.id);
return true;
default:
return super.onContextItemSelected(item);
}
}
Using the contextual action mode
当用户选择一个选项激活了这个模式的时候,一个文本控制条出现在屏幕顶部显示用户可以进行的操作。如果允许,用户可以选择多个选项。点击返回按钮取消选择或者点击完成图标完成选择。
Note:文本控制条与控制条(action bar)关系不大。它们各自分开,即便是文本控制条在视图上占据了控制条的位置。
对于提供文本操作的视图,我们通常需要让其至少实现下述两个功能的一个:
- 长按操作
- 用户选择视图里checkbox或类似的UI组件
我们的应用如何实现文本操作模式和定义每个操作的模式取决于我们的设计。通常有两种基本设计:
- 个人文本操作,任意的视图
- ListView或GridView为一组内容选择文本操作。
Enabling the contextual action mode for individual views
如果我们想在用户选择某个特定视图时实现这一模式,应当如下做:
1. 实现ActionMode.Callback接口。在接口方法中,指定文本操作条的操作响应点击动作选项的点击事件,并处理操作模式生命周期事件。
2. 当要显示控制条时调用startActionMode() (例如用户长按视图)
例如,实现ActionMode.Callback接口:
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context_menu, menu);
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_share:
shareCurrentItem();
mode.finish(); // Action picked, so close the CAB
return true;
default:
return false;
}
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
}
};
注意到这些事件除了传递事件相关的ActionMode对象,回调几乎和options menu回调完全一样。具体可以查看ActionMode的API。还注意到这个模式销毁之后mActionMode变量为空。下一步,将看到如何在activity和fragment中初始化和存储成员变量。
2.在合适的时候调用startActionMode()激活文本操作模式,例如响应视图长按:
someView.setOnLongClickListener(new View.OnLongClickListener() {
// Called when the user long-clicks on someView
public boolean onLongClick(View view) {
if (mActionMode != null) {
return false;
}
// Start the CAB using the ActionMode.Callback defined above
mActionMode = getActivity().startActionMode(mActionModeCallback);
view.setSelected(true);
return true;
}
});
当调用startActionMode()后,系统返回创建的ActionMode对象。把它存储到一个成员变量中,我们可以根据其他事件对文本控制条作出改变。在上例中,通过在启动action mode之前检测成员是否为空,ActionMode用来保证ActionMode实例已经存在的时候不再重新创建。
Enabling batch contextual actions in a ListView or GridView
1. 实现AbsListView.MultiChoiceModeListener接口并通过setMultiChoiceModeListener()把它设置到View group中去。在监听器回调方法中,可以为文本控制条指定操作响应点击事件,并处理ActionMode.Callback接口继承来的回调。
2. 调用setChoiceMode()方法,并有参数CHOICE_MODE_MULTIPLE_MODAL。
ListView listView = getListView();
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new MultiChoiceModeListener() {
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position,
long id, boolean checked) {
// Here you can do something when items are selected/de-selected,
// such as update the title in the CAB
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Respond to clicks on the actions in the CAB
switch (item.getItemId()) {
case R.id.menu_delete:
deleteSelectedItems();
mode.finish(); // Action picked, so close the CAB
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate the menu for the CAB
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context, menu);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Here you can make any necessary updates to the activity when
// the CAB is removed. By default, selected items are deselected/unchecked.
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Here you can perform updates to the CAB due to
// an invalidate()
request
return false;
}
});
Creating a Popup Menu
它是视图固定的样式菜单。如果固定视图底部有空间,它出现在视图下方,没有则出现在视图层上。这很有用,因为:
- 提供特定内容的移除菜单操作(如Gmail的email头部菜单)注意:这与context menu不同,context menu只影响选中的内容。
- 提供一个命令的第二命令。例如一个“添加”按钮,点击之后弹出一个popup menu显示不同的添加选项。
- 提供一个类似Spinner的下拉菜单,但是它不长时间保存选择。
注意:API level 11之后才支持PopupMenu
如果在XML中定义菜单,下面是PopupMenu的步骤:
1. 用构造器初始化一个PopupMenu实例,构造器需要应用Context和菜单固定视图的View。
2. 使用MenuInflater填充菜单资源,PopupMenu.getMenu()返回Menu对象。API level 14之后,可以使用PopupMenu.inflate()替代。
3. 调用PopupMenu.show()。
例如,下面是一个有android:onClick属性的按钮显示一个popup菜单。
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_overflow_holo_dark"
android:contentDescription="@string/descr_overflow_button"
android:onClick="showPopup" />
可以在activity中如下显示这个popup菜单。
public void showPopup(View v) {
PopupMenu popup = new PopupMenu(this, v);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.actions, popup.getMenu());
popup.show();
}
当用户选择了一项或者点击到菜单之外的区域时,可以监听PopupMenu.OnDismissListener取消事件。
Handling click events
当用户选择了某一菜单项,我们必须实现PopupMenu.OnMenuItemClickListener接口并调用setOnMenuItemclickListener()注册。当用户选中某一项时,系统调用onMenuItemClick()回调。
public void showMenu(View v) {
PopupMenu popup = new PopupMenu(this, v);
// This activity implements OnMenuItemClickListener
popup.setOnMenuItemClickListener(this);
popup.inflate(R.menu.actions);
popup.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.archive:
archive(item);
return true;
case R.id.delete:
delete(item);
return true;
default:
return false;
}
}
Creating Menu Groups
实现一组类似属性的菜单,可以:
- setGroupVisible()显示或隐藏所有选项
- setGroupEnabled()enable或disable所有选项。
- setGroupCheckable()设置是否所有选项可以checked。
在<group>标签下添加<item>或者用add()方法指定group ID创建组。
<?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/menu_save" android:title="@string/menu_save" /> <!-- menu group --> <group android:id="@+id/group_delete"> <item android:id="@+id/menu_archive" android:title="@string/menu_archive" /> <item android:id="@+id/menu_delete" android:title="@string/menu_delete" /> </group> </menu>Using checkable menu items
菜单用作开关选项时很有用,为一独立选项使用checkbox,或者为一组相互独立的选项使用radio buttons如图使用了radion buttons展示一组checkable子菜单。
Note:图标菜单中的菜单项不能展示checkbox或radio button。如果想要使图标菜单中的选项checkable,必须在每次状态改变时人工切换图标或文本。
可以使用android:checkable属性为单独的<item>定义checkable行为或者使用android:checkableBehavior属性为<group>定义checkable行为:例如
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item android:id="@+id/red"
android:title="@string/red" />
<item android:id="@+id/blue"
android:title="@string/blue" />
</group>
</menu>
android:checkableBehavior属性有三个值:
single:group中只有一个选项可以是checked(radio buttons)
all:所有选项都可checked(checkboxes)
none:不可checked
同时我们还可以在<item>中使用android:checked属性设置缺省checked状态。
当一个checkable选项被选中时,系统调用各自的项目选中回调方法(例如onOptionsItemSelected())在这里我们必须设置checkbox状态,因为checkbox或radio button不会自动改变状态。我们可以在用户选中之前查询当前状态(isChecked())然后设置其状态(setChecked())。例如:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.vibrate:
case R.id.dont_vibrate:
if (item.isChecked()) item.setChecked(false);
else item.setChecked(true);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
如果不这样设置,当用户选中其时,项目的可见性状态不会改变。
Note:Checkable菜单项在应用摧毁后不会保存。如果我们想要应用保存用户设置,应当使用SharedPreferences保存数据。
Adding Menu Item Based on an Intent
有时我们会希望选中某一菜单项后启动一个activity。当我们知道需要使用的intent和初始化intent的菜单,我们可以在菜单项合适的回调汇总调用startActivity()。
然而,如果我们不确定用户设备是否包含可处理该intent的应用,这样作可能会导致无作用菜单项。解决这个问题,系统允许我们在设备找到相应的activity时动态的添加选项到菜单中:
1.定义一个intent包含category:CATEGORY_ALTERNATIVE和CATEGORY_SELECTED_ALTERNATIVE以及其他要求。
2.调用Menu.addIntentOptions()。然后搜索可以处理该intent的应用添加到菜单中。如果没有符合的,则不添加。
Note:CATEGORY_SELECTED_ALTERNATIVE只处理屏幕选中的元素。所以只有onCreateContextMenu()创建的菜单才能使用它。
@Override
public boolean onCreateOptionsMenu(Menu menu){
super.onCreateOptionsMenu(menu);
// Create an Intent that describes the requirements to fulfill, to be included
// in our menu. The offering app must include a category value of Intent.CATEGORY_ALTERNATIVE.
Intent intent = new Intent(null, dataUri);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
// Search and populate the menu with acceptable offering applications.
menu.addIntentOptions(
R.id.intent_group, // Menu group to which new items will be added
0, // Unique item ID (none)
0, // Order for the items (none)
this.getComponentName(), // The current activity name
null, // Specific items to place first (none)
intent, // Intent created above that describes our requirements
0, // Additional flags to control items (none)
null); // Array of MenuItems that correlate to specific items (none)
return true;
}
对于匹配该intent的intent filter中使用intent filter的android:label为该菜单添加标题,并且应用的图标作为该菜单的图标。addIntentOptions()方法返回添加的菜单数目。
Note:当调用addIntentOptions()时,它复写第一个参数中由menu group指定的任意和所有菜单选项。
Allowing your activity to be added to other menus
我们可以把activity服务提供给其他的应用,所以我们的应用可以包括在其他应用的菜单项中。做法是需要为category添加CATEGORY_ALTERNATIVE和CATEGORY_SELECTED_ALTERNATIVE值。例如:
<intent-filter label="@string/resize_image">
...
<category android:name="android.intent.category.ALTERNATIVE" />
<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
...
</intent-filter>