Android 菜单系统分析
Posted zhenjie_chang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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创建菜单
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等内容。
布局文件生成并设置后,调用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;
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相关类的关系如图。
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的显示。
ActionMenuPresenter是ActionMenuView对应的一个管理类,ActionMenuView继承自LinearLayout,负责Menu的显示,而ActionMenuPresenter负责Menu的显示逻辑,负责将加载的Menu信息,按照对应的逻辑添加到ActionMenuView中。
将创建的ActionMenuPresenter对象调用menu.addMenuPresenter方法添加到Menu的变量presenters列表中。
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;
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中。
以上是关于Android 菜单系统分析的主要内容,如果未能解决你的问题,请参考以下文章
求C++编程题目【菜单】计算机菜单中有N个操作,而每个操作都会用一个或更多的单词来描述。