Android 菜单系统分析

Android的菜单系统主要指的是ActionBar的Menu菜单。首先来看下Android菜单的使用方法:

@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.test_menu_new,menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.removeItem(R.id.aaaa); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); }

1.        在Activity刚创建的时候会执行一次onCreateOptionsMenu方法和onPrepareOptionsMenu方法。

2.        每次点击“更多”按钮的时候,都会执行一次onPrepareOptionsMenu方法。

3.        当选中某个菜单Item的时候执行onOptionsItemSelected方法

ActionBar菜单的创建加载过程。

ActionBar及ActionBar上的菜单都是在Activity启动的时候创建的。当一个新的Activity启动的时候,ActivityManagerService调用ActivityThread的performLaunchActivity方法,此方法中会调用Activity的attach方法,为Activity初始化一些变量。在Activity的attach方法中会为每一个Activity创建一个对应的PhoneWindow对象。然后在PhoneWindow中会创建ActionBar及ActionBar相关的菜单。

Activity在启动初始化过程中会调用onCreate方法,然后调用setContentView来设置Activity的显示内容。我们就从setContentView来简单分析下Activity的menu创建过程。

Activity.setContentView

public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar(); }

Activity的setContentView方法,调用了getWindow的setContentView方法。这个getWindow获取的就是Attach方法中创建的PhoneWindow对象。然后调用initWindowDecorActionBar方法,来初始化ActionBar操作的相关方法。

PhoneWindow.setContentView

public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); }public void setContentView(View view, ViewGroup.LayoutParams params) { if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { …… } else { mContentParent.addView(view, params); } …… }

PhoneWindow的setContentView方法调用了两个参数的setContentView方法。这个方法中只保留的比较重要的和我们分析相关的代码。

    首先调用installDecor方法来创建这个Activity对应的整个Window的界面布局。然后调用mContentParent.addView方法,将自己在setContentView方法中传过来的布局文件添加到mContentParent布局中。

PhoneWindow.installDecor

private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted &;&; mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if appropriate. mDecor.makeOptionalFitsSystemWindows(); final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent); if (decorContentParent != null) { PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); if (!isDestroyed() &;&; (st == null || st.menu == null) &;&; !mIsStartingWindow) { invalidatePanelMenu(FEATURE_ACTION_BAR); } } } }

installDecor方法主要作用是生成和设置整个phoneWindow的窗口布局文件。首先调用generateDecor方法生成一个DecorView对象。然后调用generateLayout方法来添加生成窗口的布局文件,返回了一个mContentParent对象,contentParent对象就是setContentView的时候要把设置的布局文件 添加到这个View对象中,作为这个View的子View。然后从PhoneWindow的布局文件中找到id为decor_content_parent 的View对象,decorContentParent就是PhoneWindow布局文件的根布局文件。

    这个方法两个关键点:

1.  generateLayout方法生成窗口的布局文件

2.  invalidatePanelMenu创建菜单

Android 菜单系统分析 - 阿里云

PhoneWindow.generateLayout

