Android ActionBar的源代码分析(三)

上一篇已经对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的菜单动作有四种触发的方式,分别为:

  1. 通过对mClickListener来触发点击事件;
  2. 使用系统的菜单点击事件;
  3. 通过对mIntent赋值来触发跳转到相应的Activity;
  4. 通过使用ActionProvider;

通过对mClickListener来触发点击事件

我们先分析一下菜单的xml配置吧。我们知道类MenuInflater是负责解析menu的xml配置,通过MenuInflater.MenuState对菜单项属性进行赋值的, 其中MenuInflater.MenuState.readItem()负责解析xml的配置,MenuInflater.MenuState.setItem()负责对菜单项(MenuItemImpl)进行属性负责,下面看一下readItem()方法:
        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的xml中配置android:onClick属性即可,如下例:
<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中没有重写该方法,它是会一直执行后面的方法的。
由此,我们终于知道了重写Activity.onOptionItemSelected()方法的前因后果了,值得注意的是,在重写该方法后要return super.onOptionsItemSelected(item);避免事件被意外拦截而导致不可意料的问题出现。

通过对mIntent赋值来触发跳转到相应的Activity

通过对mIntent赋值来触发跳转到相应的Activity的前提有两个:
  1. 不能再xml中配置android:onClick属性的值;或者就算配置了,那配置的方法返回值必须为false;
  2. 确保Activity.onOptionsItemSelected()方法的返回值为false(默认情况返回值就是false);
举例如下:
首先重写Activity.onCreateOptionsMenu()方法:
	@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

使用ActionProvider的前提有三个:
  1. 不能再xml中配置android:onClick属性的值;或者就算配置了,那配置的方法返回值必须为false;
  2. 确保Activity.onOptionsItemSelected()方法的返回值为false(默认情况返回值就是false);
  3. 不能调用MenuItem.setIntent()方法对Menu设置Intent
举例如下:
首先写一个类继承类ActionProvider:
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的运行机制进行分析,敬请期待!

你可能感兴趣的:(android,Actionbar)