GVKun编程网logo

COCOS-3.X事件分发机制-原理(事件分发流程)

8

在本文中,我们将为您详细介绍COCOS-3.X事件分发机制-原理的相关知识,并且为您解答关于事件分发流程的疑问,此外,我们还会提供一些关于Android事件分发机制三:事件分发工作流程、Android

在本文中,我们将为您详细介绍COCOS-3.X事件分发机制-原理的相关知识,并且为您解答关于事件分发流程的疑问,此外,我们还会提供一些关于Android事件分发机制三:事件分发工作流程、Android事件分发机制四:学了事件分发有什么用?、android事件分发机制的实现原理、Android事件分发机制(上) ViewGroup的事件分发的有用信息。

本文目录一览:

COCOS-3.X事件分发机制-原理(事件分发流程)

COCOS-3.X事件分发机制-原理(事件分发流程)

一、在cocos2.x版本中,大多数事件,包括触摸、键盘等的响应机制,是通过重写父类的虚函数来实现的,这样的事件响应机制耦合度太高,导致的结果就是通用性不好、模块化不高。例如父类中关于触摸事件的虚函数:


二、而在cocos3.x版本中,原来的事件系统得到了较好的升级,演变成了一个较为独立、完善和通用的组件模块,即事件分发系统,不仅可以用来分发各种系统事件,也可以用来分发用户自定义事件。其大致工作流程如下图所示:


1、利用不同的事件订阅者订阅对应的事件,并设置事件响应回调。在cocos中,事件订阅者是EventListener类的一系列子类,包括EventListenerTouch、EventListenerMouse、EventListenerKeyboard、EventListeneracceleration、EventListenerPhysicsContact、EventListenerCustom等;而他们所对应的事件类型,则是一个Event枚举:

/** Type Event type.*/
    enum class Type
    {
        TOUCH,KEYBOARD,acceleration,MOUSE,FOCUS,GAME_CONTROLLER,CUSTOM
    };

最后设置响应事件回调则是填写对应事件订阅者内部的function类型成员变量:


2、向事件监听器提交事件订阅。cocos的事件监听器(Eventdispatcher)统一管理所有的事件,包括系统事件和用户自定义事件。Eventdispatcher可以从继承Node类中的_eventdispatcher成员变量获取,也可以使用Director::getInstance()->getEventdispatcher()获取;然后提交事件订阅则可以使用Eventdispatcher的addEventListenerWithSceneGraPHPriority(),默认以场景顺序为优先级来进行事件的处理,当然也可以使用固定优先级进行提交addEventListenerWithFixedPriority()。

3、4、当有事件发生时,事件监听器便会在事件订阅列表中查找对应事件,然后通知所有提交了该事件的订阅者。这一步Eventdispatcher会进行一个查找通知操作,所以一般提交的事件订阅不宜过多,否则对事件响应的速度会产生一定影响。

5、订阅者接收到事件监听器分发的事件通知后,触发事件回调,进入对应的事件处理逻辑进行响应。

Android事件分发机制三:事件分发工作流程

Android事件分发机制三:事件分发工作流程

前言

很高兴遇见你~

本文是事件分发系列的第三篇。

在前两篇文章中,Android事件分发机制一:事件是如何到达activity的? 分析了事件分发的真正起点:viewRootImpl,Activity只是其中的一个环节;Android事件分发机制二:viewGroup与view对事件的处理 源码解析了viewGroup和view是如何分发事件的。

事件分发的核心内容,则为viewGroup和view对事件的分发,也就是第二篇文章。第二篇文章对源码的分析较为深入,缺乏一个更高的角度来审视事件分发流程。本文在前面的分析基础上,对整个事件分发的工作流程进行一个总结,更好地把握事件是如何在不同的对象和方法之间进行传递。

回顾

先来回顾一下整体的流程,以便更好地定位我们的知识。

  1. 触摸信息从手机触摸屏幕时产生,通过ims和WMS发送到viewRootImpl
  2. viewRootImpl通过调用view的dispatchPointerEvent方法把触摸信息传递给view
  3. view通过调用自身的dispatchTouchEvent方法开始了事件分发

图中的view指的是一个控件树,他可以是一个viewGroup也可以是一个简单的view。因为viewGroup是继承自view,所以一个控件树,也可以看做是一个view。

我们今天探讨的工作流程,就是从图中的view调用自身的dispatchTouchEvent开始。

主要对象与方法

事件分发的对象

这一部分内容在第二篇有详细解析,这里做个简单的回顾。

当我们手机触碰屏幕时会产生一系列的MotionEvent对象,根据触摸的情况不同,这些对象的类型也会不同。具体如下:

  • ACTION_DOWN: 表示手指按下屏幕
  • ACTION_MOVE: 手指在屏幕上滑动时,会产生一系列的MOVE事件
  • ACTION_UP: 手指抬起,离开屏幕、
  • ACTION_CANCEL:当出现异常情况事件序列被中断,会产生该类型事件
  • ACTION_POINTER_DOWN: 当已经有一个手指按下的情况下,另一个手指按下会产生该事件
  • ACTION_POINTER_UP: 多个手指同时按下的情况下,抬起其中一个手指会产生该事件

事件分发的方法

事件分发属于控件系统的一部分,主要的分发对象是viewGroup与view。而其中核心的方法有三个: dispatchTouchEventonInterceptTouchEvent 、 onTouchEvent 。那么在讲分发流程之前,先来介绍一下这三个方法。这三个方法属于view体系的类,其中Window.CallBack接口中包含了 dispatchTouchEvent 和 onTouchEvent 方法,Activity和Dialog都实现了Window.CallBack接口,因此都实现了该方法。因这三个方法经常在自定义view中被重写,以下的分析,如果没有特殊说明都是在默认方法实现的情况下。

dispatchTouchEvent

该方法是事件分发的核心方法,事件分发的逻辑都是在这个方法中实现。该方法存在于类View中,子类ViewGroup、以及其他的实现类如DecorView都重写了该方法。

无论是在viewGroup还是view,该方法的主要作用都是处理事件。如果成功处理则返回true,处理失败则返回false,表示事件没有被处理。具体到类,在viewGroup相关类中,该方法的主要作用是把事件分发到该viewGroup所拥有的子view,如果子view没有处理则自己处理;在view的相关类中,该方法的主要作用是消费触摸事件。

onInterceptTouchEvent

该方法只存在于viewGroup中,当一个事件需要被分发到子view时,viewGroup会调用此方法检查是否要进行拦截。如果拦截则自己处理,而如果不拦截才会调用子view的 dispatchTouchEvent 方法分发事件。

方法返回true表示拦截事件,返回false表示不拦截。

这个方法默认只对鼠标的相关操作的一种特殊情况进行了拦截,其他的情况需要具体的实现类去重写拦截。

onTouchEvent

该方法是消费事件的主要方法,存在于view中,viewGroup默认并没有重写该方法。方法返回true表示消费事件,返回false表示不消费事件。

viewGroup分发事件时,如果没有一个子view消费事件,那么会调用自身的onTouchEvent方法来处理事件。View的dispatchTouchEvent方法中,并不是直接调用onTouchEvent方法来消费事件,而是先调用onTouchListener判断是否消费;如果onTouchListener没有消费事件,才会调用onTouchEvent来处理事件。

我们为view设置的onClickListener与onLongClickListener都是在View的dispatchTouchEvent方法中,根据具体的触摸情况被调用。

重要规则

事件分发有一个很重要的原则:一个触控点的事件序列只能给一个view消费,除非发生异常情况如被viewGroup拦截 。具体到代码实现就是:消费了一个触控点事件序列的down事件的view,将持续消费该触控点事件序列接下来的所有的事件 。举个栗子:

当我手指按下屏幕时产生了一个down事件,只有一个view消费了这个down事件,那么接下来我的手指滑动屏幕产生的move事件会且仅会给这个view消费。而当我手机抬起,再按下时,这时候又会产生新的down事件,那么这个时候就会再一次去寻找消费down事件的view。所以,事件分发,是以事件序列为单位的 。

因此下面的工作流程中都是指down事件的分发 ,而不是ACTION_MOVE或ACTION_UP的分发。因为消费了down事件,意味着接下来的move和up事件都会给这个view处理,也就无所谓分发了。但同时注意事件序列是可以被viewGroup的onInterceptTouchEvent中断的,这些就属于其他的情况了。