protected ViewGroup generateLayout(DecorView decor) { …… int layoutResource; int features = getLocalFeatures(); if ((features &; (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { …… } else if ((features &; (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { layoutResource = R.layout.screen_title; } // System.out.println("Title!"); } else …… } mDecor.startChanging(); mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); …… return contentParent; }

这部分关键代码如上所示,根据当前的feature来决定PhoneWindow加载那个布局文件。我们只关心有ActionBar的情况,所以PhoneWindow加载的布局文件是在theme主题的

windowActionBarFullscreenDecorLayout属性中配置的。

themes_material.xml

<style name="Theme.Material"> <item name="windowActionBarFullscreenDecorLayout">@layout/screen_toolbar</item></style>

在Material主题中配置的screen_toolbar.xml布局文件。即在feature为FEATURE_ACTION_BAR的时候,加载screen_toolbar布局文件为整个PhoneWindow的布局。

<com.android.internal.widget.ActionBarOverlayLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/decor_content_parent" android:layout_width="match_parent" android:layout_height="match_parent" android:splitMotionEvents="false" android:theme="?attr/actionBarTheme"> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.android.internal.widget.ActionBarContainer android:id="@+id/action_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" style="?attr/actionBarStyle" android:transitionName="android:action_bar" android:touchscreenBlocksFocus="true" android:gravity="top"> <Toolbar android:id="@+id/action_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:navigationContentDescription="@string/action_bar_up_description" style="?attr/toolbarStyle" /> <com.android.internal.widget.ActionBarContextView android:id="@+id/action_context_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" style="?attr/actionModeStyle" /> </com.android.internal.widget.ActionBarContainer></com.android.internal.widget.ActionBarOverlayLayout>

    Screen_toolbar布局把PhoneWindow的界面分为了两个部分,ActionBarContainer和Content。Content中就是显示Activity布局的地方。ActionBarContainer中包括ToolBar和ActionBarContextView。平时的情况下显示Toolbar,在ActionMode模式下,ActinBarContextView显示。

    ToolBar负责显示title,icon,subtitle,ActionBar menu等内容。

Android 菜单系统分析 - 阿里云

   

布局文件生成并设置后,调用invalidatePanelMenu方法来生成并刷新Activity的菜单menu,invalidatePanelMenu最终调用到了doInvalidatePanelMenu方法中

PhoneWindow. doInvalidatePanelMenu

void doInvalidatePanelMenu(int featureId) { …… if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL) &;&; mDecorContentParent != null) { st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false); if (st != null) { st.isPrepared = false; preparePanel(st, null); } } }

在该方法中判断当前的featureId为FEATURE_ACTION_BAR或者FEATURE_OPTIONS_PANEL的时候,找到对应的PanelState对象,调用preparePanel方法对menu 菜单进行准备工作。

PhoneWindow.preparePanel

public final boolean preparePanel(PanelFeatureState st, KeyEvent event) { final Callback cb = getCallback(); final boolean isActionBarMenu = (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR); if (st.menu == null || st.refreshMenuContent) { if (st.menu == null) { if (!initializePanelMenu(st) || (st.menu == null)) { return false; } mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback); } …… if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) { // Ditch the menu created above …… return false; } …… if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) { …… return false; } …… return true; }

preparePanel方法主要处理菜单展示前的一些初始化操作,首先getCallback方法获取回调对象,此处的回调对象就是要显示的Activity的对象。

    在Activity启动的时候st.menu是null,所以调用initializePanelMenu方法来初始化st.menu对象。然后将初始化好的menu对象设置到mDecorContentParent中,mDecorContentParent是PhoneWIndow的根布局ActionBarOverlayLayout的对象。此处稍后再分析。

    初始化成功后,回调Activity的onCreatePanelMenu方法,来加载menu菜单。在回调onCreatePanelMenu方法的时候,会调用Activity的onCreateOptionsMenu方法来创建菜单,同时把创建的菜单对象menu作为参数传递了过去。

    然后回调Activity的onPreparePanel方法,在回调onPreparePanel的时候会调用onPrepareOptionsMenu方法

    到此为止,菜单的 onCreateOptionsMenu 和onPrepareOptionsMenu方法调用的逻辑就分析完成了,下边接着分析Menu的加载过程。

Android菜单内容的加载过程 public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.test_menu_new,menu); return true; }

在重写Activity的onCreateOptionsMenu方法中,参数Menu就是PhoneWindow中创建的menu对象,getMenuInflater方法获取的一个MenuInflater的对象,然后调用MenuInflater的inflate方法将test_menu_new.xml文件中的菜单解析并保存到对象menu中。

    MenuInflater类主要负责menu.xml菜单的解析,并将解析的内容保存到Menu对象中。

private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) throws XmlPullParserException, IOException { MenuState menuState = new MenuState(menu); …… while (!reachedEndOfMenu) { switch (eventType) { case XmlPullParser.START_TAG: …… if (tagName.equals(XML_GROUP)) { menuState.readGroup(attrs); } else if (tagName.equals(XML_ITEM)) { menuState.readItem(attrs); } else if (tagName.equals(XML_MENU)) { …… } else { lookingForEndOfUnknownTag = true; unknownTagName = tagName; } break; case XmlPullParser.END_TAG: if (tagName.equals(XML_ITEM)) { if (!menuState.hasAddedItem()) { if (menuState.itemActionProvider != null &;&; menuState.itemActionProvider.hasSubMenu()) { registerMenu(menuState.addSubMenuItem(), attrs); } else { registerMenu(menuState.addItem(), attrs); } } } …… } eventType = parser.next(); } }
在解析菜单文件的时候,根据menu创建了一个MenuState对象,调用menuState的readItem来读取xml中的属性信息,并保存到menuState的变量中。在读取完某个item的所有属性后,调用menuState.addItem方法将读取的item信息保存到系统的Menu对象中
public MenuItem addItem() { itemAdded = true; MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle); setItem(item); return item; }

调用menu.add方法将解析的Menu item信息保存到menu中。这样就把menu.xml文件中的所有的菜单item都保存到了菜单menu中。

    菜单主要包括Menu,SubMenu以及MenuItem

    Menu是整个菜单的容器,Menu中其中包括SubMenu.Menu及SubMenu中有一些MenuItem类型的ArrayList列表。解析的menu.xml文件的item信息就保存到了Menu类的变量ArrayList<MenuItemImpl> mItems中。

    几个menu相关类的关系如图。

Android 菜单系统分析 - 阿里云

    Menu.xml文件解析完成之后,就把menu.xml文件中的menu的信息保存到了传递过来的参数menu中。

Menu接口

public interface Menu { public MenuItem add(CharSequence title); public MenuItem add(@StringRes int titleRes); SubMenu addSubMenu(final CharSequence title); SubMenu addSubMenu(@StringRes final int titleRes); public void removeItem(int id); public MenuItem getItem(int index); public boolean performIdentifierAction(int id, int flags);}

SubMenu 接口

public interface SubMenu extends Menu { public SubMenu setHeaderTitle(@StringRes int titleRes); public SubMenu setHeaderTitle(CharSequence title); public SubMenu setHeaderIcon(@DrawableRes int iconRes); public SubMenu setHeaderIcon(Drawable icon); public SubMenu setHeaderView(View view); public MenuItem getItem();}

MenuItem接口

public interface MenuItem { public int getItemId(); public int getGroupId(); public MenuItem setTitle(CharSequence title); public CharSequence getTitle(); public MenuItem setIcon(Drawable icon); public Drawable getIcon(); public MenuItem setVisible(boolean visible); public boolean isVisible(); public MenuItem setEnabled(boolean enabled); public boolean isEnabled();}

接着分析mDecorContentParent.setMenu方法。mDecorContentParent是ActionBarOverlayLayout的对象,在ActionBarOverlayLayout的setMenu方法中,ActionBarOverlayLayout获取到他的子类id 为actionBar的ToolBar,调用ToolBar的包装类ToolbarWidgetWraper的setMenu. ToolbarWidgetWraper又调用ToolBar的setMenu方法。

ToolbarWidgetWraper.setMenu

public void setMenu(Menu menu, MenuPresenter.Callback cb) { if (mActionMenuPresenter == null) { mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext()); mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter); } mActionMenuPresenter.setCallback(cb); mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter); }

