背景
activity上弹出全屏dialog,需要在dialog透明度为0的地方进行事件传递,即下方的activity响应事件。毫无疑问,事件分发。但是之前我们都是在同一个view树上进行递归分发,那么dialog和activity也是这样吗?
事件分发
于是,按照常规思路,dialog是依附在activity上展示的,那么只需要对dialog中rootview的onTouchEvent事件进行不拦截,返回false,最后会调用activity的onTouchEvent方法,代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
//伪代码
if (alpha <= threshold * 255 + 0.5) { //底部activity处理
return false;
}
return super.onTouchEvent(event);
}
在onTouchEvent方法中return false进行拦截,但是activity的onTouchevent并没有调用,这是为啥呢?
从事件分发机制说起,点击屏幕首先响应的是当前屏幕的顶层view,也就是Decorview,在Activity中就是Window的根布局,最后重新回到Decorview的super.dispatchTouchEvent()方法开始ViewGroup的事件传递,看下图
那么activity中无法获取dialog的点击事件,很明显是dialog并没有把事件传递过来,难道是Dialog的decorview和activity并不是同一个吗?
dialog & activity
我们看一下dialog的show方法
public void show() {
...
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
//步骤3
mWindowManager.addView(mDecor, l);
...
sendShowMessage();
}
构造函数
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//步骤1
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
//步骤2
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
可以看出,
//步骤1
Dialog创建时候,首先通过new Phonewindow()创建一个新的window,给window设置callback回调。
//步骤2
然后通过setWindowManager将这个window与WindowManager对象关联,这里和activity公用一个windowmanager。
//步骤3
通过window.getDecorview()方法创建自己的decorview,通过addView添加到步骤2中的windowmanager中,这样dialog就显示在了屏幕上。
通过上面的步骤可以看出,dialog使用了activity的WindowManager对象,但是新建了一个decorview,添加到了windowmanager中。
因此,Dialog和activity使用不同的window和decorview,所以点击dialog上的布局,是dialog的decorview进行响应,看一下decorview中的实现:
@Override
public boolean dispatchTrackballEvent(MotionEvent ev) {
//步骤1
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTrackballEvent(ev) : super.dispatchTrackballEvent(ev);
}
在步骤1中mWindow.callback变成了dialog中设置的对象,走到了dialog.dispatchTouchEvent方法,所以activity中并不会回调。
手动分发
看懂了原理恍然大悟,那么如果想让dialog的事件透传给activity,需要手动分发,让activity去响应事件。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (alpha <= threshold * 255 + 0.5) { //底部activity处理
//步骤2
val eventClone = MotionEvent.obtain(ev)
eventClone.offsetLocation(0f, StatusBarUtil.getStatusBarHeight(context).toFloat())
//步骤1
activity?.dispatchTouchEvent(eventClone)
}
return super.dispatchTouchEvent(event);
}
通过步骤1的方式,手动调用activity的dispatchTouchEvent(),就可以把事件传递到activity。
但是实际操作中发现,dialog中点击的位置和activity中响应的位置有偏差,这是因为dialog的view中不包含状态栏的高度,那么分发给activity的时候,需要把状态栏的高度加上,看步骤2:
通过MotionEvent的offsetLocation方法设置偏移像素即可。这样事件就成功传递给了activity。
总结
dialog和activity都有各自的decorview
,并不能直接进行事件传递,不走同一套事件分发逻辑。