Android事件分发机制二:viewGroup与view对事件的处理

本文详细介绍了Android事件分发机制,从MotionEvent的理解开始,包括ACTION_DOWN、ACTION_MOVE等事件类型,以及多点触控的处理。接着,重点讲解了ViewGroup如何分发事件,涉及到TouchTarget对象,事件拦截和子View的寻找。还探讨了View的事件处理,包括onTouchEvent方法和单击、长按事件的判断。最后,总结了事件分发的重要原则和作用。
摘要由CSDN通过智能技术生成

前言

很高兴遇见你~

在上一篇文章 Android事件分发机制一:事件是如何到达activity的? 中,我们讨论了触摸信息从屏幕产生到发送给具体 的view处理的整体流程,这里先来简单回顾一下:

  1. 触摸信息从手机触摸屏幕时产生,通过IMS和WMS发送到viewRootImpl
  2. viewRootImpl把触摸信息传递给他所管理的view
  3. view根据自身的逻辑对事件进行分发
  4. 常见的如Activity布局的顶层viewGroup为DecorView,他对事件分发方法进行了重新,会优先回调windowCallBack也就是Activity的分发方法
  5. 最后事件都会交给viewGroup去分发给子view

前面的分发步骤我们清楚了,那么viewGroup是如何对触摸事件进行分发的呢?View又是如何处理触摸信息的呢?正是本文要讨论的内容。

事件处理中涉及到的关键方法就是 dispatchTouchEvent ,不管是viewGroup还是view。在viewGroup中,dispatchTouchEvent 方法主要是把事件分发给子view,而在view中,dispatchTouchEvent 主要是处理消费事件。而主要的消费事件内容是在 onTouchEvent 方法中。下面讨论的是viewGroup与view的默认实现,而在自定义view中,通常会重写 dispatchTouchEvent 和 onTouchEvent 方法,例如DecorView等。

秉着逻辑先行源码后到的原则,本文虽然涉及到大量的源码,但会优先讲清楚流程,有时间的读者仍然建议阅读完整源码。

理解MotionEvent

事件分发中涉及到一个很重要的点:多点触控,这是在很多的文章中没有体现出来的。而要理解viewGroup如何处理多点触控,首先需要对触摸事件信息类:MotionEvent,有一定的认识。MotionEvent中承载了触摸事件的很多信息,理解它更有利于我们理解viewGroup的分发逻辑。所以,首先需要先理解MotionEvent。

触摸事件的基本类型有三种:

  • ACTION_DOWN: 表示手指按下屏幕
  • ACTION_MOVE: 手指在屏幕上滑动时,会产生一系列的MOVE事件
  • ACTION_UP: 手指抬起,离开屏幕

一个完整的触摸事件系列是:从ACTION_DOWN开始,到ACTION_UP结束 。这其实很好理解,就是手指按下开始,手指抬起结束。

手指可能会在屏幕上滑动,那么中间会有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP。

这是正常的情况,而如果出现了一些异常的情况,事件序列被中断,那么会产生一个取消事件:

  • ACTION_CANCEL:当出现异常情况事件序列被中断,会产生该类型事件

所以,完整的事件序列是:从ACTION_DOWN开始,到ACTION_UP或者ACTION_CANCEL结束 。当然,这是我们一个手指的情况,那么在多指操作的情况是怎么样的呢?这里需要引入另外的事件类型:

  • ACTION_POINTER_DOWN: 当已经有一个手指按下的情况下,另一个手指按下会产生该事件
  • ACTION_POINTER_UP: 多个手指同时按下的情况下,抬起其中一个手指会产生该事件

区别于ACTION_DOWN和ACTION_UP,使用另外两个事件类型来表示手指的按下与抬起,使得ACTION_DOWN和ACTION_UP可以作为一个完整的事件序列的边界 。

同时,一个手指的事件序列,是从ACTION_DOWN/ACTION_POINTER_DOWN开始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL结束。

到这里先简单做个小结:

触摸事件的类型有:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_DOWN、ACTION_POINTER_UP,他们分别代表不同的场景。

一个完整的事件序列是从ACTION_DOWN开始,到ACTION_UP或者ACTION_CANCEL结束。 一个手指的完整序列是从ACTION_DOWN/ACTION_POINTER_DOWN开始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL结束。


第二,我们需要理解MotionEvent中所携带的信息。

假如现在屏幕上有两个手指按下,如下图:

触摸点a  按下,而触摸点b  按下,那么自然而然就会产生两个事件:ACTION_DOWN和ACTION_POINTER_DOWN。那么是不是ACTION_DOWN事件就只包含有触摸点a的信息,而ACTION_POINTER_DOWN只包含触摸点b的信息呢?换句话说,这两个事件是不是会独立发出触摸事件?答案是:不是。

每一个触摸事件中,都包含有所有触控点的信息。例如上述的点b按下时产生的ACTION_POINTER_DOWN事件中,就包含了触摸点a和触摸点b的信息。那么他是如何区分这两个点的信息?我们又是如何知道ACTION_POINTER_DOWN这个事件类型是属于触摸点a还是触摸点b?