首先创建了一个ActionMenuPresenter的对象,然后以ActionMenuPresenter的对象和menu作为参数,调用Toolbar的setMenu方法。

private void ensureMenuView() { if (mMenuView == null) { mMenuView = new ActionMenuView(getContext()); mMenuView.setPopupTheme(mPopupTheme); mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener); mMenuView.setMenuCallbacks(mActionMenuPresenterCallback, mMenuBuilderCallback); final LayoutParams lp = generateDefaultLayoutParams(); lp.gravity = Gravity.END | (mButtonGravity &; Gravity.VERTICAL_GRAVITY_MASK); mMenuView.setLayoutParams(lp); addSystemView(mMenuView, false); } } public void setMenu(MenuBuilder menu, ActionMenuPresenter outerPresenter) { ensureMenuView(); final MenuBuilder oldMenu = mMenuView.peekMenu(); if (oldMenu != null) { oldMenu.removeMenuPresenter(mOuterActionMenuPresenter); oldMenu.removeMenuPresenter(mExpandedMenuPresenter); } if (menu != null) { menu.addMenuPresenter(outerPresenter, mPopupContext); menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext); } mMenuView.setPresenter(outerPresenter); }

Toolbar的setMenu方法首先调用ensureMenuView方法创建一个ActionMenuView对象,然后调用addSystemView方法将它添加到Toolbar中,ActionMenuView是继承自LinearLayout类,负责Toolbar上Menu的显示。

