1.PopupWindow
PopupWindow类用来实现一个弹出框,可以使用任意布局的View作为其内容,这个弹出框是悬浮在当前activity之上的。
2.用法
点击按钮弹出PopupWindow:
private void showPopupWindow(View view) {
//自定义布局,作为PopupWindow显示的内容
View contentView = LayoutInflater.from( mContext).inflate(R.layout.pop_window, null);
final PopupWindow popupWindow = new PopupWindow(contentView,LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
popupWindow.setTouchable(true);
popupWindow.setTouchInterceptor(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("mengdd", "onTouch : ");
return false; //这里如果返回true的话,touch事件将被拦截,拦截后 PopupWindow的onTouchEvent不被调用,这样点击外部区域无法dismiss
}
});
// 如果不设置PopupWindow的背景,无论是点击外部区域还是Back键都无法dismiss弹框
popupWindow.setBackgroundDrawable( getResources().getDrawable(R.drawable.selectmenu_bg_downward));
// 设置好参数之后再show
popupWindow.showAsDropDown(view);
}
第一次实现的时候遇到了问题,就是弹出框不会在按下Back键的时候消失,点击弹框外区域也没有正常消失,搜索了一下,都说只要设置背景就好了。然后就找了个图片,果然弹框能正常dismiss了(见注释)。
3.源码分析
显示提供了两种形式:
①showAtLocation()显示在指定位置,有两个方法重载:
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if(isShowing() || m content view == null) {
return;
}
TransitionManager.endTransitions( mDecorView);
detachFromAnchor();
mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);
p.x = x;
p.y = y;
invokePopup(p);
}
②showAsDropDown()显示在一个参照物View的周围,有三个方法重载:
public void showAsDropDown(View anchor) {
showAsDropDown(anchor, 0, 0);
}
public void showAsDropDown(View anchor, int xoff, int yoff) {
showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY);
}
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
if(isShowing() || m content view == null) {
return;
}
TransitionManager.endTransitions( mDecorView);
attachToAnchor(anchor, xoff, yoff, gravity);
mIsShowing = true;
mIsDropdown = true;
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getApplicationWindowToken());
preparePopup(p);
final boolean aboveAnchor = findDropDownPosition( anchor, p, xoff, yoff, p.width, p.height, gravity, mAllowScrollingAnchorParent);
updateAboveAnchor(aboveAnchor);
p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
invokePopup(p);
}
可以看出来,弹出的方法中首先都需要preparePopup() ,最后再invokePopup() 。
prepare的方法中可以看到有无背景的分别:
private void preparePopup( WindowManager.LayoutParams p) {
if (mContentView == null || mContext == null || mWindowManager == null) {
throw new IllegalStateException("You must specify a valid content view by calling setContentView() before attempting to show the popup.");
}
if (mBackground != null) {
mBackgroundView =createBackgroundView( mContentView);
mBackgroundView.setBackground( mBackground);
} else {
mBackgroundView = mContentView;
}
mDecorView = createDecorView( mBackgroundView);
mDecorView.setIsRootNamespace(true);
mBackgroundView.setElevation(mElevation);
p.setSurfaceInsets(mBackgroundView, true, true);
mPopupViewInitialLayoutDirectionInherited =(mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
}
private PopupBackgroundView createBackgroundView(View contentView) {
final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
final int height;
if(layoutParams != null && layoutParams.height == WRAP_CONTENT) {
height = WRAP_CONTENT;
} else {
height = MATCH_PARENT;
}
final PopupBackgroundView backgroundView = new PopupBackgroundView(mContent);
final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(MATCH_PARENT, height);
BackgroundView.addView(contentView, listParams);
return backgroundView;
}
背景是否为空对Touch事件的影响:
如果有背景,则会在contentView外面包一层PopupViewContainer之后作为mPopupView,如果没有背景,则直接用contentView作为mPopupView。
而这个PopupViewContainer是一个内部私有类,它继承了FrameLayout,在其中重写了Key和Touch事件的分发处理:
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
if ((event.getAction() == MotionEvent.ACTION_DOWN) && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss();
return true;
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss();
return true;
} else {
return super.onTouchEvent(event);
}
}
由于PopupView本身并没有重写Key和Touch事件的处理,所以如果没有包这个外层容器类,点击Back键或者外部区域是不会导致弹框消失的。
补充Case: 弹窗不消失,但是事件向下传递
如上所述:
设置了PopupWindow的background,点击Back键或者点击弹窗的外部区域,弹窗就会dismiss。相反,如果不设置PopupWindow的background,那么点击back键和点击弹窗的外部区域,弹窗是不会消失的.
那么,如果我想要一个效果,点击外部区域,弹窗不消失,但是点击事件会向下面的activity传递,比如下面是一个WebView,我想点击里面的链接等.
研究了半天,说是要给Window设置一个Flag,WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
看了源码,这个Flag的设置与否是由一个叫mNotTouchModal的字段控制,但是设置该字段的set方法被标记为@hide。
所以要通过反射的方法调用:
public static void setPopupWindowTouchModal( PopupWindow popupWindow, boolean touchModal) {
if (null == popupWindow) {
return;
}
Method method;
try {
method = PopupWindow.class.getDecl aredMethod("setTouchModal", boolean.class);
method.setAccessible(true);
method.invoke(popupWindow, touchModal);
} catch (Exception e) {
e.printStackTrace();
}
}
然后在程序中:
UIUtils.setPopupWindowTouchModal(popupWindow, false);
该popupWindow外部的事件就可以传递给下面的Activity了。



