上一篇介绍了PopupWindow的创建和显示,这一篇介绍一下几个比较常用方法,并借助源码解释几个使用过程中比较常见的几个问题,然后对ListPopupWindow和PopupMenu的使用进行简单介绍。主要涉及到下面三个方法的使用:
setOutsideTouchable(boolean touchable) setFocusable(boolean focusable) setBackgroundDrawable(Drawablebackground)
该方法只有在focusable为false的情况下才会起作用,touchable默认值是false,只要设置了touchable为false点击PopupWindow以外的区域,PopupWindow不会自动隐藏,但是一般情况下focusable默认值是true,所以我们点击PopupWindow以外区域会自动隐藏。当focusable为false时,我们设置touchable为true,这时候不但点击屏幕其它区域PopupWindow会自动消失,而且 事件也会有穿透性 ,如果我们点击区域处于其它可操作View的范围内如按钮,会出发按钮点击事件,下方有截图。如果focusable为true,touchable所具有的该属性也将失去作用,因此可以简单理解为focusable优先级高于touchable的。touchable事件穿透性的属性跟ListPopupWindow中setModel属性类似,下文会介绍。部分版本的手机上面在点击外部区域的时候PopupWindow并没有跟预想的一样隐藏,这种情况还跟setBackgroundDrawable方法有关,下文会借助源码做一下分析。
该方法非常重要,不但会影响PopupWindow中View事件的执行,还会影响系统返回键对PopupWindow的处理。
在PopupWindow弹出来的时候,我们点击返回键并不想返回上一页而是直接隐藏弹框,如果不设置该属性,我们点击返回键,就会直接返回到上一层级,部分版本还需要使用setBackgroundDrawable设置背景。
PopupWindow弹出来多数情况我们需要在弹框内处理一些逻辑,如果不设置focusable为true,会导致弹框中所有View的事件无响应。例如我们想弹出一个列表,列表使用的是ListView,这会导致ListView中onItemClick事件不起作用。
layout_popup.xml布局文件如下:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/listView" android:background="#ccc" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout>
创建一个包含ListView的PopupWindow部分代码如下:
Viewview=View.inflate(context, R.layout.layout_popup,null);
popupWindow.setFocusable(false);//focusable为false
ListViewlistView= (ListView) view.findViewById(R.id.listView);
listView.setAdapter(new ArrayAdapter<>(context,android.R.layout.simple_list_item_1,getData()));
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, Viewview, int position, long id) {
//无响应
}
});
该方法就是设置一个背景图片,但是它所影响的不仅仅是有无背景这样简单而已。有时候操作PopupWindow其它区域,但是PopupWindow并没有隐藏多数是该方法没有使用导致的,当然了本质上还是Android版本差异性导致的,下面会结合源代码分析一下。另外网络中有许多文章说PopupWindow是线程阻塞的,而AlertDialog不是线程阻塞的,个人认为这种情况也是该方法导致的,并不是所谓的PopupWindow线程阻塞的控件。
setBackgroundDrawable传入的是一个Drawable,可以使用BitmapDrawable或者ColorDrawable,每次PopupWindow需要显示的时候,不管是在showAsDropDown还是showAtLocation方法都有一个preparePopup方法。
public void showAsDropDown(Viewanchor, int xoff, int yoff, int gravity) {
//...
preparePopup(p);
}
有时候我们点击PopupWindow外部但是并没有消失,查看一下该方法的逻辑就可以知道原因所在了,在Android5.1.1中src源码如下,在该版本下编译运行后,点击外部区域PopupWindow并不会消失。
private void preparePopup(WindowManager.LayoutParams p) {
//...
if (mBackground != null) {
// when a background is available, we embed the content view
// within another view that owns the background drawable
PopupViewContainerpopupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParamslistParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
popupViewContainer.setBackground(mBackground);
popupViewContainer.addView(mContentView, listParams);
mPopupView = popupViewContainer;
} else {
mPopupView = mContentView;
}
}
当我们使用setBackgroundDrawable设置了背景后mPopupView使用的是popupViewContainer,否则使用的是mContentView,因为mContentView就是我们设置PopupWindow的View,但是popupViewContainer中处理的事件逻辑,包括返回键和点击屏幕touch事件。
private class PopupViewContainer extends FrameLayout {
//返回键处理
@Override
public boolean dispatchKeyEvent(KeyEventevent) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getRepeatCount() == 0) {
KeyEvent.DispatcherStatestate = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherStatestate = 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(MotionEventev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
//touch事件处理
@Override
public boolean onTouchEvent(MotionEventevent) {
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);
}
}
}
所有的事件接收都在PopupViewContainer中处理的,PopupViewContainer对象在mBackground!=null的情况下才会生成,所以如果我们不设置背景,PopupWindow不会响应返回键隐藏,当然了点击其它区域PopupWindow也不会隐藏。
所谓的“阻塞”,这里也可以解释一下了,纯属个人理解。由上面setOutsideTouchable方法知道,默认touchable是false,false情况下点击PopupWindow其它区域,事件是不具有穿透性的,也就是说一旦PopupWindow弹出后,即使可以看到其它可操作View,这时候也是无法操作的,又由于没有设置setBackgroundDrawable,点击其它区域PopupWindow也不会隐藏,这种情况下如果设置setFocusable为true,我们只可以操作PopupWindow中View,PopupWindow以外的区域都无法操作,仿佛被“阻塞”了一样,这大概就是网络中所说的PopupWindow是线程阻塞的控件的由来,它只是不响应屏幕中其它可视View的操作,后台如果跑个子线程或者执行其它方法都还会继续执行,不会中断任何操作。
但是在Android6.0中即使不设置setBackgroundDrawable,点击PopupWindow其它区域也会自动隐藏掉,还是看preparePopup中方法的实现。
private void preparePopup(WindowManager.LayoutParams p) {
// When a background is available, we embed the content view within
// another view that owns the background drawable.
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
mDecorView = createDecorView(mBackgroundView);
}
实现跟6.0以前的版本明显不同,这里不管有没有设置mBackground,最后都会创建一个mDecorView,所有的事件处理都在mDecorView中了,这样就解决了只有设置setBackgroundDrawable才会响应事件的bug。
private class PopupDecorView extends FrameLayout {
@Override
public boolean dispatchKeyEvent(KeyEventevent) {
//...
}
@Override
public boolean dispatchTouchEvent(MotionEventev) {
//...
}
@Override
public boolean onTouchEvent(MotionEventevent) {
//...
}
}
由于Android 版本差异性,所以在开发的时候建议设置一下背景,如果不需要背景设置透明即可 setBackgroundDrawable(new ColorDrawable(Color.parseColor("#00000000"))) 。
无论是从setWidth还是从构造方法中都是赋值mWidth或者mHeight,而这两个属性就是PopupWindow弹框View的高宽。
public void setWidth(int width) {
mWidth = width;
}
public PopupWindow(ViewcontentView, int width, int height, boolean focusable) {
if (contentView != null) {
mContext = contentView.getContext();
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
首先创建PopupWindow的时候必须设置一个contentView,为什么contentView的高宽不能作为PopupWindow的高宽呢?因为contentView的高宽必须最终由父View分配才可以,这点可以参看 Android浅谈LayoutParams ,PopupWindow不是一个View而是一个窗体,它不同于以前Activity中的View,在Activity中会使用DecorView作为顶层布局,顶层布局的高宽就是屏幕的高宽,但是PopupWindow弹框的高宽是动态的,不是直接铺满屏幕的,所以高宽不能是屏幕的高宽,又由于它没有父布局来为它分配高宽,所以如果不开发者不设置系统无法知道PopupWindow中View需要的高宽。
当我们在showAsDropDown显示PopupWindow的时候,里面有下面两个方法,源码摘自Android6.0:
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);
private WindowManager.LayoutParamscreatePopupLayoutParams(IBindertoken) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
//mHeightMode,mWidthMode默认值为0
if (mHeightMode < 0) {
p.height = mLastHeight = mHeightMode;
} else {
p.height = mLastHeight = mHeight;//
}
if (mWidthMode < 0) {
p.width = mLastWidth = mWidthMode;
} else {
p.width = mLastWidth = mWidth;//
}
return p;
}
private void preparePopup(WindowManager.LayoutParams p) {
// When a background is available, we embed the content view within
// another view that owns the background drawable.
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
//创建PopupWindow的顶层布局
mDecorView = createDecorView(mBackgroundView);
//赋值PopupWindow宽高
mPopupWidth = p.width;
mPopupHeight = p.height;
}
mPopupWidth和mPopupHeight就是PopupWindow的宽高,如果我们不设置mWidth和mHeight它们默认值是0,这时候根本看不到PopupWindow,所以一定要设置宽高。
ListPopupWindow是为了简化PopupWindow而专门创建的一个用于弹出列表的弹框,事实上内部有一个PopupWindow和ListView,在使用ListPopupWindow的时候我们可以不用设置宽高,当我们不设置宽高的时候会默认使用ListView中内容的宽高。除此之外还有一个属性就是可以设置model属性,该属性跟上面setOutsideTouchable类似但又有些不同,如果设置了该属性为true,那么弹出窗口后它的事件便不具有了穿透性,当弹框显示的时候,点击其它区域是没有响应的,如果设置false,事件才具有穿透性,默认值是false。
ListPopupWindow使用也很简单,示例代码如下:
//getData是一个String类型的列表 listPopupWindow=new ListPopupWindow(this); listPopupWindow.setAdapter(new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,getData())); listPopupWindow.setAnchorView(btn); listPopupWindow.setModal(true); listPopupWindow.show();
PopupMenu跟ListPopupWindow类似,只是可以直接使用菜单来填充列表了,所以它也是弹出一个window列表,使用弹出菜单跟在使用ActionBar或者Toolbar时候溢出菜单类似,内部默认不支持图标,但是我们可以使用反射强制让菜单显示图标。
<menuxmlns:android="http://schemas.android.com/apk/res/android"> <itemandroid:id="@+id/action_edit" android:icon="@drawable/ic_edit_black_24dp" android:title="@string/popup_menu_edit"/> <itemandroid:id="@+id/action_delete" android:title="@string/popup_menu_delete"/> <itemandroid:id="@+id/action_ignore" android:title="@string/popup_menu_ignore"/> <itemandroid:id="@+id/action_share" android:title="@string/popup_menu_share"> <menu> <itemandroid:id="@+id/action_share_email" android:title="@string/popup_menu_share_email"/> <itemandroid:id="@+id/action_share_circles" android:title="@string/popup_menu_share_circles"/> </menu> </item> </menu>
popupMenu = new PopupMenu(this, btn);
final MenuInflatermenuInflater = popupMenu.getMenuInflater();
menuInflater.inflate(R.menu.popup_menu, popupMenu.getMenu());
//使用反射强制显示icon
try {
Fieldfield = popupMenu.getClass().getDeclaredField("mPopup");
field.setAccessible(true);
MenuPopupHelpermHelper = (MenuPopupHelper) field.get(popupMenu);
mHelper.setForceShowIcon(true);
} catch (Exception e) {
e.printStackTrace();
}