Dialog事件穿透解析

背景

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的事件传递,看下图

img

那么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,并不能直接进行事件传递,不走同一套事件分发逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex_ChuTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值