Android ActionBar的源代码分析(二)

前面已经对ActionBar的初始化过程以及ActionBarView的布局进行比较详细的分析,没有看过的朋友可以移步android中ActionBar的源代码分析(一)。本章接着对ActionBar菜单的构造过程进行分析。

关于actionbar的菜单机制,网上已经有很多大牛写过类似的文章,个人认为写的不错的,有兴趣的朋友可以看一下这个:ANDROID的ActionBar及菜单机制

不过这篇文章从ActionBar所涉及的UML类图出发,概括了ActionBar的运行机制,可能有朋友看得云里雾里,下面我就详细地对相关点进行分析。

ActionBar的菜单构造过程还是很复杂的,中间涉及的类非常多,为了把这个过程说明白,我先把涉及到的类图画出来吧:

Android ActionBar的源代码分析(二)_第1张图片

是不是被这张图吓了一跳,个人感觉这样的设计过于复杂,类与类之间的耦合度很高。概括来说,就是MenuInflater负责解析xml获取菜单信息,MenuBuilder负责维护菜单信息列表,ActionMenuPresenter负责根据菜单信息构建菜单控件ActionMenuItemView(其实就是一个TextView),并给ActionBarView返回一个菜单容器ActionMenuView(其实就是一个LinearLayout),ActionBarView负责把ActionMenuView显示出来,详细代码跟踪如下:

首先是从Activity.onCreate()开始吧,在Activity.onCreate中我们会经常看到调用这么一个方法:setContentView(),那么setContentView()里面到底干了啥东东呢?我们打开代码瞅瞅

    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initActionBar();
    }
很简单,就调用了window.setContentView()方法,并且进行初始化ActionBar操作,initActionBar()的调用过程在 android中ActionBar的源代码分析(一)已经做过介绍,这里不再赘述。我们看看window.setContentView()做了什么东西吧,我们知道Window是一个抽象类,具体的实现类是PhoneWindow,PhoneWindow.setContentView()有三个同名方法,基本代码都差不多,我们拿其中一个进行分析:

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
首先判断ContentView是否为空,如果为空则进行初始化最顶层View的操作,否则移除ContentView下所有的子控件;然后对布局执行inflater操作,最后通知Activity说ContentView的子控件发生了改变;

这里的installDecor()方法在上一章节已经做过简单介绍,我们再来回顾一下:

    private void installDecor() {
        if (mDecor == null) {
	    // 进行decorView的实例化操作
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
	    // 获取DecorView的布局
            mContentParent = generateLayout(mDecor);
			......
            
 
                    mDecor.post(new Runnable() {
                        public void run() {
                            PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                            if (!isDestroyed() && (st == null || st.menu == null)) {
				// 构建菜单面板
                                invalidatePanelMenu(FEATURE_ACTION_BAR);
                            }
                        }
                    });
                }
            }
        }
    }

首先是进行DecorView的实例化操作,从上一章中我们知道,DecorView是Activity的顶层视图。接着获取DecorView的布局文件进行解析,并添加DecorView的子控件;最后是执行构建菜单面板操作,我们看一下invalidatePanelMenu()方法的实现逻辑:

    @Override
    public void invalidatePanelMenu(int featureId) {
        mInvalidatePanelMenuFeatures |= 1 << featureId;

        if (!mInvalidatePanelMenuPosted && mDecor != null) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            mInvalidatePanelMenuPosted = true;
        }
    }

首先判断传入参数featureId是否为空,不为空则调用mInvalidatePanelMenuRunnable的run()方法如下:

    private final Runnable mInvalidatePanelMenuRunnable = new Runnable() {
        @Override public void run() {
            for (int i = 0; i <= FEATURE_MAX; i++) {
                if ((mInvalidatePanelMenuFeatures & 1 << i) != 0) {
                    doInvalidatePanelMenu(i);
                }
            }
            mInvalidatePanelMenuPosted = false;
            mInvalidatePanelMenuFeatures = 0;
        }
    };