细心的读者还会发现事件分发中包含了多点触控。在多点触控的情况下,ACTION_POINTER_DOWN与ACTION_DOWN的分发规则是不同的,具体可前往第二篇文章了解详细。ACTION_POINTER_DOWN在ACTION_DOWN的分发模型上稍作了一些修改而已,后面会详细解析,

工作流程模型

工作流程模型,本质上就是不同的控件对象,viewGroup和view之间事件分发方法的关系。需要注意的是,这里讨论的是viewGroup和view的默认方法实现,不涉及其他实现类如DecorView的重写方法。

下面用一段伪代码来表示三个事件分发方法之间的关系( 这里再次强调,这里的事件分发模型分发的事件类型是ACTION_DOWN且都是默认的方法,没有经过重写,这点很重要 ):

public boolean dispatchTouchEvent(MotionEvent event){

    // 先判断是否拦截
    if (onInterceptTouchEvent()){
        // 如果拦截调用自身的onTouchEvent方法判断是否消费事件
        return onTouchEvent(event);
    }
    // 否则调用子view的分发方法判断是否处理事件
    if (childView.dispatchTouchEvent(event)){
        return true;
    }else{
        return onTouchEvent(event);
    }
}

这段代码非常好的展示了三个方法之间的关系:在viewGroup收到触摸事件时,会先去调用 onInterceptTouchEvent 方法判断是否拦截,如果拦截则调用自己的 onTouchEvent 方法处理事件,否则调用子view的 dispatchTouchEvent 方法来分发事件。因为子view也有可能是一个viewGroup,这样就形成了一个类似递归的关系。

这里我再补上view分发逻辑的简化伪代码:

public boolean dispatchTouchEvent(MotionEvent event){
    // 先判断是否存在onTouchListener且返回值为true
    if (mOnTouchListener!=null && mOnTouchListener.onTouch(event)){
        // 如果成功消费则返回true
        return true;
    }else{
        // 否则调用onTouchEvent消费事件
        return onTouchEvent(event);
    }
}

view与viewGroup不同的是他不需要分发事件,所以也就没有必要拦截事件。view会先检查是否有onTouchListener且返回值是否为true,如果是true则直接返回,否则调用onTouchEvent方法来处理事件。

基于上述的关系,可以得到下面的工作流程图:

这里为了展示类递归关系使用了画了两个viewGroup,只需看中间一个即可,下面对这个图进行解析:

  • viewGroup
    1. viewGroup的dispatchTouchEvent方法接收到事件消息,首先会去调用onInterceptTouchEvent判断是否拦截事件
      • 如果拦截,则调用自身的onTouchEvent方法
      • 如果不拦截则调用子view的dispatchTouchEvent方法
    2. 子view没有消费事件,那么会调用viewGroup本身的onTouchEvent
    3. 上面1、2步的处理结果为viewGroup的dispatchTouchEvent方法的处理结果,并返回给上一层的onTouchEvent处理
  • view
    1. view的dispatchTouchEvent默认情况下会调用onTouchEvent来处理事件,返回true表示消费事件,返回false表示没有消费事件
    2. 第1步的结果就是dispatchTouchEvent方法的处理结果,成功消费则返回true,没有消费则返回false并交给上一层的onTouchEvent处理

可以看到整个工作流程就是一个“U”型结构,在不拦截的情况下,会一层层向下寻找消费事件的view。而如果当前view不处理事件,那么就一层层向上抛,寻找处理的viewGroup。

上述的工作流程模型并不是完整的,还有其他的特殊情况没有考虑。下面讨论几种特殊的情况:

事件序列被中断