Android 菜单系统分析 - 阿里云

ActionMenuPresenter是ActionMenuView对应的一个管理类,ActionMenuView继承自LinearLayout,负责Menu的显示,而ActionMenuPresenter负责Menu的显示逻辑,负责将加载的Menu信息,按照对应的逻辑添加到ActionMenuView中。

将创建的ActionMenuPresenter对象调用menu.addMenuPresenter方法添加到Menu的变量presenters列表中。

Android 菜单系统分析 - 阿里云

ActionMenuView菜单显示逻辑的简单分析:ActionMenuView的显示逻辑比较复杂,主要由ActionMenuPresenter类控制,我们主要从以下几个步骤来分析:

1.  ActionMenuPresenter.initForMenu

2.  ActionMenuPresenter.getMenuView

3.  ActionMenuPresenter.updateMenuView

ActionMenuPresenter.initForMenu

public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) { super.initForMenu(context, menu); final Resources res = context.getResources(); if (mReserveOverflow) { if (mOverflowButton == null) { mOverflowButton = new OverflowMenuButton(mSystemContext); if (mPendingOverflowIconSet) { mOverflowButton.setImageDrawable(mPendingOverflowIcon); mPendingOverflowIcon = null; mPendingOverflowIconSet = false; } } } else { mOverflowButton = null; } }

首先调用BaseMenuPresenter的initForMenu方法,保存传入的参数MenuBuilder的值。然后创建了一个OverflowMenuButton,当ActionMenuView需要显示更多的时候,ActionMenuView应该添加OverflowMenuButton.

public MenuView getMenuView(ViewGroup root) { MenuView oldMenuView = mMenuView; MenuView result = super.getMenuView(root); return result; }

ActionMenuPresenter的getMenuView方法调用的是BaseMenuPresenter的getMenuView方法。

BaseMenuPresenter.getMenuView

public MenuView getMenuView(ViewGroup root) { if (mMenuView == null) { mMenuView = (MenuView) mSystemInflater.inflate(mMenuLayoutRes, root, false); mMenuView.initialize(mMenu); } return mMenuView; }
在BaseMenuPresenter.getMenuView方法中使用inflate方法加载mMenuLayoutRes文件来创建了一个MenuView的对象,并调用MenuView的initialize方法初始化了MenuView对象。

ActionMenuPresenter.updateMenuView

在分析ActionMenuPresenter的updateMenuView方法时,我们应该首先分析下ActionMenuPresenter父类BaseMenuPresenter的updateMenuView方法。

BaseMenuPresenter.updateMenuView

public void updateMenuView(boolean cleared) { final ViewGroup parent = (ViewGroup) mMenuView; if (parent == null) return; int childIndex = 0; if (mMenu != null) { mMenu.flagActionItems(); ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); final int itemCount = visibleItems.size(); for (int i = 0; i < itemCount; i++) { MenuItemImpl item = visibleItems.get(i); …… addItemView(itemView, childIndex); childIndex++; } } } }

在这个方法中首先调用mMenu.flagActionItems()方法来遍历设置每个菜单条目是ActionItems还是NoActionItems.根据不同的类型分别添加到不同的列表中。

ActionItems显示在ActionBar上,NoActionItems显示在更多的弹出菜单中。