这个方法就是调用doInvalidatePanelMenu()方法来构建菜单面板的,接着跟踪:

    void doInvalidatePanelMenu(int featureId) {
        PanelFeatureState st = getPanelState(featureId, true);
        Bundle savedActionViewStates = null;
        if (st.menu != null) {
            savedActionViewStates = new Bundle();
            st.menu.saveActionViewStates(savedActionViewStates);
            if (savedActionViewStates.size() > 0) {
                st.frozenActionViewState = savedActionViewStates;
            }
            // This will be started again when the panel is prepared.
            st.menu.stopDispatchingItemsChanged();
            st.menu.clear();
        }
        st.refreshMenuContent = true;
        st.refreshDecorView = true;
        
        // Prepare the options panel if we have an action bar
        if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL)
                && mActionBar != null) {
            st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
            if (st != null) {
                st.isPrepared = false;
                preparePanel(st, null);
            }
        }
    }
在doInvalidatePanelMenu()这个方法中,首先是根据featureId查找面板的状态,然后判断st.menu是否为空,初始状态下st.menu就是为空的,并且传入的参数featureId是等于FEATURE_ACTION_BAR的,因此就会执行preparePane()方法:

    public final boolean preparePanel(PanelFeatureState st, KeyEvent event) {
		......
		// 这里的callback就是Activity
        final Callback cb = getCallback();

        if (cb != null) {
            st.createdPanelView = cb.onCreatePanelView(st.featureId);
        }

        if (st.createdPanelView == null) {
            if (st.menu == null || st.refreshMenuContent) {
                if (st.menu == null) {
                    if (!initializePanelMenu(st) || (st.menu == null)) {
                        return false;
                    }
                }

                if (mActionBar != null) {
                    if (mActionMenuPresenterCallback == null) {
                        mActionMenuPresenterCallback = new ActionMenuPresenterCallback();
                    }
                    mActionBar.setMenu(st.menu, mActionMenuPresenterCallback);
                }

                st.menu.stopDispatchingItemsChanged();
                if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) {
                    st.setMenu(null);

                    if (mActionBar != null) {
                        mActionBar.setMenu(null, mActionMenuPresenterCallback);
                    }

                    return false;
                }
                
                st.refreshMenuContent = false;
            }

			......

            if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) {
                if (mActionBar != null) {
                    mActionBar.setMenu(null, mActionMenuPresenterCallback);
                }
                st.menu.startDispatchingItemsChanged();
                return false;
            }

        ......

        return true;
    }

preparePanel()方法的代码比较多,我们就挑重点的看,首先是通过getCallback()获取PhoneWindow的回调对象,这个回调对象就是Activity,怎么知道呢?有兴趣的朋友可以看看类Activity的attach()方法的代码,在这个方法中,首先是实例化Window类,然后接着就通过window.setCallback(this)方法,把自身传入到对象Window中了(注:类Activity是实现接口Window.Callback的)。

言归正传,在preparePanel()方法中获取回调对象Activity后,接着就是回调了Activity.onCreatePanelView()方法创建面板视图,Activity.onCreatePanelView()方法默认情况下是返回null的,因此接着就会调用initializePanelMenu()方法初始化菜单面板,下面是initializePanelMenu()方法的代码:

    protected boolean initializePanelMenu(final PanelFeatureState st) {
	......

        final MenuBuilder menu = new MenuBuilder(context);

        menu.setCallback(this);
        st.setMenu(menu);

        return true;
    }