我们知道,当一个view接收了down事件之后,该触控点接下来的事件都会被这个view消费。但是,viewGroup是可以在中途掐断事件流的,因为每一个需要分发给子view的事件都需要经过拦截方法:onInterceptTouchEvent (当然,这里不讨论子view设置不拦截标志的情况)。那么当viewGroup掐断事件流之后,事件的走向又是如何的呢?参看下图:(注意,这里不讨论多指操作的情况,仅讨论单指操作的move或up事件被viewGroup拦截的情况

  1. 当viewGroup拦截子view的move或up事件之后,会将当前事件改为cancel事件并发送给子view
  2. 如果当前事件序列还未结束,那些接下来的事件都会交给viewGroup的ouTouchEvent处理
  3. 此时不管是viewGroup还是view的onTouchEvent返回了false,那么将导致整个控件树的dispatchTouchEvent方法返回false
    • 秉承着一个事件序列只能给一个view消费的原则,如果一个view消耗了down事件却在接下来的move或up事件返回了false,那么此事件不会给上层的viewGroup处理,而是直接返回false。

多点触控情况

上面讨论的所有情况,都是不包含多点触控情况的。多点触控的情况,在原有的事件分发流程上,新增了一些特殊情况。这里就不再画图,而是把一些特殊情况描述一下,读者了解一下就可以了。

默认情况下,viewGroup是支持多点触控的分发,但view是不支持多点触控的,需要自己去重写 dispatchTouchEvent 方法来支持多点触控。

多点触控的分发规则如下:

viewGroup在已有view接受了其他触点的down事件的情况下,另一个手指按下产生ACTION_POINTER_DOWN事件传递给viewGroup:

  1. viewGroup会按照ACTION_DOWN的方式去分发ACTION_POINTER_DOWN事件
    • 如果子view消费该事件,那么和单点触控的流程一致
    • 如果子view未消费该事件,那么会交给上一个最后接收down事件的view去处理
  2. viewGroup两个view接收了不同的down事件,那么拦截其中一个view的事件序列,viewGroup不会消费拦截的事件序列。换句话说,viewGroup和其中的view不能同时接收触摸事件。

Activity的事件分发

细心的读者会发现,上述的工作流程并不涉及Activity。我们印象中的事件分发都是 Activity -> Window -> ViewGroup ,那么这是怎么回事?这一切,都是DecorView “惹的祸” 。

DecorView重写viewGroup的 dispatchTouchEvent 方法,当接收到触摸事件后,DecorView会首先把触摸对象传递给内部的callBack对象。没错,这个callBack对象就是Activity。加入Activity这个环节之后,分发的流程如下图所示:

整体上和前面的流程没有多大的不同,Activity继承了Window.CallBack接口,所以也有dispatchTouchEvent和onTouchEvent方法。对上图做个简单的分析:

  1. activity接收到触摸事件之后,会直接把触摸事件分发给viewGroup
  2. 如果viewGroup的dispatchTouchEvent方法返回false,那么会调用Activity的onTouchEvent来处理事件
  3. 第1、2步的处理结果就是activity的dispatchTouchEvent方法的处理结果,并返回给上层

上面的流程不仅适用于Activity,同样适用于Dialog等使用DecorView和callback模式的控件系统。

总结

到这里,事件分发的主要内容也就讲解完了。结合前两篇文章,相信读者对于事件分发有更高的认知。

纸上得来终觉浅,绝知此事要躬行。学了知识之后最重要的就是实践。下一篇文章将简单分析一下如何利用学习到的事件分发知识运用到实际开发中。

最后

在这里我也分享一份由几位大佬一起收录整理的 Flutter进阶资料以及Android学习PDF+架构视频+面试文档+源码笔记 ,并且还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料……

这些都是我闲暇时还会反复翻阅的精品资料。可以有效的帮助大家掌握知识、理解原理。当然你也可以拿去查漏补缺,提升自身的竞争力。
如果你有需要的话,可以前往 GitHub 自行查阅。

原创不易,你的点赞是我创作最大的动力,感谢阅读 ~

Android事件分发机制四:学了事件分发有什么用?

Android事件分发机制四:学了事件分发有什么用?

“ 学了事件分发,影响我CV大法吗?”

“ 影响我陪女朋友的时间”

“ ..... ”

前言

Android事件分发机制已经来到第四篇了,在前三篇中:

  • Android事件分发机制一:事件是如何到达activity的? : 从window机制出发分析了事件分发的整体流程,以及事件分发的真正起点
  • Android事件分发机制二:viewGroup与view对事件的处理 : 源码分析了viewGroup和view是如何分发事件的
  • Android事件分发机制三:事件分发工作流程 : 分析了触摸事件在控件树中的分发流程模型

那么关于事件分发的知识在上面三篇文章也就分析地差不多了,接下来就分析一下学了之后该如何使运用到实际开发中,简单阐述一下笔者的思考。

Android中的view一般由两个重要的部分组成:绘制和触摸反馈。如何精准地针对用户的操作给出正确的反馈,是我们学事件分发最重要的目标。

运用事件分发一般有两个场景:给view设置监听器和自定义view。接下来就针对这两方面展开分析,最后再给出笔者的一些思考与总结。

监听器

触摸事件监听器可以说是我们接触Android事件体系的第一步。监听器通常有:

  • OnClickListener : 单击事件监听器
  • OnLongClickListener : 长按事件监听器
  • OnTouchListener : 触摸事件监听器

这些是我们使用得最频繁的监听器,他们之间的关系是:

if (mOnTouchListener!=null && mTouchListener.onTouch(event)){
    return true;
}else{
    if (单击事件){
        mOnClickListener.onClick(view);
    }else if(长按事件){
        mOnLongClickListener.onLongClick(view);
    }
}

上面的伪代码可以很明显地发现:onTouchListener是直接把MotionEvent对象直接接管给自己处理且会最先调用,而其他的两个监听器是view判断点击类型之后再分别调用

除此之外,另一个很常见的监听器是双击监听器,但这种监听器并不是view默认支持的,需要我们自己去实现。双击监听器的实现思路可以参考view实现长按监听器的思路来实现:

当我们接收到点击事件时,可以发送一个单击延时任务。如果在延迟时间到还没收到另一个点击事件,那么这就是一个单击事件;如果在延迟时间内收到另一个点击事件,说明这是一个双击事件,并取消延时任务。

我们可以实现 view.OnClickListener 接口来完成以上逻辑,核心代码如下:

// 实现onClickListener接口
class MyClickListener() : View.OnClickListener{
    private var isClicking = false
    private var singleClickListener : View.OnClickListener? = null
    private var doubleClickListener : View.OnClickListener? = null
    private var delayTime = 1000L
    private var clickCallBack : Runnable? = null
    private var handler : Handler = Handler(Looper.getMainLooper())

    override fun onClick(v: View?) {
        // 创建一个单击延迟任务,延迟时间到了之后触发单击事件
        clickCallBack = clickCallBack?: Runnable {
            singleClickListener?.onClick(v)
            isClicking = false
        }
        // 如果已经点击过一次,在延迟时间内再次接受到点击
        // 意味着这是个双击事件
        if (isClicking){
            // 移除延迟任务,回调双击监听器
            handler.removeCallbacks(clickCallBack!!)
            doubleClickListener?.onClick(v)
            isClicking = false
        }else{
            // 第一次点击,发送延迟任务
            isClicking = true
            handler.postDelayed(clickCallBack!!,delayTime)
        }
    }
...
}

代码中实现了创建了一个 View.OnclickListener 接口实现类,并在类型实现单击和双击的逻辑判断。我们可以如下使用这个类:

val c = MyClickListener()
// 设置单击监听事件
c.setSingleClickListener(View.OnClickListener {
    Log.d(TAG, "button: 单击事件")
})
// 设置双击监听事件
c.setDoubleClickListener(View.OnClickListener {
    Log.d(TAG, "button: 双击事件")
})
// 把监听器设置给按钮
button.setOnClickListener(c)

这样就实现了按钮的双击监听了。

其他类型的监听器如:三击、双击长按等等,都可以基于这种思路来实现监听器接口。

自定义view

在自定义view中,我们可以更加灵活地运用事件分发来解决实际的需求。举几个例子:

滑动嵌套问题:外层是viewPager,里层是recyclerView,要实现左右滑动切换viewPager,上下滑动recyclerView。这也就是著名的滑动冲突问题。类似的还有外层viewPager,里层也是可以左右滑动的recyclerView。
实时触摸反馈问题:如设计一个按钮,要让他按下的时候缩小降低高度,放开的时候恢复到原来的大小和高度,而且如果在一个可滑动的容器中,按下之后滑动不会触发点击事件而是把事件交给外层可滑动容器。

我们可以发现,基本上都是基于实际的开发需求来灵活运用事件分发。具体到代码实现,都是围绕着三个关键方法展开: dispatchTouchEventonIntercepterTouchEventonTouchEvent 。这三个方法在view和viewGroup中已经有了默认的实现,而我们需要基于默认实现来完成我们的需求。下面看看几种常见的场景如何实现。

实现方块按下缩小

我们先来看看具体的实现效果:

方块按下后,会缩小高度变低透明度增加,释放又会恢复。

这个需求可以通过结合属性动画来实现。按钮块本身有高度、有圆角,我们可以考虑继承cardView来实现,重写cardView的dispatchTouchEvent方法,在按下的时候,也就是接收到down事件的时候缩小,在接收到up和cancel事件的时候恢复。注意,这里可能会忽视cancel事件,导致按钮块的状态无法恢复,一定要加以考虑cancel事件 。然后来看下代码实现:

public class NewCardView extends CardView {

    //点击事件到来的时候进行判断处理
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 获取事件类型
        int actionMarked = ev.getActionMasked();
        // 根据时间类型判断调用哪个方法来展示动画
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN :{
                clickEvent();
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                upEvent();
                break;
            default: break;
        }
        // 最后回调默认的事件分发方法即可
        return super.dispatchTouchEvent(ev);
    }

    //手指按下的时候触发的事件;大小高度变小,透明度减少
    private void clickEvent(){
        setCardElevation(4);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(this,"scaleX",1,0.97f),
                ObjectAnimator.ofFloat(this,"scaleY",1,0.97f),
                ObjectAnimator.ofFloat(this,"alpha",1,0.9f)
        );
        set.setDuration(100).start();
    }

    //手指抬起的时候触发的事件;大小高度恢复,透明度恢复
    private void upEvent(){
        setCardElevation(getCardElevation());
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(this,"scaleX",0.97f,1),
                ObjectAnimator.ofFloat(this,"scaleY",0.97f,1),
                ObjectAnimator.ofFloat(this,"alpha",0.9f,1)
        );
        set.setDuration(100).start();
    }
}

动画方面的内容就不分析了,不属于本文的范畴。可以看到我们只是给cardView设置了动画效果,监听事件我们可以设置给cardView内部的ImageView或者直接设置给CardView。需要注意的是,如果设置给cardView需要重写cardView的 intercepTouchEvent 方法永远返回true,防止事件被子view消费而无法触发监听事件。

解决滑动冲突

滑动冲突是事件分发运用最频繁的场景,也是一个重难点(敲黑板,考试要考的)。滑动冲突的基本场景有以下三种:

  • 情况一:内外view的滑动方向不同,例如viewPager嵌套ListView
  • 情况二:内外view滑动方向相同,例如viewPager嵌套水平滑动的recyclerView
  • 情况三:情况一和情况二的组合

解决这类问题一般有两个步骤:确定最终实现效果、代码实现。

滑动冲突的解决需要结合具体的实现需求,而不是一套解决方案可以解决一切的滑动冲突问题,这不现实。因此在解决这类问题时,需要先确定好最终的实现效果,然后再根据这个效果去思考代码实现。这里主要讨论情况一和情况二,情况三类同。

情况一

情况一是内外滑动方向不一致。这种情况的通用解决方案就是:根据手指滑动直线与水平线的角度来判断是左右滑动还是上下滑动:

如果这个角度小于45度,可以认为是在左右滑动,如果大于45度,则认为是上下滑动。那么现在确定好解决方案,接下来就思考如何代码实现。

滑动角度可以通过两个连续的MotionEvent对象的坐标计算出来,之后我们再根据角度的情况选择把事件交给外部容器还是内部view。这里根据事件处理的位置可分为内部拦截法和外部拦截法

  • 外部拦截法:在viewGroup中判断滑动的角度,如果符合自身滑动方向消费则拦截事件
  • 内部拦截法:在内部view中判断滑动的角度,如果是符合自身滑动方向则继续消费事件,否则请求外部viewGroup拦截事件处理

从实现的复杂度来看,外部拦截法会更加优秀,不需要里外view去配合,只需要viewGroup自身做好事件拦截处理即可。两者的区别就在于主动权在谁的手上。如果view需要做更多的判断可以采用内部拦截法,而一般情况下采用外部拦截法会更加简单。

