每日一问 为什么 Dialog 弹出后 Activity 就无法响应用户事件了?

这个问题很简单,这个是因为Dialog是Focusable的。如果不是Focusable的,那么Dialog 弹出后 Activity 还是可以响应用户事件的。

首先要明确一个概念是窗口是事件分发的基本单位,而不是activity或者应用。系统的事件总是要先分给窗口,决定要分给哪个窗口是在系统的InputDispatcher.cpp这个类中。

对于touch事件,还要再由窗口交给DecorView在控件树中进行分发,最终到达某个View。但在此之前,会交由activity进行处理一下。

可以用以下demo测试一下:

view = new MyButton(this);
getWindowManager().addView(view, getWindowParams());

    private WindowManager.LayoutParams getWindowParams(){
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.format = PixelFormat.TRANSLUCENT;// 支持透明
        /*这种接收时间的范围大小是整个屏幕*/
//        params.flags |= WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;// 接受窗口外事件
        /*这个接收事件的范围是自己设置的大小*/
        params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.width = 490;//窗口的宽和高
        params.height = 160;
        params.x = 0;//窗口位置的偏移量
        params.y = 50;
        params.setTitle("MyDialog");
        params.gravity = Gravity.START|Gravity.TOP;
        return params;
    }

    public void dismiss(){
        getWindowManager().removeView(view);
    }

    class MyButton extends AppCompatButton {

        private TestWmsActivity activity;
        public MyButton(TestWmsActivity context) {
            super(context);
            activity = context;
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.d(Config.TAG, "onTouchEvent: " + event.getAction());
            activity.dismiss();
            return true;

        }

        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            Log.d(Config.TAG, "onKeyDown: " + keyCode);
            activity.dismiss();
            return true;
        }
    }

getWindowManager().addView 这里的添加view其实是添加一个子窗口。添加完MyDialog以后,我们的activity一共就有两个窗口了,另外一个是默认生成的子窗口。这一点可以通过adb shell dumpsys window visible来查看:

Window #6 Window{57cc6f1 u0 MyDialog}:
   ...
Window #7 Window{a38a957 u0 com.example.xcm.demo/com.example.xcm.demo.wms.TestWmsActivity}:
   ...

主窗口的名字一般就是activity的ComponentName。子窗口的名字如果不调用params.setTitle()进行设置,那么默认也是所属activity的ComponentName,只是前面的地址不一样而已。

可以看到,子窗口是在主窗口上面,即子窗口的z序是小于主窗口的。

我们的MyDialog 没有加上params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 这句话的时候,子窗口的touch区域其实是整个Frame,也就是主窗口完完全全被子窗口挡住了,自然无法响应用户事件。这个说法有什么证据吗?可以通过adb shell dumpsys input 来查看一下:

在Input Dispatcher State:一栏中,有以下信息:

  6: name='Window{e3f21ef u0 MyDialog}',  paused=false, hasFocus=true visible=true, canReceiveKeys=true frame=[590,1127][1080,1287], **touchableRegion=[0,0][1080,2340]**,
            7: name='Window{d68c785 u0 com.example.xcm.demo/com.example.xcm.demo.wms.TestWmsActivity}', paused=false, hasFocus=false,  visible=true, canReceiveKeys=true, frame=[0,0][1080,2340], **touchableRegion=[0,0][1080,2340]**

可以看到虽然子窗口的看起来只有 frame=[590,1127][1080,1287] 这么大,但其touchableRegion=[0,0][1080,2340]却是全屏的,和主窗口一样大,又在主窗口上面,于是挡住了主窗口;所以主窗口,即大家经常说的acitivity,无法接收到touch事件。只有当子窗口接受到事件dismiss以后,才能接收到事件。

当MyDialog 加上params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 这句话的时候 ,它的touch区域发生了变化。

6: name='Window{e3f21ef u0 MyDialog}',  paused=false, hasFocus=false visible=true, canReceiveKeys=false frame=[590,1127][1080,1287], **touchableRegion=[590,1127][1080,1287]**,

现在子窗口看起来多大,能接受touch事件的区域也就多大了,其余区域不会在阻拦主窗口的事件。而且现在子窗口无法接受key事件,canReceiveKeys=false。

最后,为什么谷歌要这么做?原因不是很清楚,可以猜测一下:如果一个窗口是Focusable的,那么其是要响应全局事件的,因为Focusable对应着key事件,意味可以接受key事件,而key事件是全局的(没有一个具体的区域,touch事件是有局域的,就是touchableRegion),所以为了统一,也把touchableRegion改成了全屏。如果你调反过来,如果一个子窗口是不能接受key事件的,那么它实际上多大,它的touchableRegion也就是多大。
总结:

因为Android中特意淡化了Window的概念,所以可能很多同学只知道activity而不知道Window。如果弄清楚了Window在事件分发中的作用,这个问题还是容易理解的。

结论就是子窗口弹出了以后,因为它在窗口的上面,而且他的touchableRegion是全屏,挡住了主窗口,所以系统把touch事件都发给子窗口了,主窗口无法响应。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容