android 分发事件,Android必知必会——事件分发机制

关于事件分发

无非就是当用户触摸屏幕或者按键操作,首次触发的是硬件驱动,驱动收到事件后,将该相应事件写入到输入设备节点, 这便产生了最原生态的内核事件。接着,输入系统取出原生态的事件,经过层层封装后成为KeyEvent或者MotionEvent ;最后,交付给相应的目标窗口(Window)来消费该输入事件。

一组事件:从手指触摸屏幕开始,到手指离开屏幕结束。

从进程层面来看事件分发

早在16年,Gityuan大神就有写过关于Input系统的一些底层源码文章,详细的介绍了Android系统输入事件处理机制。

很明显,输入事件的产生和最终的消费,是分别存在于系统进程system_server和具体消费事件的App进程:

99a3d5e2b88dde8c37b1f0f825db2437.png

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层面

d90123af05dfe52d2046db5cca1d1f9d.png

ViewGroup层面

c84b45b7e086638a730f6095b4bdefc6.png

View层面

40579f0a0b3789ca1687237636532b30.png

整体工作流程

408821955fd6112ed1670b9ab350bdb4.png

从具体事件ACTION来看事件分发

在进行触摸事件分发时,针对不同的ACTION,会有不同的逻辑路线,大概可以拆分为三大类型:ACTION_DOWN、ACTION_MOVE&ACTION_UP以及ACTION_CANCLE。这三种类型的事件,会有不同的逻辑走线:

ViewGroup中分发逻辑走线

3703b00b814855de98c2abbd60dc734e.png

View中分发逻辑走线

8303777adc28fe05d1ec1cbb8f2df9f2.png

事件分发关键点

ACTION_DOWN的时候不举手,这组事件就没机会了

从上图中可以看出,ViewGroup在分发当前这组事件时,只有在ACTION_DOWN的时候,才会去寻找TouchTarget,也就是想要处理这组事件的子View。所以如果子View不在ACTION_DOWN的时候“举手”,那么这组事件它就没有机会处理了。

ViewGroup寻找TouchTarget

在一组事件开始分发时,ViewGroup会首先寻找TouchTarget,如果找到了, 那么这组事件就交个这个TouchTarget处理。ViewGroup在寻找TouchTarget时,通过如下几个环节来筛选最终的子View:

3be1fba86fc4e6d897c9b0d1936e0bc2.png

这里需要注意两点:

后添加的子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是连续的。

多指按下时:

4b338539e02395cb787f1a2da1c3e294.png

多指抬起时:

ff291bb4c52bbf57830ff4a2fc2711ca.png

多点触控处理方案

接力型

同一时刻只追踪一个手指(如始终追踪最新按下的手指)的运动轨迹。

协作型

同时追踪所有触摸手指运动轨迹,判断用户行为(捏撑缩放、多指平移等)。

各自为栈

同时追踪所有触摸手指运动轨迹,但是互不影响(如每个手指都能画画)。

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详解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值