转载

PopupWindow的基本使用二

上一篇介绍了PopupWindow的创建和显示,这一篇介绍一下几个比较常用方法,并借助源码解释几个使用过程中比较常见的几个问题,然后对ListPopupWindow和PopupMenu的使用进行简单介绍。主要涉及到下面三个方法的使用:

setOutsideTouchable(boolean touchable)
setFocusable(boolean focusable)
setBackgroundDrawable(Drawablebackground)

部分方法使用介绍

setOutsideTouchable(boolean touchable)

该方法只有在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的基本使用二 PopupWindow的基本使用二

setFocusable(boolean focusable)

该方法非常重要,不但会影响PopupWindow中View事件的执行,还会影响系统返回键对PopupWindow的处理。

在PopupWindow弹出来的时候,我们点击返回键并不想返回上一页而是直接隐藏弹框,如果不设置该属性,我们点击返回键,就会直接返回到上一层级,部分版本还需要使用setBackgroundDrawable设置背景。

PopupWindow的基本使用二 PopupWindow的基本使用二

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) {
 //无响应
 }
});

setBackgroundDrawable(Drawable background)

该方法就是设置一个背景图片,但是它所影响的不仅仅是有无背景这样简单而已。有时候操作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

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

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();
}
原文  http://www.sunnyang.com/634.html
正文到此结束
Loading...