上一篇已经对ActionBar的菜单项的执行过程进行了分析,有兴趣的朋友可以看一下android中ActionBar的源代码分析(三),本章对ActionBar的OverflowMenu的运行机制进行分析。
ActionBar的OverflowMenu,就是ActionBar右边的三个小点点,点击以后就出现一个下拉菜单项,如图
OverflowMenu是为了解决ActionBar的菜单项太多,在一个屏幕上显示不出来,而把过多的菜单项放到下拉菜单中显示的问题;在配置菜单属性的时候,把android:showAsAction设为ifRoom或者never,就会利用ActionBar的OverflowMenu特性在下拉菜单显示出来了,当然如果你的机器是带有物理menu键的,还需要进行特殊处理,这个可以看一下我之前写的一篇文章android2.x使用ActionBar-强制显示OverflowButton
既然我们知道了如何使用OverflowMenu,那这个OverflowMenu在代码层次是如何实现的呢?从上几篇文章我们知道,类ActionMenuPresenter是负责ActionBar菜单项的绘制工作的,其中初始菜单参数的方法是initForMenu(),我们先看一下这个代码是如何实现的吧。
@Override
public void initForMenu(Context context, MenuBuilder menu) {
super.initForMenu(context, menu);
final Resources res = context.getResources();
//是否显示溢出菜单按钮
final ActionBarPolicy abp = ActionBarPolicy.get(context);
if (!mReserveOverflowSet) {
mReserveOverflow = abp.showsOverflowMenuButton();
}
//嵌入菜单最大宽度即放置ActionBar菜单项的最大宽度
if (!mWidthLimitSet) {
mWidthLimit = abp.getEmbeddedMenuWidthLimit();
}
//ActionBar最多显示的按钮数目
// Measure for initial configuration
if (!mMaxItemsSet) {
mMaxItems = abp.getMaxActionButtons();
}
//初始化溢出菜单按钮,并计算溢出菜单按钮宽度
int width = mWidthLimit;
if (mReserveOverflow) {
if (mOverflowButton == null) {
mOverflowButton = new OverflowMenuButton(mSystemContext);
final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
mOverflowButton.measure(spec, spec);
}
width -= mOverflowButton.getMeasuredWidth(); //显示的菜单按钮宽度需要减掉溢出菜单按钮的宽度
} else {
mOverflowButton = null;
}
mActionItemWidthLimit = width;
mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
// Drop a scrap view as it may no longer reflect the proper context/config.
mScrapActionButtonView = null;
}
这段代码首先调用父类(BaseMenuPresenter)的initForMenu()方法,父类的这个方法无非就是一些赋值操作,代码就不贴出来了,然后就根据ActionBarPolicy确定是否显示溢出菜单按钮(OverflowButton)以及ActionBar最多显示的菜单数目,最后对溢出菜单按钮进行初始化操作并计算溢出菜单按钮的宽度。可以看到,这里涉及了很多宽度的获取,包括嵌入的菜单宽度、最大按钮显示数目和溢出菜单按钮的宽度,其实就是为了确定当菜单配置为android:showAsAction=ifRoom时,何时才显示在溢出菜单中(或者干脆就不显示出来),代码逻辑可以参考ActionMenuPresenter.flagActionItems()方法,这里就不贴出来了; 溢出菜单按钮的实现类为OverflowMenuButton,后面会重点进行介绍,这里先记住它就是溢出菜单按钮吧。
溢出菜单按钮(OverflowMenuButton)什么时候放置到ActionBar上的呢?从android中ActionBar的源代码分析(二)我们可以看出,系统在创建菜单时,会调用ActionMenuPresenter的updateMenuView()方法,我们看一下这个方法的实现逻辑:
@Override
public void updateMenuView(boolean cleared) {
final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent();
if (menuViewParent != null) {
// 使用ActionBar的淡出淡入效果,其实系统本身并没有启用该效果
ActionBarTransition.beginDelayedTransition(menuViewParent);
}
super.updateMenuView(cleared); //使用父类的方法,构建ActionBar的按钮并添加到ActionMenuView上
((View) mMenuView).requestLayout();
//判断菜单是否配置了ActionProvider,如果有则设置ActionProvider的子菜单侦听事件
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;
}
}
//如果需要显示溢出菜单,则初始化溢出菜单按钮,并添加到ActionMenuView中
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);
}
这段代码首先是设置显示ActionBar的淡出淡入效果,从类ActionBarTransition的代码中可以看出,淡出淡入的效果受TRANSITIONS_ENABLED的控制,而TRANSITIONS_ENABLED是等于false的,而且也没发现在哪能设置这个变量的值,所以按照目前来看,ActionBar的淡出淡入效果是没有启用的;然后调用父类(BaseMenuPresenter)的updateMenuView()方法构建ActionBar的菜单按钮并添加到ActionMenuView 上,接着判断菜单是否配置启用了ActionProvider,如果启用了,就设置侦听其子菜单可见行改变的事件;最后判断是否显示溢出菜单,如果显示溢出菜单则调用溢出菜单按钮的初始化方法,并把它添加到ActionMenuView上显示出来;这样溢出菜单按钮就完成了整个的构建过程;
溢出菜单按钮点击以后,就会弹出下拉菜单来,这个过程在代码层次上具体是怎么实现的呢?这就需要我们看一下类OverflowMenuButton的具体实现了,OverflowMenuButton继承于类ImageButton,也就是说它实际就是一个图片按钮;我们先看一下OverflowMenuButton的构造方法吧
public OverflowMenuButton(Context context) {
super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
//设置控件可点击、可设置焦点、可用和可见性
setClickable(true);
setFocusable(true);
setVisibility(VISIBLE);
setEnabled(true);
//侦听控件本身的touch事件
setOnTouchListener(new ForwardingListener(this) {
@Override
public ListPopupWindow getPopup() {
if (mOverflowPopup == null) {
return null;
}
return mOverflowPopup.getPopup();
}
@Override
public boolean onForwardingStarted() { // touch事件转发开始后,显示溢出菜单
showOverflowMenu();
return true;
}
@Override
public boolean onForwardingStopped() { // 溢出菜单显示后再次touch该控件,则隐藏溢出菜单
// Displaying the popup occurs asynchronously, so wait for
// the runnable to finish before deciding whether to stop
// forwarding.
if (mPostedOpenRunnable != null) {
return false;
}
hideOverflowMenu();
return true;
}
});
}
OverflowMenuButton的构造方法,首先设置控件可点击、可设置焦点、可用和可见性,然后设置侦听控件的触摸事件,这里用到了ListPopupWindow.ForwardingListener类,该类其实是实现了View.OnTouchListener接口的,有几个方法需要重写:
@Override
public boolean performClick() {
if (super.performClick()) {
return true;
}
playSoundEffect(SoundEffectConstants.CLICK);
showOverflowMenu();
return true;
}
代码很简单,就是先判断是否父类的performClick()方法的返回值是否为true,如果为true则退出,换句话讲就是判断外部调用的onClick的侦听事件是否返回true,如果没有设置onClick事件的侦听或者侦听回调方法返回值为false,则继续执行后面的showOverflowMenu()方法,本例中由于没有侦听OverflowMenuButton的onClick事件,因此必然要走showOverflowMenu()方法。
public boolean showOverflowMenu() {
if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null &&
mPostedOpenRunnable == null && !mMenu.getNonActionItems().isEmpty()) {
OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
mPostedOpenRunnable = new OpenOverflowRunnable(popup);
// Post this for later; we might still need a layout for the anchor to be right.
((View) mMenuView).post(mPostedOpenRunnable);
// ActionMenuPresenter uses null as a callback argument here
// to indicate overflow is opening.
super.onSubMenuSelected(null);
return true;
}
return false;
}
代码很简单,首先判断溢出菜单是否已经显示,如果没有显示,则实例化类OverflowPopup和OpenOverflowRunnable,并延后执行OpenOverflowRunnable中run()方法,最后调用父类的onSubMenuSelected()方法。这里的类OverflowPopup是菜单弹出的辅助类,继承于类MenuPopupHelper,负责管理弹出窗体(PopupWindow)的生命周期;而类OpenOverflowRunnable负责PopupWindow的显示,我们看一下OpenOverflowRunnable的代码逻辑:
private class OpenOverflowRunnable implements Runnable {
private OverflowPopup mPopup;
public OpenOverflowRunnable(OverflowPopup popup) {
mPopup = popup;
}
public void run() {
mMenu.changeMenuMode();
final View menuView = (View) mMenuView;
if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
mOverflowPopup = mPopup;
}
mPostedOpenRunnable = null;
}
}
我们主要看run()方法,在该方法中,首先调用MenuBuilder.changeMenuMode()方法切换菜单模式,也就是说如果菜单已经弹出则隐藏,否则就弹出显示;接着调用OverflowPopup的tryShow()方法弹出溢出菜单窗体。也许有朋友就问,MenuBuilder.changeMenuMode()方法不是已经显示过一次溢出菜单了吗,这里再次显示会不会重复显示呢?当然不会,系统通过设置变量mPostedOpenRunnable是否为空来进行区分,保证只会执行一次显示溢出菜单窗体的代码。
public boolean tryShow() {
mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle);
mPopup.setOnDismissListener(this);
mPopup.setOnItemClickListener(this);
mPopup.setAdapter(mAdapter);
mPopup.setModal(true);
View anchor = mAnchorView;
if (anchor != null) {
final boolean addGlobalListener = mTreeObserver == null;
mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest
if (addGlobalListener) mTreeObserver.addOnGlobalLayoutListener(this);
anchor.addOnAttachStateChangeListener(this);
mPopup.setAnchorView(anchor);
mPopup.setDropDownGravity(mDropDownGravity);
} else {
return false;
}
if (!mHasContentWidth) {
mContentWidth = measureContentWidth();
mHasContentWidth = true;
}
mPopup.setContentWidth(mContentWidth);
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
mPopup.show();
mPopup.getListView().setOnKeyListener(this);
return true;
}
在这个代码中,首先是实例化ListPopupWindow,然后对这个ListPopupWindow对象进行各种属性赋值操作,最后调用show方法显示出来。这里的类ListPopupWindow通俗来讲就是一个包含一个ListView的PopupWindow,其具体的用法百度上有一大堆,有兴趣的朋友可以研究一下哈,这里就不过多赘述。使用ListPopupWindow时,有三个方法要注意:
private class MenuAdapter extends BaseAdapter {
private MenuBuilder mAdapterMenu;
private int mExpandedIndex = -1;
public MenuAdapter(MenuBuilder menu) {
mAdapterMenu = menu;
findExpandedIndex();
}
public int getCount() {
ArrayList items = mOverflowOnly ?
mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
if (mExpandedIndex < 0) {
return items.size();
}
return items.size() - 1;
}
public MenuItemImpl getItem(int position) {
ArrayList items = mOverflowOnly ?
mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems();
if (mExpandedIndex >= 0 && position >= mExpandedIndex) {
position++;
}
return items.get(position);
}
public long getItemId(int position) {
// Since a menu item's ID is optional, we'll use the position as an
// ID for the item in the AdapterView
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(ITEM_LAYOUT, parent, false);
}
MenuView.ItemView itemView = (MenuView.ItemView) convertView;
if (mForceShowIcon) {
((ListMenuItemView) convertView).setForceShowIcon(true);
}
itemView.initialize(getItem(position), 0);
return convertView;
}
void findExpandedIndex() {
final MenuItemImpl expandedItem = mMenu.getExpandedItem();
if (expandedItem != null) {
final ArrayList items = mMenu.getNonActionItems();
final int count = items.size();
for (int i = 0; i < count; i++) {
final MenuItemImpl item = items.get(i);
if (item == expandedItem) {
mExpandedIndex = i;
return;
}
}
}
mExpandedIndex = -1;
}
@Override
public void notifyDataSetChanged() {
findExpandedIndex();
super.notifyDataSetChanged();
}
}
我们重点看getCount()和getView()方法即可,在getCount()方法中,获取ActionBar的不可见菜单项,然后判断在这些不可见菜单项中是否有可扩展折叠菜单项,如果有则排除掉,换句话讲就是可扩展折叠的菜单项是不会显示在溢出菜单中的;在getView()方法中通过inflater布局文件framework\data\res\layout\popup_menu_item_layout.xml来创建视图,并对菜单项进行初始化操作,下面是popup_menu_item_layout.xml的内容
可以看到,其实每一个溢出菜单的菜单项的实现类为ListMenuItemView,它继承于LinearLayout;ListMenuItemView包含三个子视图:一个ImageView,负责显示菜单图标;一个TextView,负责显示菜单标题;一个TextView,负责显示菜单的快捷方式,默认情况下快捷方式是不显示的。其中两个TextView是在布局文件中列出,还有一个ImageView,是通过代码来创建的,可以看一下ListMenuItemView的setIcon()方法如下:
public void setIcon(Drawable icon) {
//判断图标是否需要显示
final boolean showIcon = mItemData.shouldShowIcon() || mForceShowIcon;
if (!showIcon && !mPreserveIconSpacing) {
return;
}
if (mIconView == null && icon == null && !mPreserveIconSpacing) {
return;
}
//如果需要显示图标,则创建控件并添加进来
if (mIconView == null) {
insertIconView();
}
//设置可见性
if (icon != null || mPreserveIconSpacing) {
mIconView.setImageDrawable(showIcon ? icon : null);
if (mIconView.getVisibility() != VISIBLE) {
mIconView.setVisibility(VISIBLE);
}
} else {
mIconView.setVisibility(GONE);
}
}
声明一下,setIcon()方法是在ListMenuItemView.initialize()方法中调用的,而ListMenuItemView.initialize()又是由类MenuPopupHelper.MenuAdapter在方法getView()中调用的;
public boolean shouldShowIcon() {
return mMenu.getOptionalIconsVisible();
}
很简单,就是直接调用MenuBuilder.getOptionalIconsVisible()的方法进行判断,而getOptionalIconsVisible()是直接返回mOptionalIconsVisible的值,mOptionalIconsVisible的默认值为false,它是通过setOptionalIconsVisible()设置进来的,不幸的是setOptionalIconsVisible()的方法修饰符为Internal,外部是无法访问的,只能通过反射的方式来修改;这就是为什么在默认情况下,溢出菜单显示的菜单项只有文字没有图标的原因了!
public void onDismiss() {
mPopup = null;
mMenu.close();
if (mTreeObserver != null) {
if (!mTreeObserver.isAlive()) mTreeObserver = mAnchorView.getViewTreeObserver();
mTreeObserver.removeGlobalOnLayoutListener(this);
mTreeObserver = null;
}
mAnchorView.removeOnAttachStateChangeListener(this);
}
这里调用了MenuBuilder.close()方法进行关闭弹出菜单操作,下面是MenuBuilder.close()方法的实现代码:
public void close() {
close(true);
}
final void close(boolean allMenusAreClosing) {
if (mIsClosing) return;
mIsClosing = true;
for (WeakReference ref : mPresenters) {
final MenuPresenter presenter = ref.get();
if (presenter == null) {
mPresenters.remove(ref);
} else {
presenter.onCloseMenu(this, allMenusAreClosing);
}
}
mIsClosing = false;
}
其实就是调用了ActionMenuPresenter.onCloseMenu()方法执行关闭操作:
public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
dismissPopupMenus();
super.onCloseMenu(menu, allMenusAreClosing);
}
public boolean dismissPopupMenus() {
boolean result = hideOverflowMenu();
result |= hideSubMenus();
return result;
}
可以看到执行关闭菜单的操作,都是执行了hideOverflowMenu()方法来实现的:
public boolean hideOverflowMenu() {
if (mPostedOpenRunnable != null && mMenuView != null) {
((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
mPostedOpenRunnable = null;
return true;
}
MenuPopupHelper popup = mOverflowPopup;
if (popup != null) {
popup.dismiss();
return true;
}
return false;
}