标签:
以下说明全部针对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() 等操作修改菜单项。
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的样式进行设置,一般设置如下
- <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
- <!--该项的设置是保证Activity视图顶部不会出现两个上下文操作栏,当前ActionMode将会覆盖在Toolbar上显示-->
- <item name="windowActionModeOverlay">true</item>
- <!--该项设置ActionMode背景、字体、返回按键样式等内容;可以参考一个Android提供的样式拷贝过来然后进行修改;好像直接继承的话,子类对父类的样式修改是无效的-->
- <!--这里也强烈建议对于自己不懂的样式我们可以借鉴别人的设置,稍加修改部分参数,这也是很鼓励的做法-->
- <item name="actionModeStyle">@style/actionModeStyle</item>
- </style>
- <style name="actionModeStyle" >
- <item name="background">@color/colorPrimary</item>
- <item name="backgroundSplit">?attr/actionModeSplitBackground</item>
- <item name="height">?attr/actionBarSize</item>
- <item name="titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Title</item>
- <item name="subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle</item>
- <item name="closeItemLayout">@layout/abc_action_mode_close_item_material</item>
- </style>
二、为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样式文件中和主题直接定义的属性中获取到如下属性:
<declare-styleable name="ActionMode">
<!-- title的样式. -->
<attr name="titleTextStyle" />
<!-- subtitle的样式. -->
<attr name="subtitleTextStyle" />
<!-- 背景颜色 -->
<attr name="background" />
<!-- 拆分操作模式栏背景. -->
<attr name="backgroundSplit" />
<!-- 操作栏高度. -->
<attr name="height" />
<!-- 在操作栏开始位置的close视图(返回按键)的布局. -->
<attr name="closeItemLayout" format="reference" />
</declare-styleable>
下面这一行是获取返回按键布局的非常关键的一行代码!!!也可以说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
showAtLocation@PopupWindow.class
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样式文件中和主题直接定义的属性中获取到如下属性:
<declare-styleable name="PopupWindow">
<!-- 弹出窗口的背景. -->
<attr name="popupBackground" format="reference|color" />
<!-- 弹出窗口的高度(影响阴影). -->
<attr name="popupElevation" format="dimension" />
<!-- 弹出窗口的动画样式 -->
<attr name="popupAnimationStyle" format="reference" />
<!-- 弹出窗口是否遮盖锚视图 -->
<attr name="overlapAnchor" format="boolean" />
<!-- Transition used to move views into the popup window. -->
<attr name="popupEnterTransition" format="reference" />
<!-- Transition used to move views out of the popup window. -->
<attr name="popupExitTransition" format="reference" />
</declare-styleable>
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文件中<item> <menu>标签中我们是无法设置背景和字体颜色的,通常情况是通过修改Theame属性来实现的,具体如下:
<style name="ProfileTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--修改PopupMenu的item背景颜色--><!--这里为何一个使用android:而另一个没有android: 是因为前者只有在android 5.0以后版本中才能被使用,后者是兼容模式任何版本都能使用(推荐使用后者)-->
<item name="android:popupMenuStyle">@style/popupMenuProfile</item>
<item name="popupMenuStyle">@style/popupMenuProfile</item>
<!--修改PopupMenu的分界线 注意添加这个会导致menuItem的点击动画发生变化-->
<item name="android:dropDownListViewStyle">@style/dropDownStyle</item>
<item name="dropDownListViewStyle">@style/dropDownStyle</item>
<!--修改PopupMenu的字体颜色-->
<item name="android:textAppearanceLargePopupMenu">@style/popupTextProfile</item>
<item name="textAppearanceLargePopupMenu">@style/popupTextProfile</item>
<!--此处的值也控制ActionBar背景-->
<item name="colorPrimary">@color/black</item>
<!--此处的值也控制ActionBar上面显示电量、信号那行视图的背景-->
<item name="colorPrimaryDark">@color/black</item>
<item name="colorAccent">@color/white</item>
</style>
<style name="popupMenuProfile">
<item name="android:popupBackground">@color/colorAlphaBlack</item>
</style>
<style name="dropDownStyle" parent="android:style/Widget.Holo.ListView.DropDown">
<!--定义这样的style必须定义android:listSelector,否则会使用系统自带的selector那就不知道出什么幺蛾子-->
<item name="android:listSelector">@drawable/profile_popupmenu_selector</item>
<item name="android:divider">#80FFFFFF</item>
<item name="android:dividerHeight">0.5dp</item>
</style>
<style name="popupTextProfile" parent="@style/TextAppearance.Widget.AppCompat.ExpandedMenu.Item">
<item name="android:textColor">@android:color/white</item>
</style>
注意:上面的ProfileTheme在manifest.xml文件中可以加给某个Activity如:
<activity
android:name=".activity.others.ProfileActivity"
android:theme="@style/ProfileTheme">
</activity>
PopupMenu底层分析(绘制流程探究)
显示流程
android.support.v7.widget.PopupMenu中有一个域——private MenuPopupHelper mPopup;大部分操作都是委托它去执行的。
android.support.v7.view.menu.MenuPopupHelper,中有一个tryShow()方法,该方法负责具体绘制,完成工作有:
- 该部分完成的工作有对监听器初始化设置
- 计算弹出菜单宽度高度——正方形
- 调用ListPopupWindow域的show()方法
android.support.v7.widget.ListPopupWindow的show方法,完成工作有:
- 调用int height = buildDropDown()方法——创建android.support.v7.widget.ListPopupWindow.DropDownListView mDropDownList对象并设置Adapter。
- android.support.v7.widget.ListPopupWindow.DropDownListView extends ListViewCompat负责具体视图的显示。(ListViewCompat extends ListView,那么具体的itemView自然是交给对应的Adapter来提供,对于Adapter查看后面的内容)
- 方法内部调用android.widget.PopupWindow.setContentView(dropDownView);方法将当前View交给PopupWindow显示到手机上。
- 设置屏幕外触摸监听器
- 调用android.widget.PopupWindow.showAsDropDown(anchor, xoff, yoff)方法。该方法内部完成工作有:
- final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
- preparePopup(p);
- 布局信息初始化
- final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
- updateAboveAnchor(aboveAnchor);
- 计算当前弹出菜单应该显示在当前View的上面还是下面,对背景色进行设置
- invokePopup(WindowManager.LayoutParams p)
- 进行视图具体的显示,使用了mWindowManager.addView(PopupDecorView decorView , ViewGroup.LayoutParams params);等方法。PopupDecorView decorView对象的构造利用了传入的android.support.v7.widget.ListPopupWindow.DropDownListView对象。最后这样就显示了。
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样式文件中和主题直接定义的属性中获取到如下属性:
<declare-styleable name="ListPopupWindow">
<!-- 下拉垂直偏移距离. -->
<attr name="dropDownVerticalOffset" format="dimension" />
<!-- 下拉水平偏移距离. -->
<attr name="dropDownHorizontalOffset" format="dimension" />
</declare-styleable>
DropDownListView的构造器中有如下方法:
final TypedArray a =
context.obtainStyledAttributes(
attrs, R.styleable.ListView,
R.attr.dropDownListViewStyle, 0);即从主题中定义的dropDownListViewStyle样式文件中和主题直接定义的属性中获取到如下属性:
<declare-styleable name="ListView">
<!-- 为当前ListView指定静态数组资源,而不需要编写Adapter -->
<attr name="entries" />
<!-- listview中item之前的颜色或者Drawable. -->
<attr name="divider" format="reference|color" />
<!-- listview中item之前的相隔距离 . -->
<attr name="dividerHeight" format="dimension" />
<!-- 值为真则lsitview中的Header间不绘制divider,默认值为真 -->
<attr name="headerDividersEnabled" format="boolean" />
<!-- 值为真则lsitview中的Footer间不绘制divider,默认值为真 -->
<attr name="footerDividersEnabled" format="boolean" />
<!-- Drawable to draw above list content. -->
<attr name="overScrollHeader" format="reference|color" />
<!-- Drawable to draw below list content. -->
<attr name="overScrollFooter" format="reference|color" />
</declare-styleable>
PopupWindow的构造器中有如下方法:
final TypedArray a = context.obtainStyledAttributes( null, R.styleable.PopupWindow, com.android.internal.R.attr.popupWindowStyle,0);即从主题中定义的popupWindowStyle样式文件中和主题直接定义的属性中获取到如下属性:
<declare-styleable name="PopupWindow">
<!-- 弹出窗口的背景. -->
<attr name="popupBackground" format="reference|color" />
<!-- 弹出窗口的高度(影响阴影). -->
<attr name="popupElevation" format="dimension" />
<!-- 弹出窗口的动画样式 -->
<attr name="popupAnimationStyle" format="reference" />
<!-- 弹出窗口是否遮盖锚视图 -->
<attr name="overlapAnchor" format="boolean" />
<!-- Transition used to move views into the popup window. -->
<attr name="popupEnterTransition" format="reference" />
<!-- Transition used to move views out of the popup window. -->
<attr name="popupExitTransition" format="reference" />
</declare-styleable>
Android之三种Menu的使用与分析
标签:
原文地址:http://blog.csdn.net/evan_man/article/details/51685022