接下来思考一下这两种方法的代码实现。


外部拦截法中,重点在于是否拦截事件,那么我们的重心就放在了 onInterceptTouchEvent 方法中。在这个方法中计算滑动角度并判断是否要进行拦截。这里以ScrollView为例子(外部是垂直滑动,内部是水平滑动),代码如下:

public class MyScrollView extends ScrollView {
    // 记录上一次事件的坐标
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int actionMasked = ev.getActionMasked();
        // 不能拦截down事件,否则子view永远无法获取到事件
        // 不能拦截up事件,否则子view的点击事件无法被触发
        if (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_UP){
            lastX = ev.getX();
            lastY = ev.getY();
            return false;
        }   

        // 获取斜率并判断
        float x = ev.getX();
        float y = ev.getY();
        return Math.abs(lastX - x) < Math.abs(lastY - y);
    }
}

代码的实现思路很简单,记录两次触控点的位置,然后计算出斜率来判断是垂直还是水平滑动。代码中有个需要注意的点:viewGroup不能拦截up事件和down事件。如果拦截了down事件那么子view将永远接收不到事件信息;如果拦截了up事件那么子view将永远无法触发点击事件。

上面的代码是事件分发的核心代码,更加具体的代码还需要根据实际需求去完善细节,但整体的逻辑是不变的。


内部拦截法的思路和外部拦截的思路很像,只是判断的位置放到了内部view中。内部拦截法意味着内部view必须要有控制事件流走向的能力,才能对事件进行处理。这里就运用到了内部view一个重要的方法: requestDisallowInterceptTouchEvent

这个方法可以强制外层viewGroup不拦截事件。因此,我们可以让viewGroup默认拦截除了down事件以外的所有事件。当子view需要处理事件时,只需要调用此方法即可获取事件;而当想要把事件交给viewGroup处理时,那么只需要取消这个标志,外层viewGroup就会拦截所有事件。从而达到内部view控制事件流走向的目的。

代码实现需要分两步走,首先是设置外部viewGroup拦截除了down事件以外的所有事件(这里用viewPager和ListView来进行代码演示):

public class MyViewPager extends ViewPager {
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked()==MotionEvent.ACTION_DOWN){
            return false;
        }
        return true;
    }
}

接下来需要重写内部view的dispatchTouchEvent方法:

public class MyListView extends ListView {
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            // down事件,必须请求不拦截,否则拿不到move事件无法进行判断
            case MotionEvent.ACTION_DOWN:{
                requestDisallowInterceptTouchEvent(true);
                break;
            }
            // move事件,进行判断是否处理事件
            case MotionEvent.ACTION_MOVE:{
                float x = ev.getX();
                float y = ev.getY();
                // 如果滑动角度大于90度自己处理事件
                if (Math.abs(lastY-y)<Math.abs(lastX-x)){
                    requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            default:break;
        }
        // 保存本次触控点的坐标
        lastX = ev.getX();
        lastY = ev.getY();
        // 调用ListView的dispatchTouchEvent方法来处理事件
        return super.dispatchTouchEvent(ev);
    }
}

两种方法的代码思路基本一致,但是内部拦截法会更加复杂一点,所以在一般的情况下,还是使用外部拦截法较好。

到这里已经解决了情况一的滑动冲突解决方案,接下来看看情况二的滑动冲突如何解决。

情况二

第二种情况是里外容器的滑动方向是一致的,这种情况的主流解决方法有两种,一种是外容器先滑动,外容器滑动到边界之后再滑动内部view,例如京东app(注意向下滑动时的情况):

第二种情况的内部view先滑动,等内部view滑动到边界之后再滑动外部viewGroup,例如饿了么app(注意向下滑动时的情况):

这两种方案没有孰好孰坏,而是需要根据具体的业务需求来确定具体的解决方案。下面就上述的第二种方案展开分析,第一种方案类同。

首先分析一下具体的效果:外层viewGroup与内层view的滑动方向是一致的,都是垂直滑动或水平滑动;向上滑动时,先滑动viewGroup到顶部,再滑动内部view;向下滑动时,先滑动内部view到顶部后再滑动外层viewGroup。

这里我们采用外部拦截法来实现。首先我们先确定好我们的布局:

image.png

最外层是一个ScrollView,内部首先是一个LinearLayout,因为ScrollView只能有一个view。内部顶部是一个LinearLayout可以放置头部布局,下面是一个ListView。现在需要确定ScrollView的拦截规则:

  1. 当ScrollView没有滑动到底部时,直接给ScrollView处理
  2. 当ScrollView滑动到底部时:

    • 如果LinearLayout没有滑动到顶部,则交给ListView处理
    • 如果LinearLayout滑动到顶部:

      • 如果是向上滑动则交给listView处理
      • 如果是向下滑动则交给ScrollView处理

接下来就可以确定我们的代码了:

public class MyScrollView extends ScrollView {
    ...
    float lastY = 0;
    boolean isScrollToBottom = false;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:{
                // 这三种事件默认不拦截,必须给子view处理
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                LinearLayout layout = (LinearLayout) getChildAt(0);
                ListView listView = (ListView)layout.getChildAt(1);
                // 如果没有滑动到底部,由ScrollView处理,进行拦截
                if (!isScrollToBottom){
                    intercept = true;
                    // 如果滑动到底部且listView还没滑动到顶部,不拦截
                }else if (!ifTop(listView)){
                    intercept = false;
                }else{
                    // 否则判断是否是向下滑
                    intercept = ev.getY() > lastY;
                }
                break;
            }
            default:break;
        }
        // 最后记录位置信息
        lastY = ev.getY();
        // 调用父类的拦截方法,ScrollView需要做一些处理,不然可能会造成无法滑动
        super.onInterceptTouchEvent(ev);
        return intercept;
    }
    ...
}

代码中我还增加了如果listView下面有view的情况,判断是否滑动到底部。判断listView滑动情况和scrollView滑动情况的代码如下:

{
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // 设置滑动监听
        setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
            ViewGroup viewGroup = (ViewGroup)v;
            isScrollToBottom = v.getHeight() + scrollY >= viewGroup.getChildAt(0).getHeight();
        });
    }
}
// 判断listView是否到达顶部
private boolean ifTop(ListView listView){
    if (listView.getFirstVisiblePosition()==0){
        View view = listView.getChildAt(0);
        return view != null && view.getTop() >= 0;
    }
    return false;
}

最终的实现效果如下图:

这样就简单地解决一个滑动冲突了。但是要注意的是,在实际问题中,往往有更加复杂的细节需要处理。而上述只是把解决滑动冲突的一个思想分析了一下,具体到业务上,还需要去细心打磨代码才行。有兴趣可以去看看NeatedScrollView是如何解决滑动冲突的源码。

最后

事件分发作为Android的基础知识储备可谓是非常重要。不能说学了事件分发,就可以直接一飞冲天。而是掌握了事件分发之后,面对一些具体的需求,就有了一定的思路去处理。或者在了解一些框架的源码的时候,懂得他这些代码是什么意思。

学习事件分发的过程中,深入研究了很多的源码,有一些小伙伴觉得没必要。实际开发中也就用到那三个主要的方法,了解一个主要的流程就足够了。我想说:确实是这样;但没有研究背后的原理,就只能知其然而不知其所以然。当遇到一些异常的情况时,就无法从源码的角度去分析结果的bug。学习源码的过程中,也是与设计android系统的作者的一种交流。倘若现在没有事件分发机制,那么我该如何去解决触摸信息的分发问题?学习的过程就是在思考android系统作者给出的解决方案。而掌握原理之后,对于事件分发的问题,稍加思考和分析,也就手到擒来了。正所谓:

只有打败10级的敌人,才能掌控9级的敌人。

希望文章对你有帮助。

要不留下个小小的点赞鼓励一下作者?

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。
笔者才疏学浅,有任何想法欢迎评论区交流指正。
如需转载请评论区或私信交流。

另外欢迎光临笔者的个人博客:传送门

android事件分发机制的实现原理

android事件分发机制的实现原理

android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即MotionEvent都会经历一个从分发,拦截到处理的一个过程。即dispatchTouchEvent(),onInterceptEvent()到onTouchEvent()的一个过程,在dispatchTouchEvent()负责了事件的分发过程,在dispatchTouchEvent()中会调用onInterceptEvent()与onTouchEvent(),如果onInterceptEvent()返回true,那么会调用到当前view的onTouchEvent()方法,如果不拦截,事件就会下发到子view的dispatchTouchEvent()中进行同样的操作。本文将带领大家从源码角度来分析android是如何进行事件分发的。

android中的事件分发流程最先从activity的dispatchTouchEvent()开始:

public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  }
  if (getWidow().superdispatchTouchEvent(ev)) {
    return true;
  }
  return onTouchEvent(ev);
}

这里调用了getwindow().superdispatchTouchEvent(ev),这里可以看出activity将MotionEvent传寄给了Window。而Window是一个抽象类,superdispatchTouchEvent()也是一个抽象方法,这里用到的是window的子类phoneWindow。

@Override
public boolean superdispatchTouchEvent(MotionEvent event) {
  return mDecor.superdispatchTouchEvent(event);
} 

从这里可以看出,event事件被传到了DecorView,也就是我们的顶层view.我们继续跟踪:

public boolean superdispatchTouchEvent(MotionEvent event) {
  return super.dispatchTouchEvent(event);
}

这里调用到了父类的dispatchTouchEvent()方法,而DecorView是继承自FrameLayout,FrameLayout继承了ViewGroup,所以这里会调用到ViewGroup的dispatchTouchEvent()方法。

所以整个事件流从activity开始,传递到window,最后再到我们的view(viewGroup也是继承自view)中,而view才是我们整个事件处理的核心阶段。

我们来看一下viewGroup的dispatchTouchEvent()中的实现:

if (actionMasked == MotionEvent.ACTION_DOWN) {
      // Throw away all prevIoUs state when starting a new touch gesture.
      // The framework may have dropped the up or cancel event for the prevIoUs gesture
      // due to an app switch,ANR,or some other state change.
      cancelAndClearTouchTargets(ev);
      resetTouchState();
    }

这是dispatchTouchEvent()开始时截取的一段代码,我们来看一下,首先,当我们手指按下view时,会调用到resetTouchState()方法,在resetTouchState()中:

private void resetTouchState() {
  clearTouchTargets();
  resetCancelNextUpFlag(this);
  mGroupFlags &= ~FLAG_disALLOW_INTERCEPT;
  mnestedScrollAxes = SCROLL_AXIS_NONE;
}

我们继续跟踪clearTouchTargets()方法:

private void clearTouchTargets() {
  TouchTarget target = mFirstTouchTarget;
  if (target != null) {
    do {
      TouchTarget next = target.next;
      target.recycle();
      target = next;
    } while (target != null);
    mFirstTouchTarget = null;
  }
}

在clearTouchTargets()方法中,我们最终将mFirstTouchTarget赋值为null,我们继续回到dispatchTouchEvent()中,接着执行了下段代码:

// Check for interception.
final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
      final boolean disallowIntercept = (mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0;
      if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
      } else {
        intercepted = false;
      }
    } else {
      // There are no touch targets and this action is not an initial down
      // so this view group continues to intercept touches.
      intercepted = true;
    }

当view被按下或mFirstTouchTarget != null 的时候,从前面可以知道,当每次view被按下时,也就是重新开始一次事件流的处理时,mFirstTouchTarget都会被设置成null,一会我们看mFirstTouchTarget是什么时候被赋值的。

从disallowIntercept属性我们大概能猜到是用来判断是否需要坐拦截处理,而我们知道可以通过调用父view的requestdisallowInterceptTouchEvent(true)可以让我们的父view不能对事件进行拦截,我们先来看看requestdisallowInterceptTouchEvent()方法中的实现:

@Override
public void requestdisallowInterceptTouchEvent(boolean disallowIntercept) {

  if (disallowIntercept == ((mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0)) {
    // We're already in this state,assume our ancestors are too
    return;
  }

  if (disallowIntercept) {
    mGroupFlags |= FLAG_disALLOW_INTERCEPT;
  } else {
    mGroupFlags &= ~FLAG_disALLOW_INTERCEPT;
  }

  // Pass it up to our parent
  if (mParent != null) {
    mParent.requestdisallowInterceptTouchEvent(disallowIntercept);
  }
}

这里也是通过设置标志位做判断处理,所以这里是通过改变mGroupFlags标志,然后在dispatchTouchEvent()刚发中变更disallowIntercept的值判断是否拦截,当为true时,即需要拦截,这个时候便会跳过onInterceptTouchEvent()拦截判断,并标记为不拦截,即intercepted = false,我们继续看viewGroup的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;
}

即默认情况下,只有在ACTION_DOWN时,viewGroup才会表现为拦截。

我们继续往下看:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
   final float x = ev.getX(actionIndex);
   final float y = ev.getY(actionIndex);
   // Find a child that can receive the event.
   // Scan children from front to back.
   final ArrayList<View> preorderedList = buildTouchdispatchChildList();
   final boolean customOrder = preorderedList == null
              && isChildrenDrawingOrderEnabled();
   final View[] children = mChildren;
   for (int i = childrenCount - 1; i >= 0; i--) {
      final int childindex = getAndVerifyPreorderedindex(
                childrenCount,i,customOrder);
            final View child = getAndVerifyPreorderedView(
                preorderedList,children,childindex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
              if (childWithAccessibilityFocus != child) {
                continue;
              }
              childWithAccessibilityFocus = null;
              i = childrenCount - 1;
            }

            if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x,y,child,null)) {
              ev.setTargetAccessibilityFocus(false);
              continue;
            }

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
              // Child is already receiving touch within its bounds.
              // Give it the new pointer in addition to the ones it is handling.
              newTouchTarget.pointerIdBits |= idBitsToAssign;
              break;
            }

            resetCancelNextUpFlag(child);
            if (dispatchTransformedTouchEvent(ev,false,idBitsToAssign)) {
              // Child wants to receive touch within its bounds.
              mLastTouchDownTime = ev.getDownTime();
              if (preorderedList != null) {
                // childindex points into presorted list,find original index
                for (int j = 0; j < childrenCount; j++) {
                  if (children[childindex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                  }
                }
              } else {
                mLastTouchDownIndex = childindex;
              }
              mLastTouchDownX = ev.getX();
              mLastTouchDownY = ev.getY();
              newTouchTarget = addTouchTarget(child,idBitsToAssign);
              alreadydispatchedToNewTouchTarget = true;
              break;
            }

            // The accessibility focus didn't handle the event,so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
          }
          if (preorderedList != null) preorderedList.clear();
        }

这段代码首先会通过一个循环去遍历所有的子view,最终会调用到dispatchTransformedTouchEvent()方法,我们继续看dispatchTransformedTouchEvent()的实现:

private boolean dispatchTransformedTouchEvent(MotionEvent event,boolean cancel,View child,int desiredPointerIdBits) {
  final boolean handled;

  // Canceling motions is a special case. We don't need to perform any transformations
  // or filtering. The important part is the action,not the contents.
  final int oldAction = event.getAction();
  if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
      handled = super.dispatchTouchEvent(event);
    } else {
      handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
  }

  // Calculate the number of pointers to deliver.
  final int oldPointerIdBits = event.getPointerIdBits();
  final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

  // If for some reason we ended up in an inconsistent state where it looks like we
  // might produce a motion event with no pointers in it,then drop the event.
  if (newPointerIdBits == 0) {
    return false;
  }

  // If the number of pointers is the same and we don't need to perform any fancy
  // irreversible transformations,then we can reuse the motion event for this
  // dispatch as long as we are careful to revert any changes we make.
  // Otherwise we need to make a copy.
  final MotionEvent transformedEvent;
  if (newPointerIdBits == oldPointerIdBits) {
    if (child == null || child.hasIdentityMatrix()) {
      if (child == null) {
        handled = super.dispatchTouchEvent(event);
      } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        event.offsetLocation(offsetX,offsetY);

        handled = child.dispatchTouchEvent(event);

        event.offsetLocation(-offsetX,-offsetY);
      }
      return handled;
    }
    transformedEvent = MotionEvent.obtain(event);
  } else {
    transformedEvent = event.split(newPointerIdBits);
  }

  // Perform any necessary transformations and dispatch.
  if (child == null) {
    handled = super.dispatchTouchEvent(transformedEvent);
  } else {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    transformedEvent.offsetLocation(offsetX,offsetY);
    if (! child.hasIdentityMatrix()) {
      transformedEvent.transform(child.getInverseMatrix());
    }

    handled = child.dispatchTouchEvent(transformedEvent);
  }

  // Done.
  transformedEvent.recycle();
  return handled;
}

这段代码就比较明显了,如果child不为null,始终会调用到child.dispatchTouchEvent();否则调用super.dispatchTouchEvent();

如果child不为null时,事件就会向下传递,如果子view处理了事件,即dispatchTransformedTouchEvent()即返回true。继续向下执行到addTouchTarget()方法,我们继续看addTouchTarget()方法的执行结果:

private TouchTarget addTouchTarget(@NonNull View child,int pointerIdBits) {
  final TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
  target.next = mFirstTouchTarget;
  mFirstTouchTarget = target;
  return target;
}

这个时候我们发现mFirstTouchTarget又出现了,这时候会给mFirstTouchTarget重新赋值,即mFirstTouchTarget不为null。也就是说,如果事件被当前view或子view消费了,那么在接下来的ACTION_MOVE或ACTION_UP事件中,mFirstTouchTarget就不为null。但如果我们继承了该viewGroup,并在onInterceptTouchEvent()的ACTION_MOVE中拦截了事件,那么后续事件将不会下发,将由该viewGroup直接处理,从下面代码我们可以得到:

// dispatch to touch targets,excluding the new touch target if we already
      // dispatched to it. Cancel touch targets if necessary.
      TouchTarget predecessor = null;
      TouchTarget target = mFirstTouchTarget;
      while (target != null) {
        final TouchTarget next = target.next;
        if (alreadydispatchedToNewTouchTarget && target == newTouchTarget) {
          handled = true;
        } else {
          final boolean cancelChild = resetCancelNextUpFlag(target.child)
              || intercepted;
          if (dispatchTransformedTouchEvent(ev,cancelChild,target.child,target.pointerIdBits)) {
            handled = true;
          }
          if (cancelChild) {
            if (predecessor == null) {
              mFirstTouchTarget = next;
            } else {
              predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
          }
        }
        predecessor = target;
        target = next;
      }

当存在子view并且事件被子view消费时,即在ACTION_DOWN阶段mFirstTouchTarget会被赋值,即在接下来的ACTION_MOVE事件中,由于intercepted为true,所以将ACTION_CANCEL 事件传递过去,从dispatchTransformedTouchEvent()中可以看到:

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
      handled = super.dispatchTouchEvent(event);
    } else {
      handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
  }

并将mFirstTouchTarget 最终赋值为 next,而此时mFirstTouchTarget位于TouchTarget链表尾部,所以mFirstTouchTarget会赋值为null,那么接下来的事件将不会进入到onInterceptTouchEvent()中。也就会直接交由该view处理。

如果我们没有进行事件的拦截,而是交由子view去处理,由于ViewGroup的onInterceptTouchEvent()默认并不会拦截除了ACTION_DOWN以外的事件,所以后续事件将继续交由子view去处理,如果存在子view且事件位于子view内部区域的话。

所以无论是否进行拦截,事件流都会交由view的dispatchTouchEvent()中进行处理,我们接下来跟踪一下view中的dispatchTouchEvent()处理过程:

if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopnestedScroll();
  }

  if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
      result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this,event)) {
      result = true;
    }

    if (!result && onTouchEvent(event)) {
      result = true;
    }
  }

当被按下时,即ACTION_DOWN时,view会停止内部的滚动,如果view没有被覆盖或遮挡时,首先会进行mListenerInfo是否为空的判断,我们看下mListenerInfo是在哪里初始化的:

ListenerInfo getListenerInfo() {
  if (mListenerInfo != null) {
    return mListenerInfo;
  }
  mListenerInfo = new ListenerInfo();
  return mListenerInfo;
}

这里可以看出,mListenerInfo一般不会是null,知道在我们使用它时调用过这段代码,而当view被加入window中的时候,会调用下面这段代码,从注释中也可以看出来:

/**
 * Add a listener for attach state changes.
 *
 * This listener will be called whenever this view is attached or detached
 * from a window. Remove the listener using
 * {@link #removeOnAttachStatechangelistener(OnAttachStatechangelistener)}.
 *
 * @param listener Listener to attach
 * @see #removeOnAttachStatechangelistener(OnAttachStatechangelistener)
 */
public void addOnAttachStatechangelistener(OnAttachStatechangelistener listener) {
  ListenerInfo li = getListenerInfo();
  if (li.mOnAttachStatechangelisteners == null) {
    li.mOnAttachStatechangelisteners
        = new copyOnWriteArrayList<OnAttachStatechangelistener>();
  }
  li.mOnAttachStatechangelisteners.add(listener);
}

到这里我们就知道,mListenerInfo一开始就是被初始化好了的,所以li不可能为null,li.mOnTouchListener != null即当设置了TouchListener时不为null,并且view是enabled状态,一般情况view都是enable的。这个时候会调用到onTouch()事件,当onTouch()返回true时,这个时候result会赋值true。而当result为true时,onTouchEvent()将不会被调用。

从这里可以看出,onTouch()会优先onTouchEvent()调用;
当view设置touch监听并返回true时,那么它的onTouchEvent()将被屏蔽。否则会调用onTouchEvent()处理。

那么让我们继续来看看onTouchEvent()中的事件处理:

if ((viewFlags & ENABLED_MASK) == disABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_pressed) != 0) {
      setpressed(false);
    }
    // A disabled view that is clickable still consumes the touch
    // events,it just doesn't respond to them.
    return (((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
  }

首先,当view状态是disABLED时,只要view是CLICKABLE或LONG_CLICKABLE或CONTEXT_CLICKABLE,都会返回true,而button默认是CLICKABLE的,textview默认不是CLICKABLE的,而view一般默认都不是LONG_CLICKABLE的。

我们继续向下看:

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
      return true;
    }
  }

如果有代理事件,仍然会返回true.

if (((viewFlags & CLICKABLE) == CLICKABLE ||
      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
      (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
      case MotionEvent.ACTION_UP:
        boolean prepressed = (mPrivateFlags & PFLAG_PREpressed) != 0;
        if ((mPrivateFlags & PFLAG_pressed) != 0 || prepressed) {
          // take focus if we don't have it already and we should in
          // touch mode.
          boolean focusTaken = false;
          if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
            focusTaken = requestFocus();
          }

          if (prepressed) {
            // The button is being released before we actually
            // showed it as pressed. Make it show the pressed
            // state Now (before scheduling the click) to ensure
            // the user sees it.
            setpressed(true,x,y);
          }

          if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            // This is a tap,so remove the longpress check
            removeLongPressCallback();

            // Only perform take click actions if we were in the pressed state
            if (!focusTaken) {
              // Use a Runnable and post this rather than calling
              // performClick directly. This lets other visual state
              // of the view update before click actions start.
              if (mPerformClick == null) {
                mPerformClick = new PerformClick();
              }
              if (!post(mPerformClick)) {
                performClick();
              }
            }
          }

          if (mUnsetpressedState == null) {
            mUnsetpressedState = new UnsetpressedState();
          }

          if (prepressed) {
            postDelayed(mUnsetpressedState,ViewConfiguration.getpressedStateDuration());
          } else if (!post(mUnsetpressedState)) {
            // If the post Failed,unpress right Now
            mUnsetpressedState.run();
          }

          removeTapCallback();
        }
        mIgnoreNextUpEvent = false;
        break;

      case MotionEvent.ACTION_DOWN:
        mHasPerformedLongPress = false;

        if (performButtonActionOnTouchDown(event)) {
          break;
        }

        // Walk up the hierarchy to determine if we're inside a scrolling container.
        boolean isInScrollingContainer = isInScrollingContainer();

        // For views inside a scrolling container,delay the pressed Feedback for
        // a short period in case this is a scroll.
        if (isInScrollingContainer) {
          mPrivateFlags |= PFLAG_PREpressed;
          if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
          }
          mPendingCheckForTap.x = event.getX();
          mPendingCheckForTap.y = event.getY();
          postDelayed(mPendingCheckForTap,ViewConfiguration.getTapTimeout());
        } else {
          // Not inside a scrolling container,so show the Feedback right away
          setpressed(true,y);
          checkForLongClick(0,y);
        }
        break;

      case MotionEvent.ACTION_CANCEL:
        setpressed(false);
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        break;

      case MotionEvent.ACTION_MOVE:
        drawableHotspotChanged(x,y);

        // Be lenient about moving outside of buttons
        if (!pointInView(x,mTouchSlop)) {
          // Outside button
          removeTapCallback();
          if ((mPrivateFlags & PFLAG_pressed) != 0) {
            // Remove any future long press/tap checks
            removeLongPressCallback();

            setpressed(false);
          }
        }
        break;
    }

    return true;
  }

