Android之三种Menu的使用与分析

  请尊重他人劳动成果,请勿随意剽窃,转载请注明,谢谢! 转载请注明出处: http://blog.csdn.net/evan_man/article/details/51685022

    以下说明全部针对Android3.0(Api-11)。 本指南将介绍三种基本菜单分别是PartA:操作栏(选项菜单OptionMenu)、PartB:上下文操作模式(ActionMode)、PartC:弹出菜单(PopupMenu)。

PartA:操作栏(选项菜单)——onCreateOptionsMenu()创建的

    以屏幕操作项和溢出选项的组合形式呈现选项菜单中的各项。
Android之三种Menu的使用与分析_第1张图片
一、创建
    为 Activity 指定选项菜单,重写 onCreateOptionsMenu()(Fragment 对应 onCreateOptionsMenu() 回调)。启动 Activity 时会调用 onCreateOptionsMenu()方法,因此可以在该方法中将菜单资源(使用 XML 定义)注入到回调方法的Menu 中。

二、处理响应事件
    重写 onOptionsItemSelected() 方法,方法将传递所选中的 MenuItem。您可以通过调用 getItemId() 方法来识别对应item,该方法将返回菜单项的唯一 ID(由菜单资源中的 android:id 属性定义)。

补充:动态内容菜单内容
    当菜单项显示在操作栏中时,选项菜单被视为始终处于打开状态。发生事件时,如果您要执行菜单更新,则必须调用 invalidateOptionsMenu() 来请求系统调用 onPrepareOptionsMenu()。在onPrepareOptionsMenu()方法中去通过 menu.add() 等操作修改菜单项。

备注限于篇幅原因该部分详细内容参考ToolBar博客 http://blog.csdn.net/evan_man/article/details/51684947


PartB:上下文操作模式(ActionMode

    用户长按某一元素时出现的浮动菜单,此模式在屏幕顶部栏显示影响所选内容的操作项目,并允许用户选择多项,会直接影响对应的内容。上下文操作模式是 ActionMode 的一种系统实现,它将用户交互的重点转到执行上下文操作上。
Android之三种Menu的使用与分析_第2张图片
一、为单个视图创建上下文操作模式
  • 实现 ActionMode.Callback 接口:
    • 回调方法中,您既可以为上下文操作栏指定操作选项(显示内容),又可以响应操作项目的点击事件,还可以处理操作模式的其他生命周期事件。
    • private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
      public boolean onCreateActionMode(ActionMode mode, Menu menu) {
              MenuInflater inflater = mode.getMenuInflater();
              inflater.inflate(R.menu.context_menu, menu);
              return true;
      }
      //该方法用于创建Menu视图
      public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
              switch (item.getItemId()) {
                  case R.id.menu_share:
                      shareCurrentItem();
                      mode.finish(); 
                      return true;
                  default:
                      return false;
              }
      }
      //该方法用于对用户的操作做出相应的反馈
      public void onDestroyActionMode(ActionMode mode) {
              mActionMode = null;
      }
      //及时清除mActionMode引用,一者为了垃圾回收,二者为了后面再次进入上下文操作模式考虑
      }


  • 在View的LongClickListener中调用 startActionMode() 启用上下文操作模式
  • private ActionMode mActionMode;
    someView.setOnLongClickListener(new View.OnLongClickListener() { //someView是一个普通的View控件
        public boolean onLongClick(View view) {
            if (mActionMode == null) { mActionMode = getActivity().startActionMode(mActionModeCallback)};
            //根据情况如果消耗事件则返回true,没有消耗事件则返回false。
            view.setSelected(true);
            ..............
            return true;
        }
    });
  • 在当前Activity或者Application的样式中对ActionMode的样式进行设置,一般设置如下
    •    