public void flagActionItems() { final ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); //调用对应的MenuPresenter来确定MenuItem的分类,ActionItem还是NoActionItem boolean flagged = false; for (WeakReference<MenuPresenter> ref : mPresenters) { final MenuPresenter presenter = ref.get(); if (presenter == null) { mPresenters.remove(ref); } else { flagged |= presenter.flagActionItems(); } } if (flagged) { mActionItems.clear(); mNonActionItems.clear(); final int itemsSize = visibleItems.size(); for (int i = 0; i < itemsSize; i++) { MenuItemImpl item = visibleItems.get(i); if (item.isActionButton()) { //ActionButton 添加到mActionItems列表 mActionItems.add(item); } else { //noActionItem添加到mNonActionItems列表 mNonActionItems.add(item); } } } else { mActionItems.clear(); mNonActionItems.clear(); mNonActionItems.addAll(getVisibleItems()); } }

ActionMenuPresenter.updateMenuView方法主要处理逻辑就是根据NoActionItems数量来决定是否显示OverflowMenuButton,即更多按钮。当noActionItems的数量大于1,就表示有Item需要显示在更多的弹出菜单中,就需要显示更多按钮了。调用ActionMenuView的addView方法,添加“更多”按钮到ActionMenuView的末尾。

    接着分析点击更多是弹出菜单显示逻辑。点击更多菜单调用showOverflowMenu方法。弹出菜单是由一个ListPopWindow来实现的.

ActionMenuPresenter.showOverflowMenu

OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);mPostedOpenRunnable = new OpenOverflowRunnable(popup);((View) mMenuView).post(mPostedOpenRunnable);

showOverflowmenu中创建了一个OverflowPopup对象和一个OpenOverflowRunable对象。OverflowPopup继承自MenuPopupHelper,然后调用OpenOverflowRunnable中的方法。

 

private class OpenOverflowRunnable implements Runnable { private OverflowPopup mPopup; public OpenOverflowRunnable(OverflowPopup popup) { mPopup = popup; } public void run() { /// M: Add NULL pointer check if (mMenu != null) { mMenu.changeMenuMode(); } final View menuView = (View) mMenuView; mPopup.tryShow(); } }

Runable方法中首先调用了Menu.changeMenuMode(), MenuBuilder的changMenuMode方法回调PhoneWindow的onMenuModeChanged方法。最终回调Activity的onPrepareOptionsMenu方法。这边就是为什么每次点击更多按钮的时候会回调Activity的onPrepareOptionsMenu方法。

然后调用MenuPopupHelper.tryShow方法。
public boolean tryShow() { if (isShowing()) { return true; } mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes); mPopup.setOnItemClickListener(this); mPopup.setAdapter(mAdapter); …… mPopup.show(); return true; }

tryShow方法中创建了一个ListPopupWindow对象,设置他的adapter,ListPopupWindow的Adatper是根据menu.getNoActionItems来创建的,NoActionItems列表中的菜单会显示在ListPopupWindow中然后调用ListPopupWindow对象的show方法,这样ActionBar的menu菜单点击更多按钮的弹出菜单就可以显示出来了。

    ActionBar菜单实现的总结:

1.  在创建Activity的时候,PhoneWindow加载窗口布局包括对应的ActionBar,创建Menu对象,并把创建的Menu设置到Activity的ActionBar中

2.  调用invalidateOptionsPanel方法,初始化菜单,回调Activity的onCreateOptionsMenu,解析menu.xml文件,保存到Menu中。(包括Menu,SubMenu及MenuItem的实现)。

3.  ActionMenuView是ActionBar中负责显示菜单的View,继承自LinearLayout,具体的显示逻辑由ActionMenuPresenter类来实现。

ActionMenuPresenter的updateMenuView负责菜单的显示逻辑,决定哪些Item显示在ActionBar上,哪些Item显示在弹出菜单中。显示在ActionBar的菜单Item添加到ActionMenuView中,显示在弹出菜单的Item添加到ListPopupWindow中。