当view是CLICKABLE或LONG_CLICKABLE或CONTEXT_CLICKABLE状态时,当手指抬起时,如果设置了click监听,最终会调用到performClick(),触发click()事件。这点从performClick()方法中可以看出:

public boolean performClick() {
  final boolean result;
  final ListenerInfo li = mListenerInfo;
  if (li != null && li.mOnClickListener != null) {
    playSoundEffect(SoundEffectConstants.CLICK);
    li.mOnClickListener.onClick(this);
    result = true;
  } else {
    result = false;
  }

  sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  return result;
}

从这里我们也可以得出,click事件会在onTouchEvent()中被调用,如果view设置了onTouch()监听并返回true,那么click事件也会被屏蔽掉,不过我们可以在onTouch()中通过调用view的performClick()继续执行click()事件,这个就看我们的业务中的需求了。

从这里我们可以看出,如果事件没有被当前view或子view处理,即返回false,那么事件就会交由外层view继续处理,直到被消费。

如果事件一直没有被处理,会最终传递到Activity的onTouchEvent()中。

到这里我们总结一下:

事件是从Activity->Window->View(ViewGroup)的一个传递流程;

如果事件没有被中途拦截,那么它会一直传到最内层的view控件;

如果事件被某一层拦截,那么事件将不会向下传递,交由该view处理。如果该view消费了事件,那么接下来的事件也会交由该view处理;如果该view没有消费该事件,那么事件会交由外层view处理,...并最终调用到activity的onTouchEvent()中,除非某一层消费了该事件;

一个事件只能交由一个view处理;

dispatchTouchEvent()总是会被调用,而且最先被调用,onInterceptTouchEvent()和onTouchEvent()在dispatchTouchEvent()内部调用;

子view不能干扰ViewGroup对ACTION_DOWN事件的处理;

子view可以通过requestdisallowInterceptTouchEvent(true)控制父view不对事件进行拦截,跳过onInterceptTouchEvent()方法的执行。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

Android事件分发机制(上) ViewGroup的事件分发

Android事件分发机制(上) ViewGroup的事件分发

综述

  Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理。在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View。对于事件可以通过ViewGroup下发到它的子View并交由子View进行处理,而ViewGroup本身也能够对事件做出处理。下面就来详细分析一下ViewGroup对时间的分发处理。

MotionEvent

  当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成。
  1. ACTION_DOWN: 手指按下屏幕
  2. ACTION_MOVE: 手指在屏幕上移动
  3. ACTION_UP: 手指从屏幕上抬起
  例如一个简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP
  对于Android中的这个事件分发机制,其中的这个事件指的就是MotionEvent。而View的对事件的分发也是对MotionEvent的分发操作。可以通过getRawX和getRawY来获取事件相对于屏幕左上角的横纵坐标。通过getX()和getY()来获取事件相对于当前View左上角的横纵坐标。

三个重要方法

public boolean dispatchTouchEvent(MotionEvent ev)

  这是一个对事件分发的方法。如果一个事件传递给了当前的View,那么当前View一定会调用该方法。对于dispatchTouchEvent的返回类型是boolean类型的,返回结果表示是否消耗了这个事件,如果返回的是true,就表明了这个View已经被消耗,不会再继续向下传递。  

public boolean onInterceptTouchEvent(MotionEvent ev)

  该方法存在于ViewGroup类中,对于View类并无此方法。表示是否拦截某个事件,ViewGroup如果成功拦截某个事件,那么这个事件就不在向下进行传递。对于同一个事件序列当中,当前View若是成功拦截该事件,那么对于后面的一系列事件不会再次调用该方法。返回的结果表示是否拦截当前事件,默认返回false。由于一个View它已经处于最底层,它不会存在子控件,所以无该方法。   

public boolean onTouchEvent(MotionEvent event)

  这个方法被dispatchTouchEvent调用,用来处理事件,对于返回的结果用来表示是否消耗掉当前事件。如果不消耗当前事件的话,那么对于在同一个事件序列当中,当前View就不会再次接收到事件。   

 View事件分发流程图

  对于事件的分发,在这里先通过一个流程图来看一下整个分发过程。

 


ViewGroup事件分发源码分析

  根据上面的流程图现在就详细的来分析一下ViewGroup事件分发的整个过程。
  手指在触摸屏上滑动所产生的一系列事件,当Activity接收到这些事件通过调用Activity的dispatchTouchEvent方法来进行对事件的分发操作。下面就来看一下Activity的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
 onUserInteraction();
 }
 if (getwindow().superdispatchTouchEvent(ev)) {
 return true;
 }
 return onTouchEvent(ev);
}

  通过getwindow().superdispatchTouchEvent(ev)这个方法可以看出来,这个时候Activity又会将事件交由Window处理。Window它是一个抽象类,它的具体实现只有一个PhoneWindow,也就是说这个时候,Activity将事件交由PhoneWindow中的superdispatchTouchEvent方法。现在跟踪进去看一下这个superdispatchTouchEvent代码。

public boolean superdispatchTouchEvent(MotionEvent event) {
 return mDecor.superdispatchTouchEvent(event);
}

  这里面的mDecor它是一个DecorView,DecorView它是一个Activity的顶级View。它是PhoneWindow的一个内部类,继承自FrameLayout。于是在这个时候事件又交由DecorView的superdispatchTouchEvent方法来处理。下面就来看一下这个superdispatchTouchEvent方法。

public boolean superdispatchTouchEvent(MotionEvent event) {
 return super.dispatchTouchEvent(event);
}

  在这个时候就能够很清晰的看到DecorView它调用了父类的dispatchTouchEvent方法。在上面说到DecorView它继承了FrameLayout,而这个FrameLayout又继承自ViewGroup。所以在这个时候事件就开始交给了ViewGroup进行处理了。下面就开始详细看下这个ViewGroup的dispatchTouchEvent方法。由于dispatchTouchEvent代码比较长,在这里就摘取部分代码进行说明。

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
 // Throw away all prevIoUs state when starting a new touch gesture.
 // The framework may have dropped the up or cancel event for the prevIoUs gesture
 // due to an app switch,ANR,or some other state change.
 cancelAndClearTouchTargets(ev);
 resetTouchState();
}

  从上面代码可以看出,在dispatchTouchEvent中,会对接收的事件进行判断,当接收到的是ACTION_DOWN事件时,便会清空事件分发的目标和状态。然后执行resetTouchState方法重置了触摸状态。下面就来看一下这两个方法。

1. cancelAndClearTouchTargets(ev)

private TouchTarget mFirstTouchTarget;

......

private void cancelAndClearTouchTargets(MotionEvent event) {
 if (mFirstTouchTarget != null) {
 boolean syntheticEvent = false;
 if (event == null) {
  final long Now = SystemClock.uptimeMillis();
  event = MotionEvent.obtain(Now,Now,MotionEvent.ACTION_CANCEL,0.0f,0);
  event.setSource(InputDevice.soURCE_TOUCHSCREEN);
  syntheticEvent = true;
 }

 for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
  resetCancelNextUpFlag(target.child);
  dispatchTransformedTouchEvent(event,true,target.child,target.pointerIdBits);
 }
 clearTouchTargets();

 if (syntheticEvent) {
  event.recycle();
 }
 }
}

  在这里先介绍一下mFirstTouchTarget,它是TouchTarget对象,TouchTarget是ViewGroup的一个内部类,TouchTarget采用链表数据结构进行存储View。而在这个方法中主要的作用就是清空mFirstTouchTarget链表并将mFirstTouchTarget设为空。

2. resetTouchState()