在MotionEvent对象内部,维护有一个数组。这个数组中的每一项对应不同的触摸点的信息,如下图:

数组下标称为触控点的索引,每个节点,拥有一个触控点的完整信息。这里要注意的是,一个触控点的索引并不是一成不变的,而是会随着触控点的数目变化而变化。例如当同时按下两个手指时,数组情况如下图:

而当手指a抬起后,数组的情况变为下图:

可以看到触控点b的索引改变了。所以跟踪一个触控点必须是依靠一个触控点的id,而不是他的索引 。

现在我们知道每一个MotionEvent内部都维护有所有触控点的信息,那么我们怎么知道这个事件是对应哪个触控点呢?这就需要看到MotionEvent的一个方法:getAction 。

这个方法返回一个整型变量,他的低1-8位表示该事件的类型,高9-16位表示触控点索引。我们只需要将这16位进行分离,就可以知道触控点的类型和所对应的触控点。同时,MotionEvent有两个获取触控点坐标的方法:getX()/getY() ,他们都需要传入一个触控点索引来表示获取哪个触控点的坐标信息。

同时还要注意的是,MOVE事件和CANCEL事件是没有包含触控点索引的,只有DOWN类型和UP类型的事件才包含触控点索引。这里是因为非DOWN/UP事件,不涉及到触控点的增加与删除。

这里我们再来小结一下:

  • 一个MotionEvent对象内部使用一个数组来维护所有触控点的信息
  • UP/DOWN类型的事件包含了触控点索引,可以根据该索引做出对应的操作
  • 触控点的索引是变化的,不能作为跟踪的依据,而必须依据触控点id

关于MotionEvent需要了解一个更加重要的点:事件分离。

首先需要知道事件分发的一个原则:一个view消费了某一个触点的down事件后,该触点事件序列的后续事件,都由该view消费 。这也比较符合我们的操作习惯。当我们按下一个控件后,只要我们的手指一直没有离开屏幕,那么我们希望这个手指滑动的信息都交给这个view来处理。换句话说,一个触控点的事件序列,只能给一个view消费。

经过前面的描述我们知道,一个事件是包含所有触摸点的信息的。当viewGroup在派发事件时,每个触摸点的信息就需要分开分别发送给感兴趣的view,这就是事件分离。

例如Button1接收了触摸点a的down事件,Button2接收了触摸点b的down事件,那么当一个MotionEvent对象到来时,需要将他里面的触摸点信息,把触摸点a的信息拆开发送给button1,把触摸点b的信息拆开发送给button2。如下图:

那么,可不可以不进行分离?当然可以。这样的话每次都把所有触控点的信息发送给子view。这可以通过FLAG_SPLIT_MOTION_EVENTS这个标志进行设置是否要进行分离。

小结一下:

一个触控点的序列一般情况下只给一个view处理,当一个view消费了一个触控点的down事件后,该触控点的事件序列后续事件都会交给他处理。

事件分离是把一个motionEvent中的触控点信息进行分离,只向子view发送其感兴趣的触控点信息。

我们可以通过设置FLAG_SPLIT_MOTION_EVENTS标志让viewGroup是否对事件进行分离


到这里关于MotionEvent的内容就讲得差不多,当然在分离的时候,还需要进行一定的调整,例如坐标轴的更改、事件类型的更改等等,放在后面讲,接下来看看ViewGroup是如何分发事件的。

ViewGroup对于事件的分发

这一步可以说是事件分发中的重头戏了。不过在理解了上面的MotionEvent之后,对于ViewGroup的分发细节也就容易理解了。

整体来说,ViewGroup分发事件分为三个大部分,后面的内容也会围绕着三大部分展开:

  1. 拦截事件:在一定情况下,viewGroup有权利选择拦截事件或者交给子view处理
  2. 寻找接收事件序列的控件:每一个需要分发给子view的down事件都会先寻找是否有适合的子view,让子view来消费整个事件序列
  3. 派发事件:把事件分发到感兴趣的子view中或自己处理

大体的流程是:每一个事件viewGroup会先判断是否要拦截,如果是down事件(这里的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),还需要挨个遍历子view看看是否有子view消费了down事件,最后再把事件派发下去。

在开始解析之前,必须先了解一个关键对象:TouchTarget。

TouchTarget

前面我们讲到:一个触控点的序列一般情况下只给一个view处理,当一个view消费了一个触控点的down事件后,该触控点的事件序列后续事件都会交给他处理。对于viewGroup来说,他有很多个子view,如果不同的子view接受了不同的触控点的down事件,那么ViewGroup如何记录这些信息并精准把事件发送给对应的子view呢?答案就是:TouchTarget。

