Android Developer

Android的Menu菜单,这一篇就够了

2019-10-22  本文已影响0人  AnandLin

一直想抽时间把些较为基础的控件统一系统化抽取出来形成Demo,方便记录以及解答,以下是项目的效果,如果有欠缺的,欢迎小伙伴评论区留言,项目的GitHub地址:AndroidMenuDemo

效果

Menu的分类

菜单是Android应用中非常重要且常见的组成部分,主要分为三类:选项菜单上下文菜单弹出菜单

使用XML定义菜单

对于所有菜单类型,Android提供了标准的XML格式来定义菜单项。定义菜单项方法可以在XML菜单资源中定义菜单及其所有项,也可通过代码方式进行构建,推荐前者。定义后,可以在Activity或Fragment中扩充菜单资源(将其作为Menu对象加载)。

使用菜单资源是一种很好的做法,原因如下:

要定义菜单,需在项目res/menu/目录内创建一个XML文件,并使用以下元素构建菜单:

<menu>标签

定义Menu,即菜单项的容器。<menu>元素必须是该文件的根节点,并且能够包含一个或多个<item><group>元素。

<item>标签

<item>是菜单项,用于创建MenuItem,可能包含嵌套的<menu>元素,以便创建子菜单。常见属性如下:

public void onGroupItemClick(MenuItem item) {}

警告:如果混淆代码时,请确保在混淆规则中对此属性方法进行排除,因为可能会破坏其功能。

有效值 描述
ifRoom 在空间足够时,菜单项会显示在菜单栏中,否则收纳入溢出菜单中。
always 菜单项永远不会被收纳到溢出菜单中,因此在菜单项过多的情况下可能超出菜单栏的显示范围。
never 菜单项永远只会出现在溢出菜单中。
withText 无论菜单项是否定义了icon属性,都只会显示它的标题,而不会显示图标。使用这种方式的菜单项默认会被收纳入溢出菜单中。
collapseActionView 此选项是在Api14引入的属性,搭配android:actionLayout或者android:actionViewClass使用,可起到折叠视图的效果
描述
META 对应Meta键
CTRL 对应Control键
ALT 对应Alt键
SHIFT 对应Shift键
SYM 对应Sym键
FUNTION 对应Function键

注意: 可以在属性中指定多个关键字。例如,android:alphabeticModifiers="CTRL|SHIFT",表示要触发相应的菜单项,用户需要同时按下两个Control和Shift键以及快捷键。

描述
container 对于属于容器的项目
system 对于系统提供的项目。
secondary 对于用户提供的辅助(不经常使用)选项的项目。
alternative 对于对当前显示的数据执行备用操作的项目。

<group>标签

<group><item>元素的不可见容器(可选)。可以使用它对菜单项进行分组,使一组菜单项共享可用性和可见性等属性。常见属性不过多见解,可参考<item>标签

描述
none 所有项目均无法选中,默认值
single 组中只有一个项目可以选中(单选按钮)
all 所有项目均可选中(复选框)

XML方面需要注意点:当使用appcompat library时,菜单资源应引用app:namespace方式(showAsAction、actionViewClass、actionProviderClass),而不是android:namespace方式,对应引入的资源也需要调用兼容类;而相应如果不使用appcompat library,即需要使用android:namespace方式调用,这块涉及到Android Menu兼容配置。

选项菜单

选项菜单(OptionMenu):是应用的主菜单项,用于放置对应用起全局影响的操作,如搜索/设置等操作按钮。

选项菜单一般需要使用到以下几个方法:

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/menu_search"
        android:orderInCategory="1"
        android:title="搜索菜单"
        android:icon="@android:drawable/ic_menu_search"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="android.support.v7.widget.SearchView"/>
    <item android:id="@+id/menu_share"
        android:title="分享菜单"
        android:icon="@android:drawable/ic_menu_share"
        android:orderInCategory="1"
        app:showAsAction="never"
        app:actionProviderClass="android.support.v7.widget.ShareActionProvider"/>
    <item android:id="@+id/menu_collect"
        android:orderInCategory="1"
        android:title="收藏菜单"
        android:icon="@android:drawable/btn_star_big_on"
        app:actionLayout="@layout/layout_collect"
        app:showAsAction="never|collapseActionView"/>
    <item android:id="@+id/menu_previous"
        android:title="这是上一步的菜单展示效果"
        android:titleCondensed="上一步"
        android:orderInCategory="1"
        app:showAsAction="always"
        android:onClick="onPreviousMenu"/>
    <item android:id="@+id/menu_next"
        android:title="这是下一步的菜单展示效果"
        android:titleCondensed="下一步"
        android:orderInCategory="1"
        app:showAsAction="always"
        android:onClick="onNextMenu"/>
    <item android:id="@+id/menu_single_check"
        android:title="单选按钮"
        android:orderInCategory="1"
        app:showAsAction="withText">
        <menu>
            <group android:id="@+id/group_single"
                android:checkableBehavior="single"
                android:menuCategory="system">
                <item android:id="@+id/single_menu_01"
                    android:title="单选按钮01"
                    android:checked="true"/>
                <item android:id="@+id/single_menu_02"
                    android:title="单选按钮02"/>
                <item android:id="@+id/single_menu_03"
                    android:title="单选按钮03"/>
            </group>
        </menu>
    </item>
    <item android:id="@+id/menu_all_check"
        android:title="多选按钮"
        android:orderInCategory="1"
        app:showAsAction="withText">
        <menu>
            <group android:id="@+id/group_all"
                android:checkableBehavior="all"
                android:menuCategory="system"
                android:enabled="true">
                <item
                    android:id="@+id/all_menu_01"
                    android:title="多选按钮01"
                    android:checked="true"/>
                <item
                    android:id="@+id/all_menu_02"
                    android:title="多选按钮02"/>
                <item
                    android:id="@+id/all_menu_03"
                    android:title="多选按钮03"/>
            </group>
        </menu>
    </item>
</menu>

Activity中:

//创建选项菜单
getMenuInflater().inflate(R.menu.option_menu,menu);

Fragment中:

//创建选项菜单
inflater.inflate(R.menu.option_menu,menu);
    /**
     * 要让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);

参数说明:

//通知系统刷新Menu
invalidateOptionsMenu();

上下文菜单

上下文菜单

上下文菜单
上下文菜单: 是用户长按某一元素出现的浮动菜单。它提供的操作将影响所选内容,主要应用于列表中的每一项元素(如长按表项弹出删除对话框)。
上下文菜单在view实现方式(这里只贴出核心代码,具体可以在底部下载gitHub下载完整案例):

上下文选择模式

上下文操作模式: 将在屏幕顶部栏(菜单栏)显示影响所选内容的操作选项,并允许用户选择多项,一般用于对列表类型的数据进行批量操作。上下文选择模式针对不同的列表(ListView、RecycleView)或单视图实现方式有些区别,具体核心代码如下所示:

//为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();
            }
        });

要将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<Long> {
    private RecyclerView recyclerView;

    public MyItemDetailsLookup(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
    }

    @Nullable
    @Override
    public ItemDetails<Long> 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.<Long>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设置上下文操作模式同样可以分为两步:

  1. 实现ActionMode.Callback接口。在这个接口的回调方法中,可以为上下文操作栏加载Menu资源,也可以响应操作项目的点击事件,还可以处理其他需要的操作。
  2. 当需要显示操作栏时(例如,用户长按视图),调用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();
    }
上一篇 下一篇

猜你喜欢

热点阅读