在这个方法中,是实例化MenuBuilder类,并把menu赋值给传入参数st.menu了,并且返回值为true;回到方法preparePanel()中,继续往下跟踪代码,由于mActionBar已经通过installDecor()方法进行了赋值,因此mActionBar是不为null的,系统就会执行mActionBar.setMenu(st.menu, mActionMenuPresenterCallback)方法,忘了说了,这个mActionBar是ActionBarView类的对象,且st.menu是MenuBuilder的实例对象;因此我们看一下ActionBarView.setMenu()方法:

    public void setMenu(Menu menu, MenuPresenter.Callback cb) {
		......

        MenuBuilder builder = (MenuBuilder) menu;
        mOptionsMenu = builder;
        if (mMenuView != null) {
            final ViewGroup oldParent = (ViewGroup) mMenuView.getParent();
            if (oldParent != null) {
                oldParent.removeView(mMenuView);
            }
        }
        if (mActionMenuPresenter == null) {
            mActionMenuPresenter = new ActionMenuPresenter(mContext);
            mActionMenuPresenter.setCallback(cb);
            mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
            mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
        }

        ActionMenuView menuView;
        final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        if (!mSplitActionBar) {
            mActionMenuPresenter.setExpandedActionViewsExclusive(
                    getResources().getBoolean(
                    com.android.internal.R.bool.action_bar_expanded_action_views_exclusive));
            configPresenters(builder);
            menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this);
            final ViewGroup oldParent = (ViewGroup) menuView.getParent();
            if (oldParent != null && oldParent != this) {
                oldParent.removeView(menuView);
            }
            addView(menuView, layoutParams);
        } else {
            .......
        }
        mMenuView = menuView;
    }
	
    private void configPresenters(MenuBuilder builder) {
        if (builder != null) {
            builder.addMenuPresenter(mActionMenuPresenter);
            builder.addMenuPresenter(mExpandedMenuPresenter);
        } else {
            mActionMenuPresenter.initForMenu(mContext, null);
            mExpandedMenuPresenter.initForMenu(mContext, null);
            mActionMenuPresenter.updateMenuView(true);
            mExpandedMenuPresenter.updateMenuView(true);
        }
    }


 在setMenu()方法中,实例化了ActionMenuPresenter类,这是一个负责展现menu的类,接着调用configPresenters()方法配置menu的展现类,然后调用ActionMenuPresenter.getMenuView()获取到菜单容器ActionMenuView,并把它添加到ActionBarView中展现出来。这里有很多细节都没有介绍,比如ActionMenuPresenter是怎么展现菜单项的,又是如何管理菜单容器ActionMenuView的生命周期以及怎么往菜单容器添加菜单View的呢?那么我们下面逐一进行介绍。 
  

首先ActionMenuPresenter是怎么展现菜单项的呢?其实我们在进行配置menu的xml时,有一个属性叫showAsAction,当我们配置它的值为ifRoom时,表示当ActionBar宽度允许的情况下,就会显示到ActionBar中,否则就会放到OverflowMenu中(如果策略允许的话),这就需要计算ActionBar允许放置menu的最大宽度了,这个计算的过程是在ActionMenuPresenter.initForMenu()方法中执行的,这个方法是在哪个时机开始调用的呢?回到configPresenters()方法,这个方法首先判断builder是否为空,由于builder在上面已经做过实例化,因此是不为空的,这样就会调用MenuBuilder.addMenuPresenter()方法

    public void addMenuPresenter(MenuPresenter presenter) {
        mPresenters.add(new WeakReference(presenter));
        presenter.initForMenu(mContext, this);
        mIsActionItemsStale = true;
    }

嗯,终于看到initForMenu方法了,这个方法首先根据标题栏策略类ActionBarPolicy的配置来决定是否显示OverflowButton,然后计算显示菜单的最大宽度,详细代码就不贴出来,有兴趣的朋友可以看看

然后就是构造ActionMenuView的过程了,这个可以看看ActionMenuPresenter.getMenuView()的实现过程了:

    @Override
    public MenuView getMenuView(ViewGroup root) {
        MenuView result = super.getMenuView(root);
        ((ActionMenuView) result).setPresenter(this);
        return result;
    }

代码很简单,可以看出主要的实现逻辑应该在super.getMenuView()上,ActionMenuPresenter的父类是BaseMenuPresenter,我们就看一下BaseMenuPresenter.getMenuView()的实现过程吧

    public MenuView getMenuView(ViewGroup root) {
        if (mMenuView == null) {
            mMenuView = (MenuView) mSystemInflater.inflate(mMenuLayoutRes, root, false);
            mMenuView.initialize(mMenu);
            updateMenuView(true);
        }

        return mMenuView;
    }
