前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android高频面试专题 - 提升篇(三)事件分发机制

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

作者头像
Android扫地僧
发布2020-03-19 15:51:06
2.2K0
发布2020-03-19 15:51:06
举报
文章被收录于专栏:Android进阶Android进阶

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

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

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

  • MotionEvent.ACTION_DOWN:用户触摸View&ViewGroup。
  • MotionEvent.ACTION_MOVE:用户手指移动View&ViewGroup。
  • MotionEvent.ACTION_UP:用户手指离开屏幕。
  • MotionEvent.ACTION_CANCEL:事件退出了,不是用户导致的。

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

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继承而来。

代码语言:javascript
复制
//View.javapublic final boolean dispatchPointerEvent(MotionEvent event) {    if (event.isTouchEvent()) {        return dispatchTouchEvent(event);    } else {        return dispatchGenericMotionEvent(event);    }}

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

代码语言:javascript
复制
//DecorView.java@Overridepublic 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中又是怎样处理的

代码语言:javascript
复制
//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。

代码语言:javascript
复制
@Overridepublic 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个重要的方法:

  • dispatchTouchEvent

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

  • onInterceptTouchEvent

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

  • onTouchEvent

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

代码语言:javascript
复制
//事件分发机制伪代码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个方法返回值的作用

  • dispatchTouchEvent:方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。
  • onInterceptTouchEvent:方法返回值为true表示拦截这个事件并交由自身的onTouchEvent方法进行消费;返回false表示不拦截,需要继续传递给子视图。如果return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:   1.如果该View存在子View且点击到了该子View, 则不拦截, 继续分发 给子View 处理, 此时相当于return false。 2.如果该View没有子View或者有子View但是没有点击中子View(此时ViewGroup 相当于普通View), 则交由该View的onTouchEvent响应,此时相当于return true。

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

  • onTouchEvent:方法返回值为true表示当前视图可以处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。如果return super.onTouchEvent(ev),事件处理分为两种情况: 1.如果该View是clickable或者longclickable的,则会返回true, 表示消费 了该事件, 与返回true一样; 2.如果该View不是clickable或者longclickable的,则会返回false, 表示不 消费该事件,将会向上传递,与返回false一样。

8、几个重要结论

  • 同一次触摸事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。
  • 正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考3,因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  • 某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。
  • 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  • ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
  • View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
  • View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  • onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

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

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

滑动冲突的处理规则:

  • 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
  • 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件,何时由内部View拦截事件。
  • 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。

滑动冲突的实现方法:

  • 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
  • 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android扫地僧 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档