菜单是Android应用中重要且常见的组成部分,从Android3.0开始,android从一个专用的“菜单按钮”转变为提供一个应用栏来呈现常见的用户操作。
主要可以分为三类:选项菜单、上下文菜单/上下文操作模式以及弹出菜单。它们的主要区别如下:
选项菜单:Activity的主菜单项,用于放置对应用产生全局影响的操作,如搜索/设置
。
上下文菜单: 用户长按某一元素时出现的浮动菜单。它提供的操作将影响所选内容,主要应用于列表中的每一项元素(如长按列表项弹出删除对话框)。
上下文操作模式: 在屏幕顶部栏(菜单栏)显示影响所选内容的操作选项,并允许用户选择多项,一般用于对列表类型的数据进行批量操作。
弹出菜单: 以垂直列表形式显示一系列操作选项,一般由某一控件触发,弹出菜单将显示在对应控件的上方或下方。它适用于提供与特定内容相关的大量操作。
理论上而言,使用XML和Java代码都可以创建Menu。但是在实际开发中,往往通过XML文件定义Menu,这样做有以下几个好处:
XML可以可视化菜单结构
菜单内容与应用的逻辑代码分离
可以使用应用资源框架,为不同的平台版本、屏幕尺寸创建最合适的菜单(如对drawable
、string
等系统资源的使用)
要定义Menu,我们需要在res
文件夹下新建menu
文件夹,用于存储与Menu相关的所有XML文件。
该菜单文件的构成元素包括:、
、
。
: 菜单根节点,能够包含一个或多个
和
元素,是定义菜单项的容器。
: 菜单项节点,用于定义MenuItem,可以嵌套元素,以便创建子菜单。
:
元素的不可见容器(可选),可以使用它对菜单项进行分组,使一组菜单项共享可用性和可见性等属性。
XML菜单文件的示例代码如下:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_save"
android:icon="@drawable/ic_settings"
android:title="保存"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/item_settings"
android:icon="@drawable/ic_settings"
android:title="设置"/
</menu>
其中,
是我们主要需要关注的元素,它的常见属性如下:
(MenuItem)
的唯一标识ifRoom
、never
、always
、withText
。showAsAction的取值说明
取值 | 说明 |
---|---|
ifRoom | 只有在标题栏中有空间时才将此项放置其中。如果没有足够的空间容纳标记为ifRoom的所有项,标题栏显示不了的菜单项将显示在溢出菜单中 |
withText | 菜单项的文本和图标一起显示,可以将此值与其他值用竖线分隔共同起作用 |
never | 菜单项永远不显示在标题栏,而隐藏在溢出菜单中 |
always | 始终将此菜单项显示在标题栏。除非必须始终显示在标题栏,否则不要使用该值 |
collapseActionView | 将菜单项折叠到一个按钮中,选择按钮展开,一般与ifRoom一起使用 |
要创建选项菜单,首先需要在XML文件中定义各个菜单项,具体代码如下:
XML代码:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_download"
android:icon="@drawable/ic_downward"
android:title="保存"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/item_settings"
android:icon="@drawable/ic_settings"
app:showAsAction="withText"
android:title="设置"/>
</menu>
可以看到,我们在XML文件中定义了两个普通的菜单项。同时,每一个
都有一个独特的showAsAction属性。
Activity通过重写onCreateOptionsMenu()
方法加载创建的XML菜单资源,在此方法中使用MenuInflater
类的inflate()方法将XML资源加载到Menu对象。除此之外,还可以使用add()
添加菜单项来动态加载菜单项,具体实现如下:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
//加载xml菜单资源
var inflater:MenuInflater = menuInflater
inflater.inflate(R.menu.option_menu,menu)
//动态加载菜单项
menu?.add(Menu.NONE,Menu.FIRST,3,"帮助")?.setIcon(R.drawable.ic_help)
//设置菜单添加图标有效
setIconsVisible(menu,true)
return true
}
当用户从选项菜单中选择菜单项时,系统将调用onOptionsItemSelected()
,此方法将传递所选的MenuItem
对象,通过调用该MenuItem
对象的itemId
方法获取菜单项的id,具体实现如下:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
//处理菜单项的选择
return when(item.itemId){
R.id.item_download-> {
Toast.makeText(this,"下载",Toast.LENGTH_LONG).show()
true
}
R.id.item_settings-> {
Toast.makeText(this,"设置",Toast.LENGTH_LONG).show()
true
}
Menu.FIRST -> {
Toast.makeText(this,"帮助",Toast.LENGTH_LONG).show()
true
}
else -> super.onOptionsItemSelected(item)
}
}
默认情况下,即便在菜单XML
文件中定义了android:icon
属性,在溢出菜单中图标也不会显示,本案例使用反射的方法获取Menu
对象的setOptionalIconsVisible()
,调用该方法显示图标icon
,具体实现如下:
//使用反射方法显示图标
private fun setIconsVisible(menu: Menu?, flag:Boolean){
//判断menu是否为空
if(menu != null){
try {
val method:Method = menu.javaClass.getDeclaredMethod("setOptionalIconsVisible", TYPE)
//强制使用方法
method.isAccessible
//调用该方法显示icon
method.invoke(menu,flag)
}catch (e:Exception){
e.printStackTrace()
}
}
}
然后将此方法添加到onCreateOptionsMenu()
方法的return语句之前即可,具体实现如下:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
//加载xml菜单资源
var inflater:MenuInflater = menuInflater
inflater.inflate(R.menu.option_menu,menu)
//动态加载菜单项
menu?.add(Menu.NONE,Menu.FIRST,3,"帮助")?.setIcon(R.drawable.ic_help)
//设置菜单添加图标有效
setIconsVisible(menu,true)
return true
}
通常上下文菜单是以浮动菜单的形式呈现的,用户长按(按住)一个支持上下文菜单的View时,菜单将以浮动列表的形式出现(类似于对话框)。 通常用户一次可对一个项目执行上下文操作(比如一个单独的控件或列表中的一项)。
Android提供了两种上下文操作菜单的方法。
通过调用registerForContextMenu()
注册与上下文菜单关联的View。如果将ListView
或GridView
作为参数传入,那么每个列表将会有相同的浮动上下文菜单。
registerForContextMenu(binding.etName)
在Activity
或Fragment
中实现onCreateContextMenu()
,动态加载Menu
资源。当注册后的View收到长按事件时,系统将调用此方法。
override fun onCreateContextMenu(
menu: ContextMenu?,
v: View?,
menuInfo: ContextMenu.ContextMenuInfo?
) {
super.onCreateContextMenu(menu, v, menuInfo)
if (v?.id == R.id.et_name){
//加载菜单XML资源
val inflater:MenuInflater = menuInflater
inflater.inflate(R.menu.context_menu,menu)
//动态创建菜单项
// menu?.add(Menu.NONE,Menu.FIRST,0,"拷贝")
// menu?.add(Menu.NONE,Menu.FIRST+1,0,"粘贴")
// menu?.add(Menu.NONE,Menu.FIRST+2,0,"清空")
}
}
在Activity
或Fragment
中实现onContextItemSelected()
,实现菜单项的单击逻辑。
override fun onContextItemSelected(item: MenuItem): Boolean {
return when(item.itemId){
R.id.item_copy-> {
Toast.makeText(this,item.title,Toast.LENGTH_LONG).show()
true
}
R.id.item_paste-> {
Toast.makeText(this,item.title,Toast.LENGTH_LONG).show()
true
}
R.id.item_clear -> {
binding.etName.setText("")
true
}
else -> super.onOptionsItemSelected(item)
}
}
上下文操作模式是ActionMode
对象的系统实现,当用户长按控件或选中复选框等组件时调用此模式,会在屏幕顶部出现上下文操作栏,显示用户对所选控件执行的操作。
ActionMode.Callback
接口在该接口的回调方法中,为上下文操作栏指定操作、响应菜单项的单击事件等。
// 上下文操作模式对象
private var actionMode: ActionMode? = null
// 实现ActionMode 中 CallBack回调接口
val actionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
// 创建方法,在启动上下文操作模式startActionMode(Callback)时调用
// 在此配置上下文菜单的资源
override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean {
mode.menuInflater.inflate(R.menu.context_menu, menu)
return true
}
// 在创建方法后进行调用
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
// 菜单项被点击,类似onContextItemSelected()方法
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_copy -> Toast.makeText(this@MainActivity, "拷贝", Toast.LENGTH_SHORT).show()
R.id.item_paste -> Toast.makeText(this@MainActivity, "粘贴", Toast.LENGTH_SHORT).show()
R.id.item_clear -> binding.etName.setText("")
else -> {
}
}
return true
}
// 上下文操作模式结束时被调用
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
}
}
该接口的回调方法与选项菜单的回调方法基本相同,只是需要传递与事件相关联的ActionMode
对象。onActionItemClicked()
方法用于处理菜单项的单击事件,与onContextItemSelected()
类似。当系统销毁操作模式时,需要在onDestroyActionMode()
方法中将actionMode
变量设置为null。
在用户名的EditView
控件的长按事件中调用startActionMode()
启动上下文操作模式,具体实现如下:
binding.etName.setOnLongClickListener(View.OnLongClickListener {
if (actionMode!=null){
return@OnLongClickListener false
}
actionMode = startActionMode(actionModeCallback)
view.isSelected
return@OnLongClickListener false
})
调用startActionMode()
方法返回ActionMode
对象,通过该对象响应上下文操作栏的事件。
PopupMenu
是依赖View存在的模态菜单。如果空间足够,它将显示在相应View的下方,否则显示在其上方。
弹出菜单适用于:
Spinner
控件,但不保留永久选择的下拉菜单。在res/menu
目录下创建PopupMenu
的XML
资源文件。
在EditText
控件的单击事件方法中实例化PopupMenu
对象,传递当前上下文对象及绑定的View对象,然后调用MenuInflater.inflate()
方法加载菜单XML
文件,调用PopupMenu
的setOnMenuItemClickListener()
设置单击菜单项的事件监听器,最后调用show()
方法显示菜单。
binding.etPhone.setOnClickListener(View.OnClickListener {
//创建弹出菜单对象(最低版本11),第二个参数是绑定的那个view
val popup:PopupMenu = PopupMenu(this,it)
//获取菜单填充器
val inflater:MenuInflater = popup.menuInflater
//填充菜单
inflater.inflate(R.menu.popup_menu,popup.menu)
//绑定菜单项的单击事件
popup.setOnMenuItemClickListener(this)
//显示弹出菜单
popup.show()
})
OnMenuItemClick
事件MainActivity
类实现PopupMenu.OnMenuItemClickListener
接口,重写onMenuItemClick()
回调方法,当用户选择菜单项时,系统调用onMenuItemClick()
进行处理。
// PopupMenu菜单项的点击事件
override fun onMenuItemClick(item: MenuItem?): Boolean {
when(item?.itemId){
R.id.item_copy -> Toast.makeText(this,"复制...",Toast.LENGTH_SHORT).show()
R.id.item_paste -> Toast.makeText(this,"粘贴...",Toast.LENGTH_SHORT).show()
else -> {
}
}
return false
}