这个方法通过inflate的方法创建了ActionMenuView,然后调用initialize()方法初始化这个View,最后调用updateMenuView()方法往ActionMenuView添加菜单控件,由于此时菜单项还没从xml上解析并实例化,也就是说MenuBuilder里的菜单信息还是空的,因此这个方法暂时没有任何效果;

那么MenuBuilder是什么时候才有菜单信息的呢?最后又是如果根据菜单信息创建菜单View并放到ActionMenuView上的呢?回到PhoneWindow.preparePanel()方法中,我们在调用ActionBarView.setMenu()后,系统接着就调用了cb.onCreatePanelMenu(),也就是Activity.onCreatePanelMenu()方法

    public boolean onCreatePanelMenu(int featureId, Menu menu) {
        if (featureId == Window.FEATURE_OPTIONS_PANEL) {
            boolean show = onCreateOptionsMenu(menu);
            show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater());
            return show;
        }
        return false;
    }

好了,有一个方法我想大家应该是非常熟悉的了:onCreateOptionMenu(),没错,这个就是我们为了显示菜单,必须要在Activity中override的方法,一般的写法如下:

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

这就是采用inflate的方式创建菜单的,在MenuInflater.inflate()方法中,主要是调用方法parseMenu()去解析xml并创建菜单的,代码如下:

    private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
            throws XmlPullParserException, IOException {
        MenuState menuState = new MenuState(menu);
		......
        boolean reachedEndOfMenu = false;
        while (!reachedEndOfMenu) {
            switch (eventType) {
                case XmlPullParser.START_TAG:
					......
                    
                case XmlPullParser.END_TAG:
                    tagName = parser.getName();
                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
                        lookingForEndOfUnknownTag = false;
                        unknownTagName = null;
                    } else if (tagName.equals(XML_GROUP)) {
                        menuState.resetGroup();
                    } else if (tagName.equals(XML_ITEM)) {
                        if (!menuState.hasAddedItem()) {
                            if (menuState.itemActionProvider != null &&
                                    menuState.itemActionProvider.hasSubMenu()) {
                                menuState.addSubMenuItem();
                            } else {
                                menuState.addItem();
                            }
                        }
                    } else if (tagName.equals(XML_MENU)) {
                        reachedEndOfMenu = true;
                    }
                    break;
                    
                case XmlPullParser.END_DOCUMENT:
                    throw new RuntimeException("Unexpected end of document");
            }
            
            eventType = parser.next();
        }
    }

在parseMenu方法中,采用Pull的方式解析xml,并调用menuState.readItem()和menuState.addItem()方法读取xml中菜单的配置属性,并把菜单添加到MenuBuilder中,这里就调用到了MenuBuilder.add方法,而MenuBuilder.add方法又是调用addInternal()方法来执行具体操作的,我们看一下addInternal()方法的实现逻辑吧:

    private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
        final int ordering = getOrdering(categoryOrder);

        final MenuItemImpl item = createNewMenuItem(group, id, categoryOrder, ordering, title,
                mDefaultShowAsAction);

        if (mCurrentMenuInfo != null) {
            // Pass along the current menu info
            item.setMenuInfo(mCurrentMenuInfo);
        }

        mItems.add(findInsertIndex(mItems, ordering), item);
        onItemsChanged(true);

        return item;
    }

在这个方法中,首先是创建了菜单项信息类MenuItemImpl,然后把新创建的菜单信息类添加到列表中,最后调用onItemChanged()方法通知菜单项发生改变的消息,onItemsChanged()方法又是调用dispatchPresenterUpdate()方法执行具体操作的:

    private void dispatchPresenterUpdate(boolean cleared) {
        if (mPresenters.isEmpty()) return;

        stopDispatchingItemsChanged();
        for (WeakReference ref : mPresenters) {
            final MenuPresenter presenter = ref.get();
            if (presenter == null) {
                mPresenters.remove(ref);
            } else {
                presenter.updateMenuView(cleared);
            }
        }
        startDispatchingItemsChanged();
    }