二、为listView等复杂视图创建上下文操作模式
  • 实现 AbsListView.MultiChoiceModeListener 接口,并使用 setMultiChoiceModeListener() 为视图组设置该接口。
    • 侦听器的回调方法中,您既可以为上下文操作栏指定操作,也可以响应操作项目的点击事件,还可以处理从 ActionMode.Callback 接口继承的其他回调。
    • listView.setMultiChoiceModeListener(new MultiChoiceModeListener() { ......}
  • 使用 CHOICE_MODE_MULTIPLE_MODAL 参数调用 setChoiceMode()。
    • listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
注意RecyclerView并没有提供setChoiceMode这样的一个方法。但是要实现上述功能也不难,大体思路可以如下:( Adapter中需要声明SparseArray markPosition集合; ActionMode actionMode浮动上下文操作栏引用;  )
  • 在Adapter的onCreateViewHolder方法中给view注册一个 View.OnLongClickListener()监听器,该监听器的内容会首先检测当前 mActionMode 值是否为空,即浮动上下文操作栏显示与否。如果为空则调用mActionMode = getActivity().startActionMode(mActionModeCallback)显示浮动上下文操作栏。最后不管 mActionMode 值是否为空,都会将当前view对应在Adapter中的position记录进markPosition集合中,同时调用view.setSelected(true){如果期望View在选中时有特别的显示效果可以将view的background设置为一个State List类型的Drawable}。
  • Adapter的onBindViewHolder方法中首先检测当前position是否属于前面的集合中的值,如果不属于则调用view.setSelected(false),属于则调用调用view.setSelected(true)。
  • 最后点击浮动上下文菜单栏的某个按钮时,将之前的集合元素取出,处理完后清空集合。
  • 补充:如果为获得更好的用户体验,可以在view的onClickListener中检测actionMode,如果该引用不为空则记录当前位置Postion进入集合;否则进行跳转、删除等操作。

ActionMode底层分析(分析目的是修改上下文浮动操作栏的返回图标)

getActivity().startActionMode() 
@Activity.class
public ActionMode startActionMode(ActionMode.Callback callback) {
        return mWindow.getDecorView().startActionMode(callback);
}
其中mWindow = new PhoneWindow(this);因此我们往下看PhoneWindow的startActionMode方法。
@PhoneWindow.class
public ActionMode startActionMode(ActionMode.Callback callback) {
            if (mActionMode != null) { mActionMode.finish(); }
            final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapper(callback);
            ActionMode mode = null;
            ...........
            if (mode != null) {
                mActionMode = mode;
            } else {
                if (mActionModeView == null) {//创建ActionModeView
                    if (isFloating()) {
                        mActionModeView = new ActionBarContextView(mContext);//note1
                        mActionModePopup = new PopupWindow(mContext, null,
                                com.android.internal.R.attr.actionModePopupWindowStyle); 
                        mActionModePopup.setWindowLayoutType(
                                WindowManager.LayoutParams.TYPE_APPLICATION);
                        mActionModePopup.setContentView(mActionModeView); //mActionModeView这里是准备被显示的View
                        mActionModePopup.setWidth(MATCH_PARENT);

                        TypedValue heightValue = new TypedValue();
                        mContext.getTheme().resolveAttribute(
                                com.android.internal.R.attr.actionBarSize, heightValue, true);
                        final int height = TypedValue.complexToDimensionPixelSize(heightValue.data,
                                mContext.getResources().getDisplayMetrics());
                        mActionModeView.setContentHeight(height);
                        mActionModePopup.setHeight(WRAP_CONTENT);
                        mShowActionModePopup = new Runnable() {
                            public void run() {
                                mActionModePopup.showAtLocation( //note2
                                        mActionModeView.getApplicationWindowToken(),
                                        Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0);
                            }
                        };
                    } else {
                        ViewStub stub = (ViewStub) findViewById(
                                com.android.internal.R.id.action_mode_bar_stub);
                        if (stub != null) {
                            mActionModeView = (ActionBarContextView) stub.inflate();
                        }
                    }
                }

                if (mActionModeView != null) { //显示ActionModeView
                    mActionModeView.killMode();
                    mode = new StandaloneActionMode(getContext(), mActionModeView, wrappedCallback,
                            mActionModePopup == null);
                    if (callback.onCreateActionMode(mode, mode.getMenu())) {//创建菜单到ActionMode中
                        mode.invalidate();
                        mActionModeView.initForMode(mode);//note3
                        mActionModeView.setVisibility(View.VISIBLE);
                        mActionMode = mode;
                        if (mActionModePopup != null) {
                            post(mShowActionModePopup); //交给Handler去执行前面的Runnable异步方法
                        }
                        mActionModeView.sendAccessibilityEvent(
                                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
                    } else {
                        mActionMode = null;
                    }
                }
            }
            ....
            return mActionMode;
}
-------------------------------------------------------------
note1:
ActionBarContextView()@ActionBarContextView.class
ActionBarContextView(mContext)-->最终调用构造器为:public ActionBarContextView(   Context context, null,com.android.internal.R.attr.actionModeStyle, 0)
构造器内部会调用final TypedArray a = context.obtainStyledAttributes( null R.styleable.ActionMode, com.android.internal.R.attr.actionModeStyle, 0);即从主题中定义的actionModeStyle样式文件中和主题直接定义的属性中获取到如下属性:
       
       
       
       
       
       
       
       
       
       
       
       
 
下面这一行是获取返回按键布局的非常关键的一行代码!!!也可以说closeItemLayout属性定义了整个ActionMode最左边的布局视图信息,注意如果要自定义返回按钮其id必须为@+id/action_mode_close_button。
mCloseItemLayout = a.getResourceId(   com.android.internal.R.styleable.ActionMode_closeItemLayout,    R.layout.action_mode_close_item); 

note2
public void showAtLocation(IBinder token, int gravity, int x, int y) {
        .........
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);
        .....
        invokePopup(p);
}
将视图显示到手机界面上。具体内容讲完note3后就会详细分析。

note3
@ActionBarContextView.class
public void initForMode(final ActionMode mode) {
        if (mClose == null) {
            LayoutInflater inflater = LayoutInflater.from(mContext);
            mClose = inflater.inflate(mCloseItemLayout, this, false);  
            //看到这里都想哭了,,,,,,,找了半天就是想搞明白那个返回键究竟在哪设置的!!!!
            //这里终于找到了,mCloseItemLayout就是定义了返回键的布局文件,它的定义看note1,即ActionBarContextView的构造器。
            addView(mClose);
        } else if (mClose.getParent() == null) {
            addView(mClose);
        }
        View closeButton = mClose.findViewById(R.id.action_mode_close_button);
        closeButton.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                mode.finish(); //点击返回按钮则销毁当前ActionBarContextView视图
            }
        });
        ....
}
-------------------------------------------------------------
最后我们可以对PopupWindow做一个总结:PopupWindow.setContentView(View v); 方法参数是PopupWindow将要具体显示的内容,而PopupWindow的任务就是在屏幕中合适的位置将该View显示出来。但是该方法并不会将View显示出来,需要调用如下两个方法才能最终显示出来:showAtLocation(View parent, int gravity, int x, int y) 、showAsDropDown(View anchor, int xoff, int yoff)。showAtLocation是在一个特定的位置中显示视图,而showAsDropDown则会首先选取指定视图的左下方或者左上方显示视图。showAtLocation()和showAsDropDown()两者底层显示过程基本一致,先后调用preparePopup()和 invokePopup()方法,前者对即将显示的视图进行初始化操作,后者调用mWindowManager.addView(decorView, p);将视图显示出来。
PopupWindow的构造器中有如下的方法:final TypedArray a = context.obtainStyledAttributes( null , R.styleable.PopupWindow, com.android.internal.R.attr.popupWindowStyle,0);因此它从主题中定义的popupWindowStyle样式文件中和主题直接定义的属性中获取到如下属性:
       
       
       
       
       
       
       
       
       
       
       
       


