请尊重他人劳动成果,请勿随意剽窃,转载请注明,谢谢!
转载请注明出处: http://blog.csdn.net/evan_man/article/details/51685022
以下说明全部针对Android3.0(Api-11)。
本指南将介绍三种基本菜单分别是PartA:操作栏(选项菜单OptionMenu)、PartB:上下文操作模式(ActionMode)、PartC:弹出菜单(PopupMenu)。
PartA:操作栏(选项菜单)——onCreateOptionsMenu()创建的
以屏幕操作项和溢出选项的组合形式呈现选项菜单中的各项。
一、创建
为 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 的一种系统实现,它将用户交互的重点转到执行上下文操作上。
一、为单个视图创建上下文操作模式
- 实现 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的样式进行设置,一般设置如下
-
-
- true
-
-
-
- @style/actionModeStyle
-
-
- @color/colorPrimary
-
- ?attr/actionModeSplitBackground
-
- ?attr/actionBarSize
-
- @style/TextAppearance.AppCompat.Widget.ActionMode.Title
-
- @style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle
-
- @layout/abc_action_mode_close_item_material
-
二、为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:显示图标
补充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文件中-
注意:上面的ProfileTheme在manifest.xml文件中可以加给某个Activity如:
PopupMenu底层分析(绘制流程探究)
显示流程
android.support.v7.widget.
PopupMenu
中有一个域——private MenuPopupHelper mPopup;大部分操作都是委托它去执行的。
android.support.v7.view.menu.
MenuPopupHelper,中有一个tryShow()方法,该方法负责具体绘制,完成工作有:
- ListPopupWindow mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); //创建一个ListPopupWindow对象,该对象继承自Object
- 对mPopup进行一些初始化设置,如
- mPopup.setOnDismissListener(this);
- mPopup.setOnItemClickListener(this);
- mPopup.setAdapter(mAdapter);
- mPopup.show(); //调用该方法显示视图
- 总结:MenuPopupHelper管理了一个ListPopupWindow对象
android.support.v7.widget.
ListPopupWindow的show方法,完成工作有:
- 调用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查看后面的内容
- 对mPopup进行一些初始化设置,如
- mPopup.setWidth(widthSpec);
- mPopup.setHeight(heightSpec);
- mPopup.setOutsideTouchable(...)
- mPopup.setTouchInterceptor(mTouchInterceptor);
- 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样式文件中和主题直接定义的属性中获取到如下属性: