Android进阶之路Android开发Android开发经验谈

Android高频面试专题 - 提升篇(三)事件分发机制

2020-03-02  本文已影响0人  Android扫地僧

关于事件分发机制的流程,网上博客已经讲烂了。但是对于这个流程,还是建议大家都自己亲自动手,跟着源码走一遍,不然面试官一问,Activity中,dispatchTouchEvent(event)中的MotionEvent是哪里来的,还不一下就露馅了?

1、事件分发机制分发的是什么

当用户点击屏幕里View或者ViewGroup的时候,将会产生一个事件对象,这个事件对象就是MotionEvent对象,这个对象记录了事件的类型,触摸的位置,以及触摸的时间等。MotionEvent里面定义了事件的类型,其实很容易理解,因为用户可以在屏幕触摸,滑动,离开屏幕动作,分别对应:

因此用户在触摸屏幕到离开屏幕会产生一系列事件,ACTION_DOWN->ACTION_MOVE(0个或者多个)->ACTION_UP,那么ACTION_ CANCEL事件是怎么回事呢?请看下面的图你就懂的更彻底了:

image

2、ACTION_CANCEL什么时候触发

如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件。常见场景就是ListView中Item内部有一个Button,我们让ACTION_DOWN落在这个Button上,然后上下滑动,此时MOVE事件就会被ListView拦截,那么Button就会收到ACTION_CANCEL事件了。

3、MotionEvent在哪里产生

我们知道,触摸屏幕,首先肯定是硬件产生的一个电信号,但是我们能接触到的触摸事件直接就到了MotionEvent,那么这个MotionEvent在哪里产生?其实是在framework层做的处理,如果不做系统应用开发,基本上接触不到framework的。屏幕对应Android来说,担任了键盘的作用,就是我们计算机组成的输入设备,我们知道Android是基于Linux系统的,当我们的输入设备可用时(我们这里只来讲解触摸屏),我们对触摸屏进行操作时,Linux就会收到相应的硬件中断,然后将中断加工成原始的输入事件并写入相应的设备节点中。而我们的Android 输入系统所做的事情概括起来说就是监控这些设备节点,当某个设备节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中找到合适的事件接收者,并派发给它。这里所说的Android输入系统,就是InputManagerService(IMS),它和我们熟知的ActivityManagerService(AMS)一样,作为系统服务,都是在SystemServer中创建。

前面我们讲过,Activity、Window和View之间的关系,我们知道,我们的Activity创建是,会创建对应的PhoneWindow,创建完成之后,我们也在该Window上注册了InputChannel并与IMS通信,IMS把事件写入InputChannel,WindowInputEventReceiver对事件进行处理并最终还是通过InputChannel反馈给IMS。

具体细节:https://segmentfault.com/a/1190000012227736

篇幅原因,这里不贴细节源码,我们在ViewRoot调用setView时,会创建WindowInputEventReceiver(简称receiver),IMS写入事件时,receiver就会回调onInputEvent(InputEvent event, int displayId),这个时候我们收到的还是InputEvent,最后交由processPointerEvent()方法处理,这个方法内部会将InputEvent强转成MotionEvent(继承自InputEvent),然后调用mView.dispatchPointerEvent(event), 由于都是ViewRoot的内部类,这里的mView其实就是DecorView了,而DecorView的dispatchPointerEvent直接是从View继承而来。

//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

这里又直接调用了dispatchTouchEvent(event),而DecorView又重写了这个方法。

//DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

可以看到,这里最终又是交由Window.Callback来进行分发,实际上这里的callback就是Activity,在Activity的attach()方法中,会通过mWindow.setCallback(this), 毫无疑问,Activity肯定是实现了Window.Callback这个接口的,至此,MotionEvent传递到了Activity,也就是调用了Activityity.dispatchTouchEvent()。

4、MotionEvent的传递顺序

从上面可以看到,MotionEvent最开始是从DecorView传递到Activity的,那么Activity中又是怎样处理的

//Activity.java
 public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //在这里我们又把事件给了PhoneWindow.superDispatchTouchEvent方法根据其返回值,
    //若返回值为true,那么dispatchTouchEvent返回true,我们Activity的onTouchEvent方法无法得到执行
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //这里就是我们的Activity的onTouchEvent方法
    return onTouchEvent(ev);
}

Activity又调用了getWindow().superDispatchTouchEvent(ev)也就是PhoneWindow。

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //兜兜转转一大圈,还是把事件交给我们的DecorView,
    //DecorView继承自FrameLayout,FrameLayout呢又继承自ViewGroup,
    //所以作为一个ViewGroup,DecorView继续向其子View派发事件,其流程我在文章的开头就已经给了
    return mDecor.superDispatchTouchEvent(event);
}

这里又调用了DecorView的superDispatchTouchEvent(event),这里面其实就是直接继承自ViewGroup的dispatchTouchEvent(MotionEvent ev)方法,也就是说,事件从DecorView传递到Activity,最终又回到DecorView,最后按照分发机制分发到ViewGroup再到所有的子View。

所以完整的事件分发顺序应该是IMS→WindowInputEventReceiver(ViewRoot)→DecorView→Activity→DecorView→ViewGroup→View

是不是豁然开朗,网上的博客都只告诉你,事件分发从Activity开始,原来并不是从Activity开始的。

5、事件分发流程

事件分发机制使用的是责任链设计模式,从Activity如果传到最下层的View都没有组件处理该事件,该事件会依次回传到Activity。这里面就涉及到3个重要的方法:

用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。

在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。​

同样也会在dispatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

//事件分发机制伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;//记录返回值
    if(onInterceptTouchEvent(ev)){//判断是否拦截此事件
        consume = onTouchEvent(ev);//如果当前确认拦截此事件,那么就处理这个事件 
    }else{
        consume = child.dispatchToucnEvent(ev);//如果当前确认不拦截此事件,那么就将事件分发给下一级
    }
    return consume;
}

这段经典的伪代码,就可以诠释整个分发过程:对于一个根ViewGroup而言,点击事件产生后,首先会传递给它,这时它的dispatchTouch就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。

6、onTouchListener,onTouchEvent和onClick的优先级别

这个从View的onTouchEvent源码可以看到整个过程,如果mTouchListener.onTouch()方法返回true,那么事件就会被onTouchListener.onTouch消费掉,而onClick是在onTouchEvent()的ACTION_UP中处理的,所以优先级是onTouchListener>onTouchEvent>onclick

7、事件分发3个方法返回值的作用

注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,得看具体情况。

8、几个重要结论

9、如何解决View的事件冲突?****举个开发中遇到的例子?

常见开发中事件冲突的有ScrollView与RecyclerView的滑动冲突、RecyclerView内嵌同时滑动同一方向。

滑动冲突的处理规则:

滑动冲突的实现方法:


在这我也分享一份自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点

总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

image.png

关注微信公众号“Android扫地僧”,回复【资源】,即可获取下载地址

扫码领取资源.png
上一篇下一篇

猜你喜欢

热点阅读