PartC:弹出菜单(PopupMenu)

    PopupMenu 是锚定到 View 的模态菜单。如果空间足够,它将显示在定位视图左下方,否则显示在其左上方。适用于提供与特定内容相关的大量操作,或者为命令的另一部分提供选项。不会直接影响对应的内容。
一、实例化PopupMenu及其构造器函数
  • 该函数将提取当前应用的 Context 以及菜单应锚定到的 View。
  • PopupMenu popup = new PopupMenu(this, v);
二、使用 MenuInflater 将菜单资源扩充到 PopupMenu.getMenu() 返回的 Menu 对象中
  • MenuInflater inflater = popup.getMenuInflater();
  • inflater.inflate(R.menu.actions, popup.getMenu());
  • API 级别 14 及更高版本中,您可以改为使用 PopupMenu.inflate()
三、调用 PopupMenu.show()
  • PopupMenu.show();
  • 这里就会显示上图的菜单选项了
四、处理点击事件
  • 实现 PopupMenu.OnMenuItemClickListener 接口,并通过调用 setOnMenuItemclickListener() 将其注册到 PopupMenu
补充1:监听PopupMenu销毁
  • 当用户选择项目或触摸菜单以外的区域时,系统即会清除此菜单。 您可使用 PopupMenu.OnDismissListener 侦听清除事件。
