当初一个一般 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 个问题其实是一个问题
这个实现简略平安甚至还带着几分优雅~
———- 0628
果然还是有问题!
发现滚动是好的, 然而点击操作有偏移! y 坐标总是偏移了一个 statusbarHeight
的间隔, 传到上层的 Activity 就是点击的地位向上偏了, 尽管能够获取到 statusbar 高度并且强行批改 MotionEvent
的(x, y)
信息, 但底层的 activity 也有可能是是 FULLSCREEN
的, 兴许会引入问题, 最终发现咱们理论须要的 y 就是 MotionEvent
的rawY
, 于是再创立一个新的 MotionEvent
对象传入就 ok, 于是有:
private fun passThrough(ev: MotionEvent): Boolean {- return activity?.dispatchTouchEvent(ev) ?: false
+ val e = MotionEvent.obtain(ev.downTime, ev.eventTime, ev.action,
+ ev.rawX, ev.rawY,
+ ev.pressure, ev.size, ev.metaState, ev.xPrecision, ev.yPrecision, ev.deviceId, ev.edgeFlags)
+ return activity?.dispatchTouchEvent(e) ?: false
}
原始的地位信息 (ev.rawX, ev.rawY)
(而不是(ev.x, ev.y + statusBarHeight)
) 就是咱们须要透传给 activity 的地位信息, 让 activity 本人决定要如何解决触摸, 这样防止了地位信息的强行批改.
另外一个问题: 当点击上层 activity 视图上的输入框时, 输入法无奈弹起!
这个有点辣手, 不过个别输入法不弹是因为没有获取到焦点 (没有其它谬误的状况下), 那有让 dialog 去除焦点或者禁止获取焦点的办法么? 十分侥幸的是咱们有一个针对Window
的 flag: FLAG_NOT_FOCUSABLE
, 于是在创立对话框后获取到它的 window 并设置:
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
如此, 输入法果然依照预期弹起了~!