TouchTarget中维护了每个子view以及所对应的触控点id,这里的id可以不止一个。TouchTarget本身是个链表,每个节点记录了子view所对应的触控点id。在viewGroup中,该链表的链表头是mFirstTouchTarget,如果他为null,表示没有任何子view接收了down事件。

TouchTarget有个非常神奇的设计,他只使用一个整型变量来记录所有的触控id。整型变量中哪一个二进制位为1,则对应绑定该id的触控点。

例如 00000000 00000000 00000000 10001000,则表示绑定了id为3和id为7的两个触控点,因为第3位和第7位的二进制位是1。这里可以间接说明系统支持的最大多点触控数是32,当然实际上一般是8比较多。当要判断一个TouchTarget绑定了哪些id时,只需要通过一定的位操作即可,既提高了速度,也优化了空间占用。

当一个down事件来临时,viewGroup会为这个down事件寻找适合的子view,并为他们创建一个TouchTarget加入到链表中。而当一个up事件来临时,viewGroup会把对应的TouchTarget节点信息删除。那接下来,就直接看到viewGroup中的dispatchTouchEvent 是如何分发事件的。首先看到源码中的第一部分:事件拦截。


事件拦截

这里的拦截分为两部分:安全拦截和逻辑拦截。

安全拦截是一直被忽略的一种情况。当一个控件a被另一个非全屏控件b遮挡住的时候,那么有可能被恶意软件操作发生危险。例如我们看到的界面是这样的:

但实际上,我们看到的这个按钮时不可点击的,实际上触摸事件会被分发到这个按钮后面的真正接收事件的按钮:

然后我们就白给了。这个安全拦截行为由两个标志控制:

  • FILTER_TOUCHES_WHEN_OBSCURED:这个标志可以手动给控件设置,表示被非全屏控件覆盖时,直接过滤掉所有触摸事件。
  • FLAG_WINDOW_IS_OBSCURED:这个标志表示当前窗口被一个非全屏控件覆盖。

具体的源码如下:

View.java api29
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    // 两个标志,前者表示当被覆盖时不处理;后者表示当前窗口是否被非全屏窗口覆盖
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

第二种拦截是逻辑拦截。如果当前viewGroup中没有TouchTarget,而且这个事件不是down事件,这就意味着viewGroup自己消费了先前的down事件,那么这个事件就无须分发到子view必须自己消费,也就不需要拦截这种情况的事件。除此之外的事件都是需要分发到子view,那么viewGroup就可以对他们进行判断是否进行拦截。简单来说,只有需要分发到子view的事件才需要拦截 。

判断是否拦截主要依靠两个因素:FLAG_DISALLOW_INTERCEPT标志和 onInterceptTouchEvent() 方法。

  1. 子view可以通过requestDisallowInterupt方法强制要求viewGroup不要拦截事件,viewGroup中会设置一个FLAG_DISALLOW_INTERCEPT标志表示不拦截事件。但是当前事件序列结束后,这个标志会被清除。如果需要的话需要再次调用requestDisallowInterupt方法进行设置。
  2. 如果子view没有强制要求不拦截,那么会调用onInterceptTouchEvent() 方法判断是否需要拦截。onInterceptTouchEvent方法默认只对一种特殊情况作了拦截。一般情况下我们会重写这个方法来拦截事件:
// 只对一种特殊情况做了拦截
// 鼠标左键点击了滑动块
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

viewGroup的 dispatchTouchEvent 方法逻辑中对于事件拦截部分的源码分析如下:

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    // 对遮盖状态进行过滤
    if (onFilterTouchEventForSecurity(ev)) {

        ...

        // 判断是否需要拦截
        final boolean intercepted;
        // down事件或者有target的非down事件则需要判断是否需要拦截
        // 否则不需要进行拦截判断,因为一定是交给自己处理
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            // 此标志为子view通过requestDisallowInterupt方法设置
            // 禁止viewGroup拦截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 调用onInterceptTouchEvent判断是否需要拦截
                intercepted = onInterceptTouchEvent(ev);
                // 恢复事件状态
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 自己消费了down事件,那么后续的事件非down事件都是自己处理
            intercepted = true;
        }
        ...;
    }
    ...;
}

寻找消费down事件的子控件

对于每一个down事件,不管是ACTION_DOWN还是ACTION_POINTER_DOWN,viewGroup都会优先在控件树中寻找合适的子控件来消费他。因为对于每一个down事件,标志着一个触控点的一个崭新的事件序列,viewGroup会尽自己的最大能力寻找合适的子控件。如果找不到合适的子控件,才会自己处理down事件。因为,消费了down事件,意味着接下来该触控点的事件序列事件都会交给该view消费,如果viewGroup拦截了事件,那么子view就无法接收到任何事件消息。

viewGroup寻找子控件的步骤也不复杂。首先viewGroup会为他的子控件构造一个控件列表,构造的顺序是view的绘制顺序的逆序,也就是一个view的z轴系数越高,显示高度越高,在列表的顺序就会越靠前。这其实比较好理解,显示越高的控件肯定是优先接收

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值