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事件是怎么回事呢?请看下面的图你就懂的更彻底了:
image2、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。
篇幅原因,这里不贴细节源码,我们在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个重要的方法:
- dispatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。
- onInterceptTouchEvent
在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
- onTouchEvent
同样也会在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个方法返回值的作用
-
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方法。
在这我也分享一份自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点
总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
image.png扫码领取资源.png关注微信公众号“Android扫地僧”,回复【资源】,即可获取下载地址