现在一个普通activity页希望对话框弹出来之后, 空白区域仍然能进行滑动点击等操作,也就是希望能够透传给下面的activity, 同时原有的在对话框视图上的各种点击和滑动操作也不应该受到影响. 这个需求听起来多余且棘手, 对话框弹出的目的就是为了强化提醒和屏蔽操作,现在竟然要去除那就失去了使用对话框的意义了, 然而iOS竟然可以做到!所以不得不硬着头皮看源码实现一下.
众所周知安卓中的Dialog
是在另外一个Window
实例中添加的视图,比Activity
所在的Widnow
层级要高. 如果仅仅是为了达到效果,如上所说其实就根本不应该用Dialog! 能够想到的控件当然是PopupWindow
了,但改成PopupWindow
之后它的层级就放在了Activity所在的Window了, 层级一低会破坏原有程序实现的很多case,而且可能出现测试覆盖不到的情况增加线上风险, 所以很多实现的麻烦并不是实现本身,而且是涉及太多的程序上下文.
只能针对Dialog进行修改了,这篇帖子给我们一个启发(虽然帖子本身没人回答), 那就是利用Activity.dispatchTouchEvent
! 把没有消费的事件整个的交给activity实例去处理,这样即能透传事件也不影响原有对话框视图的各种操作! 那么Touch事件又从何而来? 这就得了解Dialog的实现机制了,先看Dialog创建时的源码:
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
复制代码
Dialog
创建了一个Window
的实例, 最关键的是w.setCallback(this)
, window的所有回调都传给了dialog对象,其中就有public boolean dispatchTouchEvent(MotionEvent event);
, 同时各种dialog的子类AppCompatDialog
, AlertDialog
都有可覆盖的方法: public boolean onTouchEvent(@NonNull MotionEvent event)
, 这就h了, 只要在dialog的子类持有activity实例, 再把没有消费的事件直接传送给activity不就完事了! 一个小的delegate而已, 于是有:
class YourDialog(context: Context) : AlertDialog(context, resolveDialogTheme(context, 0)) {
private val activity = if (context is Activity) context else null
companion object {
// Copy from AlertDialog.resolveDialogTheme
fun resolveDialogTheme(context: Context, @StyleRes resid: Int): Int {
// Check to see if this resourceId has a valid package ID.
return if (resid ushr 24 and 0x000000ff >= 0x00000001) { // start of real resource IDs.
resid
} else {
val outValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.alertDialogTheme, outValue, true)
outValue.resourceId
}
}
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
return super.onTouchEvent(ev) || passThrough(ev)
}
private fun passThrough(ev: MotionEvent): Boolean {
return activity?.dispatchTouchEvent(ev) ?: false
}
}
复制代码
resolveDialogTheme
这个方法是为了获取Dialog对应主题,因为声明成package access只能copy过来;- 另一个需要注意的问题是activity的实例不能从dialog的context获取, 它的实际类型是
ContextThemeWrapper
; - 第三透传给activity的是dialog没有消费的事件,所以
onTouchEvent
返回false才调用passThrough
. - 最后 也是特别需要注意的 需要设置对话框的2个方法:
setCanceledOnTouchOutside(false)
setCancelable(false)
复制代码
如果没有这2行会发生什么? 实际运行一下就会发现最后2个问题其实是一个问题
这个实现简单安全甚至还带着几分优雅~