一直想抽时间把些较为基础的控件统一系统化抽取出来形成Demo,方便记录以及解答,以下是项目的效果,如果有欠缺的,欢迎小伙伴评论区留言,项目的GitHub地址:AndroidMenuDemo
Menu的分类
菜单是Android应用中非常重要且常见的组成部分,主要分为三类:选项菜单
、上下文菜单
、弹出菜单
。
使用XML定义菜单
对于所有菜单类型,Android提供了标准的XML格式来定义菜单项。定义菜单项方法可以在XML菜单资源中定义菜单及其所有项,也可通过代码方式进行构建,推荐前者。定义后,可以在Activity或Fragment中扩充菜单资源(将其作为Menu对象加载)。
使用菜单资源是一种很好的做法,原因如下:
- 更易于使用XML可视化菜单结构
- 将菜单内容与应用的行为代码分离
- 允许利用应用资源框架,为不同的平台版本、屏幕尺寸和其他配置创建备用菜单配置。
要定义菜单,需在项目res/menu/
目录内创建一个XML文件,并使用以下元素构建菜单:
定义Menu
,即菜单项的容器。元素必须是该文件的根节点,并且能够包含一个或多个
和
元素。
- 标签
是菜单项,用于创建MenuItem
,可能包含嵌套的元素,以便创建子菜单。常见属性如下:
- android:id:菜单项(MenuItem)的唯一标识。
- android:icon:菜单项的图标(可选),在溢出菜单中需要显示图标以及标题,需要额外代码中配置。
- android:title:菜单项的标题(必选)
- android:titleCondensed:菜单项的短标题(可选),当菜单项标题太长时会显示该属性值
- android:onClick:方法名称。单击此菜单项时要调用的方法。该方法必须在Activity中声明为
public
,并将menuItem作为唯一参数,该参数指示单击的项。此方法优先于OnOptionsItemSelected() 的标准回调。
public void onGroupItemClick(MenuItem item) {}
警告:如果混淆代码时,请确保在混淆规则中对此属性方法进行排除,因为可能会破坏其功能。
- android:showAsAction:指定菜单项的显示方式,多个属性值之间可以使用
|
隔开,参数值有:
有效值 | 描述 |
---|---|
ifRoom | 在空间足够时,菜单项会显示在菜单栏中,否则收纳入溢出菜单中。 |
always | 菜单项永远不会被收纳到溢出菜单中,因此在菜单项过多的情况下可能超出菜单栏的显示范围。 |
never | 菜单项永远只会出现在溢出菜单中。 |
withText | 无论菜单项是否定义了icon属性,都只会显示它的标题,而不会显示图标。使用这种方式的菜单项默认会被收纳入溢出菜单中。 |
collapseActionView | 此选项是在Api14 引入的属性,搭配android:actionLayout 或者android:actionViewClass 使用,可起到折叠视图的效果 |
- android:actionLayout:布局资源,动作视图使用的布局文件。
- android:actionViewClass:类名,所使用的动作视图的全类名名称。例如当你使用
SearchView
只需要引入android.widget.SearchView
。在Api 11
引入的。
警告:如果项目混淆记得添加对应的忽略文件。 - android:actionProviderClass:操作提供器类名,例如使用
ShareActionProvider
需要引入"android.widget.ShareActionProvider",当然也可以自行自定义ActionProvider
的子类,此选项在Api14引入
。
警告:如果项目混淆记得添加对应的忽略文件。 - android:numericShortcut:数字快捷键。
- android:alphabeticShortcut:字母快捷键。
- android:alphabeticModifiers:字母快捷键的修饰符,默认值为
Control
,有效值:
值 | 描述 |
---|---|
META | 对应Meta键 |
CTRL | 对应Control键 |
ALT | 对应Alt键 |
SHIFT | 对应Shift键 |
SYM | 对应Sym键 |
FUNTION | 对应Function键 |
注意: 可以在属性中指定多个关键字。例如,android:alphabeticModifiers="CTRL|SHIFT",表示要触发相应的菜单项,用户需要同时按下两个Control和Shift键以及快捷键。
- android:numericModifiers:数字快捷键的修饰符,用法同上。
- android:checkable:是否可选中
- android:checked:是否选中
- android:visible:是否可见
- android:enabled:是否启用
- android:menuCategory:定义组优先级,常见值如下:
值 | 描述 |
---|---|
container | 对于属于容器的项目 |
system | 对于系统提供的项目。 |
secondary | 对于用户提供的辅助(不经常使用)选项的项目。 |
alternative | 对于对当前显示的数据执行备用操作的项目。 |
- orderInCategory:组内的重要性顺序
标签
是
元素的不可见容器(可选)。可以使用它对菜单项进行分组,使一组菜单项共享可用性和可见性等属性。常见属性不过多见解,可参考
。
- android:checkableBehavior:为组内项目实现单选或多选的选择模式。有三种可选值。
值 | 描述 |
---|---|
none | 所有项目均无法选中,默认值 |
single | 组中只有一个项目可以选中(单选按钮) |
all | 所有项目均可选中(复选框) |
XML方面需要注意点:当使用appcompat library时,菜单资源应引用app:namespace方式(showAsAction、actionViewClass、actionProviderClass),而不是android:namespace方式,对应引入的资源也需要调用兼容类;而相应如果不使用appcompat library,即需要使用android:namespace方式调用,这块涉及到Android Menu兼容配置。
选项菜单
选项菜单(OptionMenu):是应用的主菜单项,用于放置对应用起全局影响的操作,如搜索/设置等操作按钮。
选项菜单一般需要使用到以下几个方法:
public boolean onCreateOptionsMenu(Menu menu): 初始化Activity选项菜单方法,将要设置的菜单关联到menu中。在
Fragment
中调用public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)。public boolean onOptionsItemSelected(MenuItem item): 选项菜单点击事件的处理方法。
public void onOptionsMenuClosed(Menu menu): 当选项菜单关闭时(用户使用后退/菜单按钮取消菜单),都会调用此方法。
public boolean onPrepareOptionsMenu(Menu menu):选项菜单显示前调用的方法,一般在此方法调整菜单,在高版本显示icon以及一些逻辑处理。
-
public boolean onMenuOpened(int featureId, Menu menu): 当用户打开溢出菜单选项时调用,当菜单从一种类型更改为另一种类型(例如,从图标菜单更改为拓展菜单),也调用此方法。一般用此方法监听拓展菜单是否被打开,用于重置一些菜单操作。此方法仅Activity存在。‘
XML实现方式
采用XML是实现菜单主要方式,官方也推荐此种方案,具体如下:
- 实现option_menu.xml文件
- 在Activity或Fragment的
onCreateOptionsMenu
关联Option_menu.xml文件。
Activity中:
//创建选项菜单
getMenuInflater().inflate(R.menu.option_menu,menu);
Fragment中:
//创建选项菜单
inflater.inflate(R.menu.option_menu,menu);
- 要让Fragment中的菜单项显示出来,还需要在Fragment中调用setHasOptionsMenu(true)方法。传入true作为参数表明Fragment需要加载菜单项。建议在Fragment的onCreate方法中调用这个方法
/**
* 要让Fragment中的菜单项显示出来,还需要在Fragment中调用setHasOptionsMenu(true)方法。
* 传入true作为参数表明Fragment需要加载菜单项。
* 建议在Fragment的onCreate方法中调用这个方法
* @param savedInstanceState
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
代码实现方式
代码方式也是在onCreateOptionsMenu
挂载添加menu子项,主要核心代码
//添加普通菜单
public MenuItem add(int groupId, int itemId, int order, CharSequence title);
//添加子菜单
SubMenu addSubMenu(final int groupId, final int itemId, int order, final CharSequence title);
参数说明:
-
groupId
为组id,一般在主菜单中都是相同的组id,如果有subMenu的组id可以设置不同; -
itemId
为菜单项的唯一标识,参考xml布局方式中的id,一般用于菜单点击事件的区分; -
order
为序号,主要为组内排列顺序,相当于xml中的orderInCategory
属性。 -
title
菜单标题。
//通知系统刷新Menu
invalidateOptionsMenu();
上下文菜单
上下文菜单
上下文菜单: 是用户长按某一元素出现的浮动菜单。它提供的操作将影响所选内容,主要应用于列表中的每一项元素(如长按表项弹出删除对话框)。
上下文菜单在view实现方式(这里只贴出核心代码,具体可以在底部下载gitHub下载完整案例):
- ListView或RecycleView上下文选择菜单(有两种方法实现),单个View相同
- 方法一:通过registerForContextMenu方法实现注册
//在onCreate中对列表进行注册 registerForContextMenu(recyclerView); //在onDestroy中对列表进行解绑 unregisterForContextMenu(recyclerView); //重写方法onCreateContextMenu @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if(menuInfo instanceof RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo){ RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo contextMenuInfo = (RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo) menuInfo; if(contextMenuInfo != null && contextMenuInfo.getPostion() >= 0){ menu.setHeaderTitle("点击:"+mAdapter.getItem(contextMenuInfo.getPostion())); getMenuInflater().inflate(R.menu.context_menu,menu); } } super.onCreateContextMenu(menu, v, menuInfo); }
- 方法二:列表对象对onCreateContextMenu实现
以上是列表对于上下文菜单两种实现方式,监听可用过onContextItemSelected方法进行监听:recyclerView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if(menuInfo instanceof RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo){ RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo contextMenuInfo = (RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo) menuInfo; if(contextMenuInfo != null && contextMenuInfo.getPostion() >= 0){ menu.setHeaderTitle("点击:"+mAdapter.getItem(contextMenuInfo.getPostion())); getMenuInflater().inflate(R.menu.context_menu,menu); } } } });
@Override public boolean onContextItemSelected(MenuItem item) { if(item.getMenuInfo() instanceof RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo){ RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo contextMenuInfo = (RecyclerViewWithContextMenu.RecyclerViewContextMenuInfo) item.getMenuInfo(); if(contextMenuInfo != null && contextMenuInfo.getPostion() >= 0){ switch (item.getItemId()){ case R.id.context_menu_add: Toast.makeText(this,mAdapter.getItem(contextMenuInfo.getPostion())+":添加菜单被点击" , Toast.LENGTH_SHORT).show(); return true; case R.id.context_menu_del: Toast.makeText(this,mAdapter.getItem(contextMenuInfo.getPostion())+":删除菜单被点击" , Toast.LENGTH_SHORT).show(); return true; case R.id.context_menu_save: Toast.makeText(this,mAdapter.getItem(contextMenuInfo.getPostion())+":保存菜单被点击" , Toast.LENGTH_SHORT).show(); return true; } } } return super.onContextItemSelected(item); }
针对于RecycleView列表,因为Google只是更加倾向其布局的重用性,提倡视图自定义,所以这里需要自己实现对于ContextMenuInfo的实现,不然RecycleView无法实现上下文菜单。
public class RecyclerViewWithContextMenu extends RecyclerView { private RecyclerViewContextMenuInfo mContextMenuInfo = new RecyclerViewContextMenuInfo(); public RecyclerViewWithContextMenu(Context context) { super(context); } public RecyclerViewWithContextMenu(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public RecyclerViewWithContextMenu(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean showContextMenuForChild(View originalView) { getPositionByChild(originalView); return super.showContextMenuForChild(originalView); } @Override public boolean showContextMenuForChild(View originalView, float x, float y) { getPositionByChild(originalView); return super.showContextMenuForChild(originalView, x, y); } /** * 重写实现ContextMenuInfo返回,不然在onCreateContextMenu无法获取到menuInfo信息 * @return */ @Override protected ContextMenu.ContextMenuInfo getContextMenuInfo() { return mContextMenuInfo; } /** * 记录当前RecycleView中Item上下文菜单的Postion * @param originalView */ private void getPositionByChild(View originalView){ LayoutManager layoutManager = getLayoutManager(); if(layoutManager != null){ int position = layoutManager.getPosition(originalView); mContextMenuInfo.setPostion(position); } } public class RecyclerViewContextMenuInfo implements ContextMenu.ContextMenuInfo{ private int mPostion = -1; public int getPostion() { return mPostion; } public void setPostion(int mPostion) { this.mPostion = mPostion; } } }
上下文选择模式
上下文操作模式: 将在屏幕顶部栏(菜单栏)显示影响所选内容的操作选项,并允许用户选择多项,一般用于对列表类型的数据进行批量操作。上下文选择模式针对不同的列表(ListView、RecycleView)或单视图实现方式有些区别,具体核心代码如下所示:
- ListView实现上下文选择模式
//为Listview配置上下文操作模式
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
//当列表中的项目选中或取消勾选时,这个方法会被触发
//可以在这个方法中做一些更新操作,比如更改上下文操作栏的标题
//这里显示已选中的项目数
mode.setTitle("已选中:"+listView.getCheckedItemCount()+"项");
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.context_menu,menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
//可以对上下文操作栏做一些更新操作(会被ActionMode的invalidate方法触发)
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()){
case R.id.context_menu_add:
StringBuilder sb = new StringBuilder();
for (long id:listView.getCheckedItemIds()) {
sb.append(id);
}
Toast.makeText(ContextMenu2Activity.this, "点击了添加按钮"+sb.toString(), Toast.LENGTH_SHORT).show();
//关闭上下文操作栏
mode.finish();
return true;
case R.id.context_menu_del:
Toast.makeText(ContextMenu2Activity.this, "点击了删除按钮", Toast.LENGTH_SHORT).show();
//关闭上下文操作栏
mode.finish();
return true;
case R.id.context_menu_save:
Toast.makeText(ContextMenu2Activity.this, "点击了保存按钮", Toast.LENGTH_SHORT).show();
//关闭上下文操作栏
mode.finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
//在上下文操作栏被移除时会触发,可以对Activity做一些必要的更新
//默认情况下,此时所有的选中项将会被取消选中
}
});
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
Toast.makeText(ContextMenu2Activity.this, "点击了菜单", Toast.LENGTH_SHORT).show();
}
});
- RecycleView实现上下文选择模式,在RecycleView中官方并未对齐支持上下文菜单以及上下文操作模式,但官方针对这个情况,官方于19年3月提出
RecyclerView Selection
插件库,试图解决此问题。
要将RecyclerView Selection库添加到Android Studio项目,请implementation 在app 模块的build.gradle 文件中提及以下依赖项:
//recycleView
implementation 'com.android.support:recyclerview-v7:28.0.0'
//recyclerview-selection(如果采用androidx可采用对应目录下的版本)
implementation 'com.android.support:recyclerview-selection:28.0.0'
在Adapter中明确指定指出此适配器的每个项目将具有类型的唯一稳定标识符。
//在adapter构造函数中实现
//明确指出此适配器的每个项目将具有类型的唯一稳定标识符非常重要Long。setHasStableIds(true);
/**
* 为了能够使用项目的位置作为其唯一标识符,需重写getItemId
* @param position
* @return
*/
@Override
public long getItemId(int position) {
return position;
}
在ViewHolder中实现可以调用以唯一标识所选列表项的方法。
public ItemDetailsLookup.ItemDetails getItemDetails(){
return new LongItemDetails(getAdapterPosition(),getItemId());
}
在onBindViewHolder()方法(如果采用BaseRecyclerViewAdapterHelper需要在convert和convertPayloads)实现调用此代码块。
if(mSelectionTracker != null){
if(mSelectionTracker.isSelected(getItemId(helper.getLayoutPosition()))){
helper.getConvertView().setBackgroundColor(Color.parseColor("#80deea"));
if(helper.tv instanceof CheckedTextView){
((CheckedTextView)helper.tv).setChecked(true);
}
}else {
helper.getConvertView().setBackgroundColor(Color.WHITE);
if(helper.tv instanceof CheckedTextView){
((CheckedTextView)helper.tv).setChecked(false);
}
}
}
并且挂载选择跟踪器
public void setSelectionTracker(SelectionTracker mSelectionTracker) {
this.mSelectionTracker = mSelectionTracker;
}
实现ItemDetailsLookup
这个类将为选择库提供有关与用户选择关联的项目的信息,该选择基于MotionEvent,所以我们必须映射到我们的ViewHolders,返回产生MotionEvent事件的item的信息
public class MyItemDetailsLookup extends ItemDetailsLookup {
private RecyclerView recyclerView;
public MyItemDetailsLookup(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
}
@Nullable
@Override
public ItemDetails getItemDetails(@NonNull MotionEvent motionEvent) {
View childViewUnder = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
if(childViewUnder != null){
RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(childViewUnder);
if(childViewHolder instanceof SelectionQuickAdapter.SelectionQickViewHolder){
return ((SelectionQuickAdapter.SelectionQickViewHolder)childViewHolder).getItemDetails();
}
}
return null;
}
}
在activity中创建选择跟踪器以及创建选择观察器
//创建选择跟踪器
mSelectionTracker = new SelectionTracker.Builder(
"mySelection",
recyclerView,
new StableIdKeyProvider(recyclerView), //密钥提供者
new MyItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
mAdapter.setSelectionTracker(mSelectionTracker);
//创建选择观察器
mSelectionTracker.addObserver(new SelectionTracker.SelectionObserver() {
@Override
public void onItemStateChanged(@NonNull Object key, boolean selected) {
super.onItemStateChanged(key, selected);
Log.i(TAG, "onItemStateChanged: "+key+" to "+selected);
setSelectionTitle();
}
@Override
public void onSelectionRefresh() {
super.onSelectionRefresh();
}
@Override
public void onSelectionChanged() {
super.onSelectionChanged();
Log.i(TAG, "onSelectionChanged: ");
setSelectionTitle();
}
@Override
public void onSelectionRestored() {
super.onSelectionRestored();
}
});
至此,RecycleView创建上下文模式核心代码已完成,详情可参考RecyclerView-Selection
- 为单个View设置上下文选择模式
为单个View设置上下文操作模式同样可以分为两步:
- 实现ActionMode.Callback接口。在这个接口的回调方法中,可以为上下文操作栏加载Menu资源,也可以响应操作项目的点击事件,还可以处理其他需要的操作。
- 当需要显示操作栏时(例如,用户长按视图),调用Activity的startActionMode方法,并传入前面创建的Callback对象作为参数。
btnContextMode.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if(actionMode == null){
actionMode = startSupportActionMode(callback);
v.setSelected(true); //设置View为选中状态
return true;
}
return false;
}
});
private ActionMode.Callback callback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.context_menu,menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
switch (menuItem.getItemId()){
case R.id.context_menu_add:
Toast.makeText(ContextMenu4Activity.this, "点击了添加按钮", Toast.LENGTH_SHORT).show();
return true;
case R.id.context_menu_del:
Toast.makeText(ContextMenu4Activity.this, "点击了删除按钮", Toast.LENGTH_SHORT).show();
return true;
case R.id.context_menu_save:
Toast.makeText(ContextMenu4Activity.this, "点击了保存按钮", Toast.LENGTH_SHORT).show();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
}
};
弹出菜单
弹出菜单: 以垂直列表形式显示一系列操作选项,一般由某一控件触发,弹出菜单将显示在对应控件的上方或下方。它适用于提供与特定内容相关的大量操作。
主要核心代码:
private void createPopupMenu(View view){
PopupMenu popupMenu = new PopupMenu(this,view);
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH){
//写法1:getMenuInflater().inflate(R.menu.context_menu,popupMenu.getMenu());
popupMenu.getMenuInflater().inflate(R.menu.context_menu,popupMenu.getMenu());
}else {
//在 API 级别 14 及更高版本中,您可以将两行合并在一起,使用 PopupMenu.inflate() 扩充菜单。
popupMenu.inflate(R.menu.context_menu);
}
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()){
case R.id.context_menu_add:
Toast.makeText(PopupMenuActivity.this, "点击保存菜单", Toast.LENGTH_SHORT).show();
return true;
case R.id.context_menu_del:
Toast.makeText(PopupMenuActivity.this, "点击删除菜单", Toast.LENGTH_SHORT).show();
return true;
case R.id.context_menu_save:
Toast.makeText(PopupMenuActivity.this, "点击保存菜单", Toast.LENGTH_SHORT).show();
return true;
default:
return false;
}
}
});
popupMenu.show();
}