美文网首页
事件分发机制详解

事件分发机制详解

作者: wuchao226 | 来源:发表于2019-04-01 17:20 被阅读0次

    在上一篇文章 事件分发机制原理 中简要分析了一下事件分发机制的原理,原理是十分简单的,一句话就能总结:责任链模式,事件层层传递,直到被消费。

    常见事件


    既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。

    根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:

    事件 简介
    ACTION_DOWN 手指 初次接触到屏幕 时触发。
    ACTION_MOVE 手指 在屏幕上滑动 时触发,会多次触发。
    ACTION_UP 手指 离开屏幕 时触发。
    ACTION_CANCEL 事件 被上层拦截 时触发。

    对于单指触控来说,一次简单的交互流程是这样的:
    手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)

    • 本次事例中 ACTION_MOVE 有多次触发。
    • 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
    image

    事件分发、拦截与消费

    √ 表示有该方法。
    X 表示没有该方法。

    类型 相关方法 ViewGroup View
    事件分发 dispatchTouchEvent
    事件拦截 onInterceptTouchEvent X
    事件消费 onTouchEvent

    View 相关


    dispatchTouchEvent 是事件分发机制中的核心,所有的事件调度都归它管。不过我细看表格, ViewGroup 有 dispatchTouchEvent 也就算了,毕竟人家有一堆 ChildView 需要管理,但为啥 View 也有?这就引出了我们的第一个疑问。

    Q: 为什么 View 会有 dispatchTouchEvent ?

    我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也会有事件分发。

    Q: 与 View 事件相关的各个方法调用顺序是怎样的?
    A: 如果不去看源码,想一下让自己设计会怎样?

    • 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
    • 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
    • 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
    • View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)

    所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener。

    下面我们来看一下实际测试结果:

    手指按下,不移动,稍等片刻再抬起。

    [Listener ]: onTouchListener        ACTION_DOWN
    [View     ]: onTouchEvent           ACTION_DOWN
    [Listener ]: onLongClickListener  
    [Listener ]: onTouchListener        ACTION_UP
    [View     ]: onTouchEvent           ACTION_UP
    [Listener ]: onClickListener  
    

    可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。

    接下来就看一下源码是怎么设计的(省略了大量无关代码):

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            /** 
             * 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
             * 如果 onTouch 方法返回值为 true,就设置 result 为 true。
             */
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
          
            /** 
             * 如果 result 为 false,则调用自身的 onTouchEvent。
             * 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
             */
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        return result;
    }
    

    如果觉得源码还是太长,那么用伪代码实现应当是这样的(省略若干安全判断),简单粗暴:

    public boolean dispatchTouchEvent(MotionEvent event) {
      if (mOnTouchListener.onTouch(this, event)) {
          return true;
      } else if (onTouchEvent(event)) {
          return true;
      }
      return false;
    }
    

    OnClick 和 OnLongClick 的具体调用位置在 onTouchEvent 中,看源码(同样省略大量无关代码):

    public boolean onTouchEvent(MotionEvent event) {
        ...
        final int action = event.getAction();
        // 检查各种 clickable
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    removeLongPressCallback();  // 移除长按
                    ...
                    performClick();             // 检查单击
                    ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    checkForLongClick(0);       // 检测长按
                    ...
                    break;
                ...
            }
            return true;                        // ◀︎表示事件被消费
        }
        return false;
    }
    

    注意上面代码中存在一个 return true; 并且是只要 View 可点击就返回 true,就表示事件被消费了。

    举个栗子: I have a RelativeLayout,I have a View,Ugh,RelativeLayout - View

    <RelativeLayout
        android:background="#CCC"
        android:id="@+id/layout"
        android:onClick="myClick"
        android:layout_width="200dp"
        android:layout_height="200dp">
        <View
            android:clickable="true"
            android:layout_width="200dp"
            android:layout_height="200dp" />
    </RelativeLayout>
    

    现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件myClick,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 android:clickable="true" 正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:

      1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
      1. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

    ViewGroup 相关


    ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突。

    上一篇文章 事件分发机制原理 中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。

    • 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
    • 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
    • 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。

    用伪代码应该是这样的:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result = false;             // 默认状态为没有消费过
    
        if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
            result = child.dispatchTouchEvent(ev);
        }
    
        if (!result) {                      // 如果事件没有被消费,询问自身onTouchEvent
            result = onTouchEvent(ev);
        }
    
        return result;
    }
    

    1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
    这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
    2. 当该点的 ChildView 有重叠时应该如何分配?
    当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。
    如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。

    如下:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools" 
        android:id="@+id/activity_main"
        android:layout_width="match_parent" 
        android:layout_height="match_parent"
        tools:context="com.gcssloop.viewtest.MainActivity">
        <View
            android:id="@+id/view1"
            android:background="#E4A07B"
            android:layout_width="200dp"
            android:layout_height="200dp"/>
        <View
            android:id="@+id/view2"
            android:layout_margin="100dp"
            android:background="#BDDA66"
            android:layout_width="200dp"
            android:layout_height="200dp"/>
    </RelativeLayout>
    

    当手指点击有重叠区域时,分如下几种情况:

    1. 只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。
    2. 只有 View2 可点击时,事件将会分配给 View2。
    3. View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。

    注意:

    • 上面说的是可点击,可点击包括很多种情况,只要你给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable="true" 就代表这个 View 是可点击的。
      另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。
    • 给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件。

    3. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
    事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。

    4. 所有事件都应该被同一 View 消费
    在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。

    View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发

    安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
    如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。

    核心要点


    1. 事件分发原理: 责任链模式,事件层层传递,直到被消费。
    2. View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
    3. View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
    4. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
    5. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
    6. ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
    7. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
    8. 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
    9. 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
    10. 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

    本文参考来源:[https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B15%5DDispatch-TouchEvent-Source.md)

    相关文章

      网友评论

          本文标题:事件分发机制详解

          本文链接:https://www.haomeiwen.com/subject/pmvvbqtx.html