前面已经对ActionBar的初始化过程以及ActionBarView的布局进行比较详细的分析,没有看过的朋友可以移步android中ActionBar的源代码分析(一)。本章接着对ActionBar菜单的构造过程进行分析。
关于actionbar的菜单机制,网上已经有很多大牛写过类似的文章,个人认为写的不错的,有兴趣的朋友可以看一下这个:ANDROID的ActionBar及菜单机制
不过这篇文章从ActionBar所涉及的UML类图出发,概括了ActionBar的运行机制,可能有朋友看得云里雾里,下面我就详细地对相关点进行分析。
ActionBar的菜单构造过程还是很复杂的,中间涉及的类非常多,为了把这个过程说明白,我先把涉及到的类图画出来吧:
是不是被这张图吓了一跳,个人感觉这样的设计过于复杂,类与类之间的耦合度很高。概括来说,就是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);
}
}
});
}
}
}
}
@Override
public void invalidatePanelMenu(int featureId) {
mInvalidatePanelMenuFeatures |= 1 << featureId;
if (!mInvalidatePanelMenuPosted && mDecor != null) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
mInvalidatePanelMenuPosted = true;
}
}
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;
}
};
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()方法中获取回调对象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);
}
}
首先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;
}
然后就是构造ActionMenuView的过程了,这个可以看看ActionMenuPresenter.getMenuView()的实现过程了:
@Override
public MenuView getMenuView(ViewGroup root) {
MenuView result = super.getMenuView(root);
((ActionMenuView) result).setPresenter(this);
return result;
}
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;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
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();
}
}
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;
}
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的菜单项就算是构造完成并添加进来了,是不是感觉特别的绕,恩,总结一下就是下图的调用过程:
关于ActionBar的菜单构造过程分析到此为止,下一篇就ActionBar菜单的执行过程进行分析,敬请期待!