private void resetTouchState() {
 clearTouchTargets();
 resetCancelNextUpFlag(this);
 mGroupFlags &= ~FLAG_disALLOW_INTERCEPT;
 mnestedScrollAxes = SCROLL_AXIS_NONE;
}

  在这里介绍一下FLAG_disALLOW_INTERCEPT标记,这是禁止ViewGroup拦截事件的标记,可以通过requestdisallowInterceptTouchEvent方法来设置这个标记,当设置了这个标记以后,ViewGroup便无法拦截除了ACTION_DOWN以外的其它事件。因为在上面代码中可以看出,当事件为ACTION_DOWN时,会重置FLAG_disALLOW_INTERCEPT标记。
  那么下面就再次回到dispatchTouchEvent方法中继续看它的源代码。

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
 || mFirstTouchTarget != null) {
 final boolean disallowIntercept = (mGroupFlags & FLAG_disALLOW_INTERCEPT) != 0;
 if (!disallowIntercept) {
 intercepted = onInterceptTouchEvent(ev);
 ev.setAction(action); // restore action in case it was changed
 } else {
 intercepted = false;
 }
} else {
 // There are no touch targets and this action is not an initial down
 // so this view group continues to intercept touches.
 intercepted = true;
}

  这段代码主要就是ViewGroup对事件是否需要拦截进行的判断。下面先对mFirstTouchTarget是否为null这两种情况进行说明。当事件没有被拦截时,ViewGroup的子元素成功处理事件后,mFirstTouchTarget会被赋值并且指向其子元素。也就是说这个时候mFirstTouchTarget!=null。可是一旦事件被拦截,mFirstTouchTarget不会被赋值,mFirstTouchTarget也就为null。
  在上面代码中可以看到根据actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null这两个情况进行判断事件是否需要拦截。对于actionMasked==MotionEvent.ACTION_DOWN这个条件很好理解,对于mFirstTouchTarget!=null的两种情况上面已经说明。那么对于一个事件序列,当事件为MotionEvent.ACTION_DOWN时,会重置FLAG_disALLOW_INTERCEPT,也就是说!disallowIntercept一定为true,必然会执行onInterceptTouchEvent方法,对于onInterceptTouchEvent方法默认返回为false,所以需要ViewGroup拦截事件时,必须重写onInterceptTouchEvent方法,并返回true。这里有一点需要注意,对于一个事件序列,一旦序列中的某一个事件被成功拦截,执行了onInterceptTouchEvent方法,也就是说onInterceptTouchEvent返回值为true,那么该事件之后一系列事件对于条件actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null必然为false,那么这个时候该事件序列剩下的一系列事件将会被拦截,并且不会执行onInterceptTouchEvent方法。于是在这里得出一个结论:对于一个事件序列,当其中某一个事件成功拦截时,那么对于剩下的一系列事件也会被拦截,并且不会再次执行onInterceptTouchEvent方法
  下面再来看一下对于ViewGroup并没有拦截事件是如何进行处理的。

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
 final float x = ev.getX(actionIndex);
 final float y = ev.getY(actionIndex);
 // Find a child that can receive the event.
 // Scan children from front to back.
 final ArrayList<View> preorderedList = buildOrderedChildList();
 final boolean customOrder = preorderedList == null
  && isChildrenDrawingOrderEnabled();
 final View[] children = mChildren;
 for (int i = childrenCount - 1; i >= 0; i--) {
 final int childindex = customOrder
  ? getChildDrawingOrder(childrenCount,i) : i;
 final View child = (preorderedList == null)
  ? children[childindex] : preorderedList.get(childindex);

 // If there is a view that has accessibility focus we want it
 // to get the event first and if not handled we will perform a
 // normal dispatch. We may do a double iteration but this is
 // safer given the timeframe.
 if (childWithAccessibilityFocus != null) {
  if (childWithAccessibilityFocus != child) {
  continue;
  }
  childWithAccessibilityFocus = null;
  i = childrenCount - 1;
 }

 if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x,y,child,null)) {
  ev.setTargetAccessibilityFocus(false);
  continue;
 }

 newTouchTarget = getTouchTarget(child);
 if (newTouchTarget != null) {
  // Child is already receiving touch within its bounds.
  // Give it the new pointer in addition to the ones it is handling.
  newTouchTarget.pointerIdBits |= idBitsToAssign;
  break;
 }

 resetCancelNextUpFlag(child);
 if (dispatchTransformedTouchEvent(ev,false,idBitsToAssign)) {
  // Child wants to receive touch within its bounds.
  mLastTouchDownTime = ev.getDownTime();
  if (preorderedList != null) {
  // childindex points into presorted list,find original index
  for (int j = 0; j < childrenCount; j++) {
   if (children[childindex] == mChildren[j]) {
   mLastTouchDownIndex = j;
   break;
   }
  }
  } else {
  mLastTouchDownIndex = childindex;
  }
  mLastTouchDownX = ev.getX();
  mLastTouchDownY = ev.getY();
  newTouchTarget = addTouchTarget(child,idBitsToAssign);
  alreadydispatchedToNewTouchTarget = true;
  break;
 }

 // The accessibility focus didn't handle the event,so clear
 // the flag and do a normal dispatch to all children.
 ev.setTargetAccessibilityFocus(false);
 }
 if (preorderedList != null) preorderedList.clear();
}

  对于这段代码虽然说比较长,但是在这里面的逻辑去不是很复杂。首先获取当前ViewGroup中的子View和ViewGroup的数量。然后对该ViewGroup中的元素进行逐步遍历。在获取到ViewGroup中的子元素后,判断该元素是否能够接收触摸事件。子元素若是能够接收触摸事件,并且该触摸坐标在子元素的可视范围内的话,便继续向下执行。否则就continue。对于衡量子元素能否接收到触摸事件的标准有两个:子元素是否在播放动画和点击事件的坐标是否在子元素的区域内。
  一旦子View接收到了触摸事件,然后便开始调用dispatchTransformedTouchEvent方法对事件进行分发处理。对于dispatchTransformedTouchEvent方法代码比较多,现在只关注下面这五行代码。从下面5行代码中可以看出,这时候会调用子View的dispatchTouchEvent,也就是在这个时候ViewGroup已经完成了事件分发的整个过程。

if (child == null) {
 handled = super.dispatchTouchEvent(event);
} else {
 handled = child.dispatchTouchEvent(event);
}

  当子元素的dispatchTouchEvent返回为true的时候,也就是子View对事件处理成功。这时候便会通过addTouchTarget方法对mFirstTouchTarget进行赋值。
  如果dispatchTouchEvent返回了false,或者说当前的ViewGroup没有子元素的话,那么这个时候便会调用如下代码。

if (mFirstTouchTarget == null) {
 // No touch targets so treat this as an ordinary view.
 handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}

  在这里调用dispatchTransformedTouchEvent方法,并将child参数设为null。也就是执行了super.dispatchTouchEvent(event)方法。由于ViewGroup继承自View,所以这个时候又将事件交由父类的dispatchTouchEvent进行处理。对于父类View是如何通过dispatchTouchEvent对事件进行处理的,在下篇文章中会进行详细说明。
  到这里对于ViewGroup的事件分发已经讲完了,在这一路下来,不难发现对于dispatchTouchEvent有一个boolean类型返回值。对于这个返回值,当返回true的时候表示当前事件处理成功,若是返回false,一般来说是因为在事件处理onTouchEvent返回了false,这时候变会交由它的父控件进行处理,以此类推,若是一直处理失败,则最终会交由Activity的onTouchEvent方法进行处理。

总结

  在这里从宏观上再看一下这个ViewGroup对事件的分发,当ViewGroup接收一个事件序列以后,首先会判断是否拦截该事件,若是拦截该事件,则通过调用父类View的dispatchTouchEvent来处理这个事件。若是不去拦截这一事件,便将该事件下发到子View当中。若果说ViewGroup没有子View,或者说子View对事件处理失败,则将该事件有交由该ViewGroup处理,若是该ViewGroup对事件依然处理失败,最终则会将事件交由Activity进行处理。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

关于COCOS-3.X事件分发机制-原理事件分发流程的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于Android事件分发机制三:事件分发工作流程、Android事件分发机制四:学了事件分发有什么用?、android事件分发机制的实现原理、Android事件分发机制(上) ViewGroup的事件分发的相关知识,请在本站寻找。

本文标签: