序、慢慢来才是最快的方法。
Android 2020年面试系列(02 — View事件分发)_view事件分发 2020-CSDN博客
当我们触摸屏幕或者按键操作时,首先触发的是硬件驱动
驱动收到事件后,将相应事件写入到输入设备节点,这便产生了最原生态的内核事件
当屏幕被触摸,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event[x]目录下
这样做的目的是将输入事件封装为通用的Event,供后续处理。
我们知道,当系统启动时,在SystemServer进程会启动一系列系统服务,如AMS,WMS等。
其中还有一个就是我们管理事件输入的InputManagerService。这个服务就是用来负责与硬件通信,接受屏幕输入事件。
现在系统进程已经拿到输入事件了,但还需要传递给App进程,这就涉及到跨进程通信的部分
我们的App中的Window与InputManagerService之间的通信实际上使用的InputChannel
InputChannel是一个pipe,底层实际是通过socket进行通信。
我们知道在Activity启动时会调用ViewRootImpl.setView()
在ViewRootImpl.setView()过程中,也会同时注册InputChannel:
事件到达应用端的主线程,会通过ViewRootImpl进行一系列InputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。
我们的View触摸事件就发生在ViewPostImeInputStage阶段
可以看到事件分发经过了:DecorView -> Activity -> PhoneWindow -> DecorView
主要是为了解耦
ViewRootImpl并不知道有Activity这种东西存在!它只是持有了DecorView。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent()
因为Activity不知道有DecorView!但是,Activity持有PhoneWindow ,而PhoneWindow当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView。
在Android中,Activity并不知道自己的Window中有些什么,这样耦合性就很低了,Activity不需要知道Window中的具体内容
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = super.dispatchTouchEvent(event);
}
}
return isConsume;
}
如果是ViewGroup,会先执行到onInterceptTouchEvent方法判断是否拦截,如果拦截,则执行父类View的dispatchTouchEvent方法。
如果ViewGroup不拦截,则会传递到子View
如果不拦截,ViewGroup内主要做以下几件事
1. 遍历当前ViewGroup的所有子View
2. 判断当前View是否在当前子View的坐标范围内,不在范围内不能接收事件,直接跳过
3. 利用dispatchTransformedTouchEvent,如果返回true,则通过addTouchTarget对mFirstTouchTarget赋值
4. dispatchTransformedTouchEvent做的主要就是两个事,如果child不为null,则事件分发到child,否则调用super.dispatchTouchEvent,并最终返回结果
5. mFirstTouchTarget是单链表结构,记录消费链,但是在单点触控的时候这个特性没有用上,只是一个普通的TouchTarget对象
子View的diapatchTouchEvent逻辑比较简单
如果设置了setOnTouchListener并且返回为true,那么onTouchEvent就不再执行
否则执行onTouchEvent,我们常用的OnClickListenr就是在onTouchEvent里触发的
所以默认情况下会直接执行onTouchEvent,如果我们设置了setOnClickListener或者setLongClickListener,都会正常触发。
上面说了,如果子View消费事件,即dispatchTouchEvent方法返回true
表示这个事件我处理了,那么事件从此结束,ViewGroup的dispatchTouchEvent也返回true
子View不拦截事件,那么mFirstTouchTarget就为null,退出循环后,调用了dispatchTransformedTouchEvent方法。
最后回到Activity的dispatchTouchEvent,也是直接返回true
如果ViewGroup与子View都不拦截,即mFirstTouchTarget == null,dispatchTouchEvent也返回false
再看看Activity的源码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
答案很明显:会执行Activity的onTouchEvent方法
事件分发的本质就是一个递归方法,通过往下传递,调用dispatchTouchEvent方法,找到事件的处理者,这也就是项目中常见的责任链模式。
在分发过程中,ViewGroup通过onInterceptTouchEvent判断是否拦截事件
在分发过程中,View的默认通过onTouchEvent处理事件
如果底层View不消费,则默认一步步往上执行父元素onTouchEvent方法。
如果所有View的onTouchEvent方法都返回false,则最后会执行到Activity的onTouchEvent方法,事件分发也就结束了。
常见的滑动冲突解决方法有两种:
1. 外部拦截法
2. 内部拦截法
外部拦截法的原理很简单,就是通过我们上面分析的onInterceptTouchEvent进行。外部拦截法的模板代码如下:
内部拦截法是将主动权交给子View,如果子View需要事件就直接消耗,否则交给父容器处理
内部拦截法主要通过requestDisallowInterceptTouchEvent方法控制
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
//只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
}
}
}
如上所示,原理很简单
1. 子View通过requestDisallowInterceptTouchEvent控制mGroupFlags的值,从而控制disallowIntercept的值
2. disallowIntercept为true时就不会走到onInterceptTouchEvent,外部也就无法拦截了,当需要外部处理时,将disallowIntercept置为false即可
当然会有!所以 Android 为了避免每个事件都递归遍历,定义了一个 【事件序列】 的概念:将用户每一次触摸屏幕 --> 移动屏幕-->抬起手指称为一个事件序列。
一个事件序列必然包含 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等多个事件。其中 ACTION_MOVE 数量不确定,ACTION_DOWN 和 ACTION_UP 数量则为 1
当接收到 ACTION_DOMN 事件时,意味着一次完成事件序列的开始。ViewGroup 会通过递归遍历找到 View 树中真正对事件进行消费的子 View,并将其保存。这之后接收到 ACTION_MOVE 和 ACTION_DOWN 事件时,则跳过递归遍历的过程,直接交给之前保存的消费者。当下一次 ACTION_DOWN 事件来临时重置整个过程。
如果有 View 能够消费事件,那么该事件序列所有的后续事件都会交给这个 View 处理。但如果不希望它处理全部的后续事件怎么办?比如手指点击一个 Button 然后滑出它的边界。在这个事件序列中,我只希望 Button 处理它边界内的 move 事件。对于边界外的 move 事件,虽然它们都在一个事件序列中,但理论上不应该再传递给 Button 了。
ACTION_CANCEL 就是用来解决这个问题的。当 Button 判断 move 事件已经超出 view 的边界时,会将自己的 mPrivateFlags 置为 cancel 状态。等下次事件分发来临,Button 的父 ViewGroup 会检测 Button 的 mPrivateFlags,如果为 cancel 则将之前保存的 mFirstTouchTarget(也就是指向 Button 的引用) 设为 null,并向 Button 发送一个 ACTION_CANCEL 事件,表示以后不会再接受事件了。
参考:
【面试官爸爸】唠唠Android事件分发? - 掘金