上一篇已经对ActionBar菜单构造过程进行比较详细的分析,没有看过的朋友可以移步android中ActionBar的源代码分析(二)。本章接着对ActionBar菜单的执行过程进行分析。
在介绍ActionBar菜单的执行过程之前,首先我们需要了解android的消息处理机制,我们知道activity的activity等组件和view控件都是运行在主线程上的,这个主线程我们也称为UI线程,UI线程的管理类为ActivityThread,它由Zygote进程孵化应用程序进程过程中创建起来的,在ActivityThread创建过程中,会调用Looper.prepareMainLooper()创建一个主looper来维护整个应用程序UI方面的消息队列,并由ViewRootImpl.ViewRootHandler负责消息的分发处理;比如按钮或者菜单的点击事件,是由ViewRootHandler对该点击消息进行分发,执行点击消息的callback事件即View.performClick()进行处理,在View.performClick()方法中如果发现有注册了View.OnClickListener回调接口的对象,就调用该接口的onClick方法执行回调。
ActionBar菜单执行过程分析
在ActionBar中,菜单项控件的实现类为ActionMenuItemView,ActionMenuItemView继承于TextView,也就是说一个菜单项就是一个TextView。在ActionMenuItemView的构造函数中就注册了View.OnClickListener回调接口:
public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
......
setOnClickListener(this);
setOnLongClickListener(this);
setTransformationMethod(new AllCapsTransformationMethod(context));
mSavedPaddingLeft = -1;
}
可以看到,在调用setOnClickListener()方法时,
ActionMenuItemView把本身作为参数传了进去,也即是ActionMenuItemView是实现了View.OnClickListener接口的,查看一下ActionMenuItemView.onClick(View v)方法如下:
@Override
public void onClick(View v) {
if (mItemInvoker != null) {
mItemInvoker.invokeItem(mItemData);
}
}
这里调用判断mItemInvoker是否为空,如果不为空则执行mItemInvoker.invokeItem方法,既然菜单能够进行点击,那mItemInvoker肯定不为空啊,那这个mItemInvoker是个什么东东呢?从上一章的ActionBar菜单的构造过程我们知道,
ActionMenuItemView是由类ActionMenuPresenter创建并绑定MenuItemImpl的,我们看一下ActionMenuPresenter.bindItemView()方法的实现吧:
@Override
public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
itemView.initialize(item, 0);
final ActionMenuView menuView = (ActionMenuView) mMenuView;
ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
actionItemView.setItemInvoker(menuView);
}
可以清晰的看到,在这个方法中首先调用了ActionMenuItemView.initialize()进行初始化操作,值得注意的是,这里把MenuItemImpl作为参数传入,也就是说在ActionMenuItemView中存在对MenuItemImpl的引用,在后面的点击事件中会用到这个MenuItemImpl,这里先不深入说明;然后调用了ActionMenuItemView.setItemInvoker()方法,并把menuView传入进去,这个menuView实现类为ActionMenuView,也就是说,在ActionMenuItemView.onClick()方法中调用的
mItemInvoker.invokeItem,实际就是调用ActionMenuView.invokeItem()方法,下面我们看一下ActionMenuView.invokeItem()的实现逻辑:
public boolean invokeItem(MenuItemImpl item) {
return mMenu.performItemAction(item, 0);
}
这个方法很简单,就是调用了mMenu.performItemAction(),这个mMenu就是MenuBuilder,下面是MenuBuilder.performItemAction()方法的实现逻辑:
public boolean performItemAction(MenuItem item, int flags) {
MenuItemImpl itemImpl = (MenuItemImpl) item;
if (itemImpl == null || !itemImpl.isEnabled()) {
return false;
}
boolean invoked = itemImpl.invoke();
......
return invoked;
}
这个方法就调用了itemImpl.invoke()方法进行处理,这里的itemImpl就是上面ActionMenuPresenter.bindItemView()方法中调用ActionMenuItemView.initialize()方法中传入的MenuItemImpl对象,下面是MenuItemImpl.invoke()方法的实现逻辑:
public boolean invoke() {
if (mClickListener != null &&
mClickListener.onMenuItemClick(this)) {
return true;
}
if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) {
return true;
}
if (mItemCallback != null) {
mItemCallback.run();
return true;
}
if (mIntent != null) {
try {
mMenu.getContext().startActivity(mIntent);
return true;
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Can't find activity to handle intent; ignoring", e);
}
}
if (mActionProvider != null && mActionProvider.onPerformDefaultAction()) {
return true;
}
return false;
}
在这个方法中,首先判断mClickListener是否为空,如果不为空,则调用mClickListener.onMenuItemClick()方法,并且判断该方法返回值是否为true,如果为true,则退出并完成整个点击处理过程;否则就会调用MenuBuilder.dispatchMenuItemSelected()方法,当MenuBuilder.dispatchMenuItemSelected()方法返回值为true,则退出并完成整个点击处理过程;否则就判断mItemCallback是否为空,不为空就调用mItemCallback.run()方法,这个mItemCallback是通过MenuItemImpl.setCallback()传入进来的回调对象,而由于MenuItem并没有setCallback()这个方法,因此外部是无法调用的,可能考虑是android内部使用的方法,我们暂且不考虑;如果mItemCallback为空,则判断mIntent是否为空,不为空则根据Intent跳转到对应的Activity上;否则判断ActionProvider是否为空,不为空则调用ActionProvider.onPerformDefaultAction()方法;
根据invoke()方法的调用逻辑,我们对ActionBar的菜单动作有四种触发的方式,分别为:
- 通过对mClickListener来触发点击事件;
- 使用系统的菜单点击事件;
- 通过对mIntent赋值来触发跳转到相应的Activity;
- 通过使用ActionProvider;
通过对mClickListener来触发点击事件
public void readItem(AttributeSet attrs) {
TypedArray a = mContext.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.MenuItem);
// Inherit attributes from the group as default value
itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
itemTitle = a.getText(com.android.internal.R.styleable.MenuItem_title);
itemTitleCondensed = a.getText(com.android.internal.R.styleable.MenuItem_titleCondensed);
itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
itemAlphabeticShortcut =
getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
itemNumericShortcut =
getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
// Item has attribute checkable, use it
itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
} else {
// Item does not have attribute, use the group's (group can have one more state
// for checkable that represents the exclusive checkable)
itemCheckable = groupCheckable;
}
itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
itemShowAsAction = a.getInt(com.android.internal.R.styleable.MenuItem_showAsAction, -1);
itemListenerMethodName = a.getString(com.android.internal.R.styleable.MenuItem_onClick);
itemActionViewLayout = a.getResourceId(com.android.internal.R.styleable.MenuItem_actionLayout, 0);
itemActionViewClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionViewClass);
itemActionProviderClassName = a.getString(com.android.internal.R.styleable.MenuItem_actionProviderClass);
final boolean hasActionProvider = itemActionProviderClassName != null;
if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
itemActionProvider = newInstance(itemActionProviderClassName,
ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
mActionProviderConstructorArguments);
} else {
if (hasActionProvider) {
Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
+ " Action view already specified.");
}
itemActionProvider = null;
}
a.recycle();
itemAdded = false;
}
从这段代码中,可以看出ActionBar的菜单支持配置的属性包括:
- android:id 菜单的项ID,也是菜单项的唯一标识,不能重复;
- android:menuCategory 同种菜单项的种类。该属性可取4个值:container、system、secondary和alternative。通过menuCategroy属性可以控制菜单项的位置。其排列顺序受类MenuBuilder的数组变量sCategoryToOrder的控制,其排列顺序从小到大为alternative、secondary、container、system;例如将属性设为system,表示该菜单项是系统菜单,应放在其他种类菜单项的后面;
- android:orderInCategory 同种类菜单的排列顺序。该属性需要设置一个整数值。例如menuCategory属性值都为system的3个菜单项(item1、item2和item3)。将这3个菜单项的orderInCategory属性值设为3、2、1,那么item3会显示在最前面,而item1会显示在最后面
- android:title 菜单项标题(菜单项显示的文本)
- android:titleCondensed 菜单项的短标题。当菜单项标题太长时会显示该属性值
- android:icon 菜单项图标资源ID
- android:alphabeticShortcut 菜单项的字母快捷键
- android:numericShortcut 菜单项的数字快捷键
- android:checkable 表示菜单项是否带复选框。该属性可设计为true或false
- android:checked 如果菜单项带复选框(checkable属性为true),该属性表示复选框默认状态是否被选中。可设置的值为true或false
- android:visible 菜单项默认状态是否可见
- android:enabled 菜单项默认状态是否被激活
- android:onClick 菜单项点击时触发的事件方法
- android:showAsAction 菜单项显示的方式,可设置值包括:never(在ActionBar上不显示,而是显示在Overflow菜单中)、ifRoom(当ActionBar有足够的空间时则显示在ActionBar上,否则显示在Overflow菜单中)、always(总是显示在ActionBar上)、withText(在ActionBar上显示文字)、collapseActionView(声明了这个操作视窗应该被折叠到一个按钮中,当用户选择这个按钮时,这个操作视窗展开)
- android:actionLayout 通过布局文件设置菜单项ActionView,不能和android:actionViewClass、android:actionProviderClass共同使用,否则会覆盖android:actionViewClass和android:actionProviderClass的设置;
- android:actionViewClass 通过代码类设置菜单项的ActionView,不能和android:actionProviderClass共同使用,否则会覆盖android:actionProviderClass的设置
- android:actionProviderClass 通过代码类设置菜单项的ActionProvider;
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_groupchat"
android:icon="@drawable/ofm_group_chat_icon"
android:showAsAction="ifRoom|withText"
android:title="@string/action_groupchat"
android:onClick="groupchatClick"/>
</menu>
然后在Activity中写方法如下:
public boolean groupchatClick(MenuItem item){
System.out.println("click group chat item");
return true;
}
注意这里的返回值,如果设置为true,则表示拦截菜单项的点击事件,此时不会再执行Activity.onOptionsItemSelected()方法;如果设置为false,则会继续执行Activity.onOptionsItemSelected()方法。
使用系统的菜单点击事件
从MenuItemImpl.invoke()方法我们可知,如果mClickListener为空,则会执行MenuBuilder.dispatchMenuItemSelected()方法如下: boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
return mCallback != null && mCallback.onMenuItemSelected(menu, item);
}
代码很简单,判断mCallback是否为空,如果不为空则执行mCallback.onMenuItemSelected()方法,这个mCallback是什么呢?从上一章的分析可知,类PhoneWindow在initializePanelMenu方法中创建MenuBuilder时,调用了MenuBuilder.setCallback()方法,把自己作为参数传入,因此这个mCallback就是PhoneWindow,我们看一下PhoneWindow.onMenuItemSelected()方法:
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
final PanelFeatureState panel = findMenuPanel(menu.getRootMenu());
if (panel != null) {
return cb.onMenuItemSelected(panel.featureId, item);
}
}
return false;
}
在这个方法中,获取PhoneWindow的回调对象,判断不为空则调用回调方法onMenuItemSelected(),从上一章分析可知,这个getCallback()返回值为Activity,下面是Activity.onMenuItemSelected()方法的代码:
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch (featureId) {
case Window.FEATURE_OPTIONS_PANEL:
// Put event logging here so it gets called even if subclass
// doesn't call through to superclass's implmeentation of each
// of these methods below
EventLog.writeEvent(50000, 0, item.getTitleCondensed());
if (onOptionsItemSelected(item)) {
return true;
}
if (mFragments.dispatchOptionsItemSelected(item)) {
return true;
}
if (item.getItemId() == android.R.id.home && mActionBar != null &&
(mActionBar.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) {
if (mParent == null) {
return onNavigateUp();
} else {
return mParent.onNavigateUpFromChild(this);
}
}
return false;
case Window.FEATURE_CONTEXT_MENU:
EventLog.writeEvent(50000, 1, item.getTitleCondensed());
if (onContextItemSelected(item)) {
return true;
}
return mFragments.dispatchContextItemSelected(item);
default:
return false;
}
}
在这个方法中,判断传入的featureId的值,从前面分析可知,这个featureId就是Window.FEATURE_OPTIONS_PANEL,因此就会执行onOptionsItemSelected()方法;如果onOptionsItemSelected()返回值为true,则退出处理,否则执行fragment的菜单分发事件;代码最后是判断item是否home键,并且ActionBar的选项设置是否为显示Home的返回键,如果是则调用onNavigateUp()方法返回到上一个Activity中。我们重点看一下Activity.onOptionsItemSelected()方法如下:
public boolean onOptionsItemSelected(MenuItem item) {
if (mParent != null) {
return mParent.onOptionsItemSelected(item);
}
return false;
}
可以看到,Activity.onOptionItemSelected()方法默认返回值为false,也就是说如果我们在Activity中没有重写该方法,它是会一直执行后面的方法的。
通过对mIntent赋值来触发跳转到相应的Activity
通过对mIntent赋值来触发跳转到相应的Activity的前提有两个:- 不能再xml中配置android:onClick属性的值;或者就算配置了,那配置的方法返回值必须为false;
- 确保Activity.onOptionsItemSelected()方法的返回值为false(默认情况返回值就是false);
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.normal, menu);
// 找到对应的菜单项,并设置Intent
MenuItem item = menu.findItem(R.id.action_groupchat);
item.setIntent(new Intent(this, TestActivity.class));
return true;
}
在这个方法中找到对应的菜单项,并调用setIntent()方法传入Intent即可;
通过使用ActionProvider
- 不能再xml中配置android:onClick属性的值;或者就算配置了,那配置的方法返回值必须为false;
- 确保Activity.onOptionsItemSelected()方法的返回值为false(默认情况返回值就是false);
- 不能调用MenuItem.setIntent()方法对Menu设置Intent
public class MyActionProvider extends ActionProvider {
public MyActionProvider(Context context) {
super(context);
}
@Override
public View onCreateActionView() {
return null;
}
@Override
public boolean onPerformDefaultAction() {
System.out.println("execute Action provider method");
return super.onPerformDefaultAction();
}
}
然后在menu的xml中配置android:actionProviderClass属性
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_addfriend"
android:icon="@drawable/ofm_add_icon"
android:showAsAction="ifRoom"
android:title="@string/action_addfriend"
android:numericShortcut="2"
android:actionProviderClass="com.example.actionbardemo.MyActionProvider"/>
</menu>
或者通过重写Activity.onCreateOptionsMenu()方法也能达到同样的效果:
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.normal, menu);
// 找到对应的菜单项,并设置ActionProvider
MenuItem item = menu.findItem(R.id.action_groupchat);
item.setActionProvider(new ActionProvider(this) {
@Override
public View onCreateActionView() {
return null;
}
@Override
public boolean onPerformDefaultAction() {
System.out.println("execute Action provider method");
return super.onPerformDefaultAction();
}
});
return true;
}
关于ActionBar的菜单执行过程分析到此为止,下一篇就ActionBar的OverflowMenu的运行机制进行分析,敬请期待!