1, 概述
PopWindow (弹出框)和Dialog最关键的区别是Dialog不能指定显示位置,只能默认显示在屏幕中间(当然可以通过设置WindowManager参数来改变位置)。而PopWindow可以指定显示位置,更加灵活。PopupMenu就是利用PopWindow来实现的。
2 实现
PopupMenu是一个子窗口,不仅依附于Activity还依附于一个View控件,开发步骤如下,
1,初始化
View vpop = findViewById(R.id.~~~); // View
mMenu = new PopupMenu(this, vpop);
mMenu.inflate(R.menu.menu~~~); // 添加菜单项
mMenu.setOnMenuItemClickListener(this);
R.menu格式如下,
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_4g_conference_call"
android:title="@string/menu_4g_conference_call" />
<item
android:id="@+id/menu_history"
android:icon="@drawable/ic_menu_history_lt"
android:title="@string/action_menu_call_history_description" />
<item
android:id="@+id/menu_import_export"
android:title="@string/menu_import_export" />
<item
android:id="@+id/menu_clear_frequents"
android:title="@string/menu_clear_frequents" />
<item
android:id="@+id/menu_add_contact"
android:title="@string/menu_newContact"/>
<item
android:id="@+id/menu_call_settings"
android:title="@string/dialer_settings_label" />
</menu>
2,在acvitity中显示PopupMenu并且实现单击方法,
mMenu.show(); // 一般在单击vpop时显示
实现PopupMenu.OnMenuItemClickListener方法,
@Override
public boolean onMenuItemClick(MenuItem item) { // 菜单单击事件
switch (item.getItemId()) {
case R.id.menu_4g_conference_call:
•••
break;
case R.id.menu_history:
•••
break;
~~~
}
return false;
}
3,源码解析
3.1 对象创建
创建流程如下,
首先看PopupMenu中的构造方法,构造方法中为变量赋值,并且构造了2个对象,
public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr,
int popupStyleRes) {
mContext = context;
mMenu = new MenuBuilder(context);
mMenu.setCallback(this);
mAnchor = anchor; // 赋值
mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes);
mPopup.setGravity(gravity);
mPopup.setCallback(this);
}
3.2 加载菜单项
菜单项加载方法,
mMenu.inflate(R.menu.menu~~~);
Inflate方法如下,
public void inflate(@MenuRes int menuRes) {
getMenuInflater().inflate(menuRes, mMenu); // mMenu是MenuBuilder对象
}
getMeuInflater方法如下,
public MenuInflater getMenuInflater() {
return new MenuInflater(mContext); // 新建了一个MenuInflater对象
}
parseMenu方法如下,很明显的是一个xml文件解析过程,和Activity中布局文件解析的过程几乎完全一样,
private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
throws XmlPullParserException, IOException {
MenuState menuState = new MenuState(menu);
int eventType = parser.getEventType();
String tagName;
boolean lookingForEndOfUnknownTag = false;
String unknownTagName = null;
// This loop will skip to the menu start tag
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
if (tagName.equals(XML_MENU)) {
// Go to next tag
eventType = parser.next();
break;
}
throw new RuntimeException("Expecting menu, got " + tagName);
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
boolean reachedEndOfMenu = false;
while (!reachedEndOfMenu) {
switch (eventType) {
case XmlPullParser.START_TAG:
if (lookingForEndOfUnknownTag) {
break;
}
tagName = parser.getName();
if (tagName.equals(XML_GROUP)) {
menuState.readGroup(attrs);
} else if (tagName.equals(XML_ITEM)) {
menuState.readItem(attrs);
} else if (tagName.equals(XML_MENU)) {
// A menu start tag denotes a submenu for an item
SubMenu subMenu = menuState.addSubMenuItem();
registerMenu(subMenu, attrs);
// Parse the submenu into returned SubMenu
parseMenu(parser, attrs, subMenu);
} else {
lookingForEndOfUnknownTag = true;
unknownTagName = tagName;
}
break;
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)) {
// Add the item if it hasn't been added (if the item was
// a submenu, it would have been added already)
if (!menuState.hasAddedItem()) {
if (menuState.itemActionProvider != null &&
menuState.itemActionProvider.hasSubMenu()) {
registerMenu(menuState.addSubMenuItem(), attrs);
} else {
registerMenu(menuState.addItem(), attrs);
}
}
} else if (tagName.equals(XML_MENU)) {
reachedEndOfMenu = true;
}
break;
case XmlPullParser.END_DOCUMENT:
throw new RuntimeException("Unexpected end of document");
}
eventType = parser.next();
}
}
3.3 显示(show)
PopupWindow的showAsDropDown方法如下,
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
registerForScrollChanged(anchor, xoff, yoff, gravity);
mIsShowing = true;
mIsDropdown = true;
// 构造LayoutParams对象
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
preparePopup(p); // createDecorView方法最后会构造一个PopupDecorView对象
final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
updateAboveAnchor(aboveAnchor);
invokePopup(p);
}
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
mWindowManager.addView(decorView, p); // 完成View的测量,定位,绘制等流程
if (mEnterTransition != null) {
decorView.requestEnterTransition(mEnterTransition);
}
}
3.4 单击事件
onItemClick方法如下,
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MenuAdapter adapter = mAdapter;
adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
}
performItemAction方法如下,
public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
MenuItemImpl itemImpl = (MenuItemImpl) item; // 类型转换
if (itemImpl == null || !itemImpl.isEnabled()) {
return false;
}
boolean invoked = itemImpl.invoke(); // 分发菜单单击事件
final ActionProvider provider = item.getActionProvider();
final boolean providerHasSubMenu = provider != null && provider.hasSubMenu();
if (itemImpl.hasCollapsibleActionView()) {
invoked |= itemImpl.expandActionView();
if (invoked) close(true); // 处理完成之后,关闭弹出菜单
} else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
close(false);
if (!itemImpl.hasSubMenu()) {
itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl));
}
final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu();
if (providerHasSubMenu) {
provider.onPrepareSubMenu(subMenu);
}
invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter);
if (!invoked) close(true);
} else {
if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
close(true);
}
}
return invoked;
}
performItemAction逻辑比较简单,首先分发单击事件,处理完成之后,关闭菜单。
3.5 关闭
关闭菜单流程图如下,
接着上面的小节, close方法如下,
public final void close(boolean allMenusAreClosing) {
if (mIsClosing) return;
mIsClosing = true;
for (WeakReference<MenuPresenter> ref : mPresenters) {
final MenuPresenter presenter = ref.get();
if (presenter == null) {
mPresenters.remove(ref);
} else {
presenter.onCloseMenu(this, allMenusAreClosing);
}
}
mIsClosing = false;
}
MenuPresenter只是一个接口,继承该接口的也有多个类,到底调用谁的呢?
MenuBuilder有一个addMenuPresenter方法,添加MenuPresenter实例,
public void addMenuPresenter(MenuPresenter presenter, Context menuContext) {
mPresenters.add(new WeakReference<MenuPresenter>(presenter));
presenter.initForMenu(menuContext, this);
mIsActionItemsStale = true;
}
谁调用该方法呢? 在MenuPopupHelper的构造方法中,
menu.addMenuPresenter(this, context);
所以调用的是MenuPopupHelper的onCloseMenu方法,
最后的dismissImmediate方法如下,
private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) {
// If this method gets called and the decor view doesn't have a parent,
// then it was either never added or was already removed. That should
// never happen, but it's worth checking to avoid potential crashes.
if (decorView.getParent() != null) {
mWindowManager.removeViewImmediate(decorView); // 关闭
}
if (contentHolder != null) {
contentHolder.removeView(contentView);
}
// This needs to stay until after all transitions have ended since we
// need the reference to cancel transitions in preparePopup().
mDecorView = null;
mBackgroundView = null;
mIsTransitioningToDismiss = false;
}
4 PopupMenu
PopupMenu中的方法如下,
setGravity |
对其方法 |
inflate |
加载资源 |
show |
显示 |
dismiss |
关闭 |
setOnMenuItemClickListener |
菜单项单击监听 |
内部接口,OnMenuItemClickListener
public interface OnMenuItemClickListener {
/**
* This method will be invoked when a menu item is clicked if the item itself did
* not already handle the event.
*
* @param item {@link MenuItem} that was clicked
* @return <code>true</code> if the event was handled, <code>false</code> otherwise.
*/
public boolean onMenuItemClick(MenuItem item);
}
一般需要实现OnMenuItemClickListener接口的onMenuItemClick方法。