当初一个一般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而已, 于是有:
<code class="kotlin">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, 于是有:
<code class="diff"> 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)
如此,输入法果然依照预期弹起了~!