关于事件分发
无非就是当用户触摸屏幕或者按键操作,首次触发的是硬件驱动,驱动收到事件后,将该相应事件写入到输入设备节点, 这便产生了最原生态的内核事件。接着,输入系统取出原生态的事件,经过层层封装后成为KeyEvent或者MotionEvent ;最后,交付给相应的目标窗口(Window)来消费该输入事件。
一组事件:从手指触摸屏幕开始,到手指离开屏幕结束。
从进程层面来看事件分发
早在16年,Gityuan大神就有写过关于Input系统的一些底层源码文章,详细的介绍了Android系统输入事件处理机制。
很明显,输入事件的产生和最终的消费,是分别存在于系统进程system_server和具体消费事件的App进程:
InputManagerService
系统进程SysterServer启动的时候,会开启核心服务以及其他各种服务,其中就包含InputManagerService,用来处理各种输入事件,比如触摸事件和按键事件,以及外设键入事件等。
可见,我们所关心触摸事件,只是众多输入事件的一部分。
InputChannle
InputChannle是指定用于在另一个进程中将输入事件发送到窗口的文件描述符。
InputChannle会创建socket pair,用于两个进程的线程间相互通信。
在Activity启动流程中,ActivityThread的handleResumeActivity会创建ViewRootImpl,并调用其setView方法。
ViewRootImpl.setView会创建InputChannle,并通过WindowInputEvent在native层初始化channle,同时注册监听事件。
ViewRootImpl中的InputChannle是socket的客户端,而系统服务IMS中获取到的,则是socket的服务端。
从应用内视图来看事件分发
对于应用内视图来说,事件分发的起点在Activity,并且经过一个递归循环之后,又在Activity中将最终事件处理结果返回。
由于这里已经属于老生常谈的点,也就不再进行大篇幅的代码分析,详细内容可移步Carson_Ho大神的Android事件分发机制详解:史上最全面、最易懂,这里仅引用几张图片,对Activity、ViewGroup和View各个层级,以及整体的工作流程进行一个总结:
Activity层面
ViewGroup层面
View层面
整体工作流程
从具体事件ACTION来看事件分发
在进行触摸事件分发时,针对不同的ACTION,会有不同的逻辑路线,大概可以拆分为三大类型:ACTION_DOWN、ACTION_MOVE&ACTION_UP以及ACTION_CANCLE。这三种类型的事件,会有不同的逻辑走线:
ViewGroup中分发逻辑走线
View中分发逻辑走线
事件分发关键点
ACTION_DOWN的时候不举手,这组事件就没机会了
从上图中可以看出,ViewGroup在分发当前这组事件时,只有在ACTION_DOWN的时候,才会去寻找TouchTarget,也就是想要处理这组事件的子View。所以如果子View不在ACTION_DOWN的时候“举手”,那么这组事件它就没有机会处理了。
ViewGroup寻找TouchTarget
在一组事件开始分发时,ViewGroup会首先寻找TouchTarget,如果找到了, 那么这组事件就交个这个TouchTarget处理。ViewGroup在寻找TouchTarget时,通过如下几个环节来筛选最终的子View:
这里需要注意两点:
后添加的子View会先收到事件,也就是越上层的View越先收到
变更子View的Z轴偏移,会影响接收事件的先后顺序,Z值越大越先接收
关于FLAG_DISALLOW_INTERCEPT
每当ACTION_DOWN触发时,会reset此flag,保证ViewGroup的onInterceptTouchEvent可以在每一组事件中都有机会被调用到
子View在处理当前这组事件时,可以适时的关闭父View对当前这组事件后续事件的拦截:
//禁止拦截
getParent().requestDisallowInterceptTouchEvent(true)
//打开拦截
getParent().requestDisallowInterceptTouchEvent(false)
复制代码
ViewGroup中的super.dispatchTouchEvent()
通过之前的图解可以看出,当没有找到touchTarget或者ViewGroup决定拦截时,ViewGroup会从View的角度,去考虑要不要自己消费这个事件。
View.canReceivePointerEvents
在ViewGroup遍历寻找TouchTarget过程中,这个方法也起到了决定性的作用,具体来看一下这个方法:
/**
* @hide
*/
protected boolean canReceivePointerEvents() {
//可见或当前执行动画Animation不为null
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
复制代码
所以在动画执行完毕时,应该适时的清除view的动画view.clearAnimation()。
多点触控的处理
在某些场景下,用户可以通过多个手指进行交互操作,这就需要在这个场景下,合理的处理用户的多点触控行为。
TouchTarget
在进行多点触控分析前,首先来看一下TouchTarget:
private static final class TouchTarget {
//省略部分代码
// 保存的子View引用,
@UnsupportedAppUsage
public View child;
// 当前子View追踪手指的掩码
public int pointerIdBits;
// 指向下一个TouchTarget
public TouchTarget next;
//省略部分代码
复制代码
在TouchTarget中保存着消费事件的子View,以及这个子View目前追踪的手指ids(可为多个手指),还有一个next指向下一个TouchTarget,那么下一个TouchTarget又是什么意思呢?
我们知道,顶层ViewGroup会在手指按下的时候,reset一些属性同时还会寻找TouchTarget。reset属性时,需要事件只能是ACTION_DOWN,但是寻找TouchTarget却还允许触摸事件为ACTION_POINTER_DOWN。
ACTION_POINTER_DOWN,表示当手指按下时,已经存在其他手指在事件序列中,即还没有抬起。那么就可能存在这么一种情况,第一个手指按在了一个Button上,但是第二根手指按在了另一个Button上,那么当第二根手指按下时,就会寻找到第二个TouchTarget,这个时候就会将这个新的TouchTarget.next指向mFirstTouchTarget(即第一个),然后将mFirstTouchTarget指向这个新的TouchTarget。
如果你现在是使用的掘金APP,那么可以试一下长按右上角会弹出分享,然后别松开,再长按文章内容区域,会再弹出menu。
MotionEvent.getActionMask
MotionEvent就是事件分发时的主角,只有了解了它才能正确的处理触摸事件。
请先查看GcsSloop大佬的MotionEvent详解。
获取事件类型
1.getAction:单点触控触摸事件
2.getActionMask:多点触控必须使用此方法获取Action
追踪事件时,应该瞅准PointerId
int index = event.getActionIndex()
int pointerId = event.getPointerId(index)
复制代码
多指按下和抬起时,每个手指对应的pointerId是固定不变的,而对应的index会发生变化,变化的规则是保证index是连续的。
多指按下时:
多指抬起时:
多点触控处理方案
接力型
同一时刻只追踪一个手指(如始终追踪最新按下的手指)的运动轨迹。
协作型
同时追踪所有触摸手指运动轨迹,判断用户行为(捏撑缩放、多指平移等)。
各自为栈
同时追踪所有触摸手指运动轨迹,但是互不影响(如每个手指都能画画)。
ViewGroup.setMotionEventSplittingEnabled(false)
此方法可以关闭被调用ViewGroup对多点触控的支持,再来看一下ViewGroup的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
//省略部分代码
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
//省略部分代码
if (actionMasked == MotionEvent.ACTION_DOWN
//当ACTION_POINTER_DOWN发生时,必须split为true,才会发起对TouchTarget的寻找
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//省略部分代码
findTouchTarget
}
}
复制代码
GestureDetector更便捷的处理用户触摸事件
使用手势监听器,可以更加便捷的处理用户手势,详细的内容可以移步Carson_Ho大佬博文Android GestureDetector详解。