补充2:显示图标
  • PopupMenu默认是不显示图标的,而且对外也不提供相应的修改方法,通过反射进行如下修改。
  • //使用反射,强制显示菜单图标  
  •  try {  
                Field field = popupMenu.getClass().getDeclaredField("mPopup");  
                field.setAccessible(true);  
                MenuPopupHelper mHelper = (MenuPopupHelper) field.get(popupMenu);  
                mHelper.setForceShowIcon(true);  
    } catch (IllegalAccessException | NoSuchFieldException e) {      e.printStackTrace();   } 


补充3:下一个完整的用例:
private void showPopupMenu(View v){
        PopupMenu popup = new PopupMenu(this, v);
        MenuInflater inflater = popup.getMenuInflater();
        inflater.inflate(R.menu.popmenu, popup.getMenu());
        try {
            Field field = popup.getClass().getDeclaredField("mPopup");
            field.setAccessible(true);
            MenuPopupHelper mHelper = (MenuPopupHelper) field.get(popup);
            mHelper.setForceShowIcon(true);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();   }
        popup.setOnMenuItemClickListener(new OnPopupMenuItemClickListener(this));
        popup.setGravity(Gravity.RIGHT);
        popup.show();
}
补充4:主题设置(PopupMenu的 字体背景等
在xml文件中 标签中我们是无法设置背景和字体颜色的,通常情况是通过修改Theame属性来实现的,具体如下:
    
  

    
    
    

注意:上面的ProfileTheme在manifest.xml文件中可以加给某个Activity如:


PopupMenu底层分析(绘制流程探究)

显示流程

android.support.v7.widget. PopupMenu 中有一个域——private MenuPopupHelper mPopup;大部分操作都是委托它去执行的。
android.support.v7.view.menu. MenuPopupHelper,中有一个tryShow()方法,该方法负责具体绘制,完成工作有:
  1. ListPopupWindow mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); //创建一个ListPopupWindow对象,该对象继承自Object
  2. 对mPopup进行一些初始化设置,如
    • mPopup.setOnDismissListener(this);
    • mPopup.setOnItemClickListener(this);
    • mPopup.setAdapter(mAdapter);
  3. mPopup.show(); //调用该方法显示视图
  • 总结:MenuPopupHelper管理了一个ListPopupWindow对象
android.support.v7.widget. ListPopupWindow的show方法,完成工作有:
  1. 调用int height = buildDropDown()方法,该方法进行的操作有:
    • 创建android.support.v7.widget.ListPopupWindow.DropDownListView mDropDownList = new DropDownListView(context, !mModal);对象
    • 对mDropDownList进行一些初始化设置,如
      • mDropDownList.setAdapter(mAdapter);
      • mDropDownList.setOnItemClickListener(mItemClickListener);
      • mDropDownList.setFocusable(true);
      • mDropDownList.setFocusableInTouchMode(true);
      • mDropDownList.setOnScrollListener(mScrollListener);
      • mDropDownList.setOnItemSelectedListener
    • mPopup.setContentView(mDropDownList); //mPopup是在创建ListPopupWindow时创建的,PopupWindow mPopup = new PopupWindow(context, attrs, defStyleAttr)。通过setContentView将PopupWindow和DropDownList进行绑定。
    • 注意:android.support.v7.widget.ListPopupWindow.DropDownListView extends ListViewCompat 而ListViewCompat extends ListView,那么具体的itemView自然是交给对应的Adapter来提供,对于Adapter查看后面的内容
  2. 对mPopup进行一些初始化设置,如
    1. mPopup.setWidth(widthSpec);
    2. mPopup.setHeight(heightSpec);
    3. mPopup.setOutsideTouchable(...)
    4. mPopup.setTouchInterceptor(mTouchInterceptor);
  3. mPopup.showAsDropDown(View anchor, int xoff, int yoff, int gravity)方法。该方法内部完成工作有:
    • {
    •   final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
    •   preparePopup(p);//布局信息初始化
    •   
    •   final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
    •   updateAboveAnchor(aboveAnchor);//计算当前弹出菜单应该显示在当前View的上面还是下面,对背景色进行设置

    •   invokePopup(p); 
    •   //使用了mWindowManager.addView(PopupDecorView mDecorView , ViewGroup.LayoutParams params)方法。
    •   //PopupDecorView mDecorView = new PopupDecorView(mContext);
    •   //decorView.addView(contentView, ViewGroup.LayoutParams.MATCH_PARENT, height);contentView是我们在第一步通过mPopup.setContentView(mDropDownList)方法传进来的DropDownListView对象
    •   //decorView.setClipChildren(false);
    •   //decorView.setClipToPadding(false);
    • }
  • 总结:ListPopupWindow对象管理了一个PopupWindow对象,PopupWindow的showAsDropDown方法将一个PopupDecorView对象通过WindowManager.addView方法添加到屏幕上,PopupDecorView中具体显示的内容则是DropDownListView对象。