在dispatchPresenterUpdate()方法中,这里的presenter其实就是ActionMenuPresenter,只不过这里使用了WeakReference进行了引用,也就是说调用了ActionMenuPresenter.updateMenuView()方法执行了更新菜单控件的操作:

    public void updateMenuView(boolean cleared) {
        final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent();
        if (menuViewParent != null) {
            ActionBarTransition.beginDelayedTransition(menuViewParent);
        }
        super.updateMenuView(cleared);

        ((View) mMenuView).requestLayout();

        if (mMenu != null) {
            final ArrayList actionItems = mMenu.getActionItems();
            final int count = actionItems.size();
            for (int i = 0; i < count; i++) {
                final ActionProvider provider = actionItems.get(i).getActionProvider();
                if (provider != null) {
                    provider.setSubUiVisibilityListener(this);
                }
            }
        }

        final ArrayList nonActionItems = mMenu != null ?
                mMenu.getNonActionItems() : null;

        boolean hasOverflow = false;
        if (mReserveOverflow && nonActionItems != null) {
            final int count = nonActionItems.size();
            if (count == 1) {
                hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
            } else {
                hasOverflow = count > 0;
            }
        }

        if (hasOverflow) {
            if (mOverflowButton == null) {
                mOverflowButton = new OverflowMenuButton(mSystemContext);
            }
            ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
            if (parent != mMenuView) {
                if (parent != null) {
                    parent.removeView(mOverflowButton);
                }
                ActionMenuView menuView = (ActionMenuView) mMenuView;
                menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
            }
        } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
            ((ViewGroup) mMenuView).removeView(mOverflowButton);
        }

        ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
    }
这个方法首先是调用父类的updateMenuView()方法执行更新菜单的操作,然后是判断是否需要显示overflowbutton,如果需要显示则把overflowbutton添加到ActionMenuView中,主要的菜单控件的创建过程以及菜单控件与菜单信息的绑定过程应该是在父类的updateMenuView()中进行的:

    public void updateMenuView(boolean cleared) {
        final ViewGroup parent = (ViewGroup) mMenuView;
        if (parent == null) return;

        int childIndex = 0;
        if (mMenu != null) {
            mMenu.flagActionItems();
            ArrayList visibleItems = mMenu.getVisibleItems();
            final int itemCount = visibleItems.size();
            for (int i = 0; i < itemCount; i++) {
                MenuItemImpl item = visibleItems.get(i);
                if (shouldIncludeItem(childIndex, item)) {
                    final View convertView = parent.getChildAt(childIndex);
                    final MenuItemImpl oldItem = convertView instanceof MenuView.ItemView ?
                            ((MenuView.ItemView) convertView).getItemData() : null;
                    final View itemView = getItemView(item, convertView, parent);
                    if (item != oldItem) {
                        // Don't let old states linger with new data.
                        itemView.setPressed(false);
                        ViewCompat.jumpDrawablesToCurrentState(itemView);
                    }
                    if (itemView != convertView) {
                        addItemView(itemView, childIndex);
                    }
                    childIndex++;
                }
            }
        }

        // Remove leftover views.
        while (childIndex < parent.getChildCount()) {
            if (!filterLeftoverView(parent, childIndex)) {
                childIndex++;
            }
        }
    }
BaseMenuPresenter.updateMenuView()方法首先是执行了MenuBuilder.flagActionItems()方法,该方法根据配置的可见菜单项是否需要显示到ActionBar中对菜单信息进行分类:一类是需要到ActionBar中的,一类是需要显示到OverflowMenu中的;如果是需要显示到ActionBar中(也就是MenuItem.isActionButton()==true),则通过调用addItemView()方法把它添加到ActionMenuView中。

至此,整个ActionBar的菜单项就算是构造完成并添加进来了,是不是感觉特别的绕,恩,总结一下就是下图的调用过程:

Android ActionBar的源代码分析(二)_第2张图片
  

关于ActionBar的菜单构造过程分析到此为止,下一篇就ActionBar菜单的执行过程进行分析,敬请期待!


你可能感兴趣的:(Android)