上一篇介绍了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(); }