-------------------------------------------------------------
最后我们可以对PopupWindow做一个总结:PopupWindow.setContentView(View v); 方法参数是PopupWindow将要具体显示的内容,而PopupWindow的任务就是在屏幕中合适的位置将该View显示出来。但是该方法并不会将View显示出来,需要调用如下两个方法才能最终显示出来:showAtLocation(View parent, int gravity, int x, int y) 、showAsDropDown(View anchor, int xoff, int yoff)。showAtLocation是在一个特定的位置中显示视图, showAsDropDown则会首先选取指定视图的左下方或者左上方显示视图 。showAtLocation()和showAsDropDown()两者底层显示过程基本一致,先后调用preparePopup()和 invokePopup()方法,前者对即将显示的视图进行初始化操作,后者调用mWindowManager.addView(decorView, p);将视图交给WindowManager显示出来。之所以showAtLocation和showAsDropDown会 得到两种不同显示效果的原因在于WindowManager的addView(View view, ViewGroup.LayoutParams params)方法的第二个参数,WindowManager.LayoutParams——它的x、y决定视图在屏幕中的起始显示位置。


MenuPopupHelper中的MenuAdapter的定义
android.support.v7.view.menu.MenuPopupHelper中有一个适配器private class MenuAdapter extends BaseAdapter提供PopupMenu所要显示的视图。
其中一个很重要的getView方法如下:
public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); //note1
            }
            MenuView.ItemView itemView = (MenuView.ItemView) convertView;
            if (mForceShowIcon) {
                ((ListMenuItemView) convertView).setForceShowIcon(true); //note2
            }
            itemView.initialize(getItem(position), 0); //note3
            return convertView;
}

1、static final int ITEM_LAYOUT = R.layout.abc_popup_menu_item_layout;该布局文件只有一个Title和SubTitle的布局信息。外面包裹的是一个android.support.v7.view.menu.ListMenuItemView类型(是一个继承自LinearLayout的类)
2、设置ListMenuItemView的标志位 mPreserveIconSpacing = mForceShowIcon = true。mForceShowIcon域可以通过MenuPopupHelper的setForceShowIcon方法进行设置,默认是false。
3、调用android.support.v7.view.menu.ListMenuItemView的initialize方法对该行视图进行初始化设置:设置title、icon等。最后返回当前的convertView对象。

MenuPopupHelper中的MenuAdapter的传递
android.support.v7.view.menu.MenuPopupHelper在创建android.support.v7.widget.ListPopupWindow对象的时候会将MenuAdapter传过去。android.support.v7.widget.ListPopupWindow在创建android.support.v7.widget.ListPopupWindow.DropDownListView的时候也会将MenuAdapter传过去。DropDownListView是一个继承自ListView的控件, 之后就是ListView和Adapter的情况了,该部分可以参考ListView知识。

关于Style的设置

ListPopupWindow的构造器中有如下方法:
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,  R.attr.popupMenuStyle, 0);即从主题中定义的popupMenuStyle样式文件中和主题直接定义的属性中获取到如下属性:

        
        
        
        

DropDownListView的构造器中有如下方法:
final TypedArray a =  context.obtainStyledAttributes(   attrs, R.styleable.ListView, R.attr.dropDownListViewStyle, 0); 从主题中定义的dropDownListViewStyle样式文件中和主题直接定义的属性中获取到如下属性:

         
        
        
        
         
        
         
        
         
        
        
        
        
        
    


PopupWindow的构造器中有如下方法:
final TypedArray a = context.obtainStyledAttributes( null , R.styleable.PopupWindow, com.android.internal.R.attr.popupWindowStyle,0);即从主题中定义的popupWindowStyle样式文件中和主题直接定义的属性中获取到如下属性:

        
        
        
        
        
        
        
        
        
        
        
        



你可能感兴趣的:(Android源码,Android技术)