PopupWindow的基本使用二

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

setOutsideTouchable(boolean touchable)
setFocusable(boolean focusable)
setBackgroundDrawable(Drawable background)

部分方法使用介绍

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方法有关,下文会借助源码做一下分析。

setFocusable(boolean focusable)

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

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

PopupWindow弹出来多数情况我们需要在弹框内处理一些逻辑,如果不设置focusable为true,会导致弹框中所有View的事件无响应。例如我们想弹出一个列表,列表使用的是ListView,这会导致ListView中onItemClick事件不起作用。

layout_popup.xml布局文件如下:

<LinearLayout xmlns: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部分代码如下:

View view=View.inflate(context, R.layout.layout_popup,null);
popupWindow.setFocusable(false);//focusable为false
ListView listView= (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, View view, int position, long id) {
		//无响应
	}
});

setBackgroundDrawable(Drawable background)

该方法就是设置一个背景图片,但是它所影响的不仅仅是有无背景这样简单而已。有时候操作PopupWindow其它区域,但是PopupWindow并没有隐藏多数是该方法没有使用导致的,当然了本质上还是Android版本差异性导致的,下面会结合源代码分析一下。另外网络中有许多文章说PopupWindow是线程阻塞的,而AlertDialog不是线程阻塞的,个人认为这种情况也是该方法导致的,并不是所谓的PopupWindow线程阻塞的控件。

setBackgroundDrawable传入的是一个Drawable,可以使用BitmapDrawable或者ColorDrawable,每次PopupWindow需要显示的时候,不管是在showAsDropDown还是showAtLocation方法都有一个preparePopup方法。

public void showAsDropDown(View anchor, 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
		PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
		PopupViewContainer.LayoutParams listParams = 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(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);
	}

	//touch事件处理
	@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);
		}
	}
}

所有的事件接收都在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(KeyEvent event) {
		//...
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		//...
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		//...
	}
}

由于Android 版本差异性,所以在开发的时候建议设置一下背景,如果不需要背景设置透明即可setBackgroundDrawable(new ColorDrawable(Color.parseColor("#00000000")))

为什么必须设置宽高

无论是从setWidth还是从构造方法中都是赋值mWidth或者mHeight,而这两个属性就是PopupWindow弹框View的高宽。

public void setWidth(int width) {
	mWidth = width;
}
public PopupWindow(View contentView, 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.LayoutParams createPopupLayoutParams(IBinder token) {
	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时候溢出菜单类似,内部默认不支持图标,但是我们可以使用反射强制让菜单显示图标。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/action_edit"
        android:icon="@drawable/ic_edit_black_24dp"
          android:title="@string/popup_menu_edit" />
    <item android:id="@+id/action_delete"
          android:title="@string/popup_menu_delete" />
    <item android:id="@+id/action_ignore"
          android:title="@string/popup_menu_ignore" />
    <item android:id="@+id/action_share"
          android:title="@string/popup_menu_share">
        <menu>
            <item android:id="@+id/action_share_email"
                  android:title="@string/popup_menu_share_email" />
            <item android:id="@+id/action_share_circles"
                  android:title="@string/popup_menu_share_circles" />
        </menu>
    </item>
</menu>
popupMenu = new PopupMenu(this, btn);
final MenuInflater menuInflater = popupMenu.getMenuInflater();
menuInflater.inflate(R.menu.popup_menu, popupMenu.getMenu());

//使用反射强制显示icon
try {
	Field field = popupMenu.getClass().getDeclaredField("mPopup");
	field.setAccessible(true);
	MenuPopupHelper mHelper = (MenuPopupHelper) field.get(popupMenu);
	mHelper.setForceShowIcon(true);
} catch (Exception e) {
	e.printStackTrace();
}

评论

您确定要删除吗?删除之后不可恢复