Android 事件分发机制源码和实例解析

  1. 事件分发过程的理解
    1.1. 概述
    1.2. 主要方法
    1.3. 核心行为
    1.4. 特殊情况

  2. 案例分析
    2.1. 案例1:均不消费 down 事件
    2.2. 案例2:View0 消费 down 事件
    2.3. 案例3:ViewGroup2nd 消费 down 事件

  3. down 事件分发图

1. 事件分发过程的理解

1.1. 概述

事件主要有 down (MotionEvent.ACTION_DOWN),moveMotionEvent.ACTION_MOVE),upMotionEvent.ACTION_UP)。
基本上的手势均由 down 事件为起点,up 事件为终点,中间可能会有一定数量的 move 事件。这三种事件是大部分手势动作的基础。

事件和相关信息(比如坐标)封装成 MotionEvent

大体的分发过程为:首先传递到 Activity,然后传给了 Activity 依附的 Window,接着由 Window 传给视图的顶层 View 也就是 DecorView,最后由 DecorView 向整个 ViewTree 分发。分发还会有回溯的过程。最后还会回到 Activity 的调用中。

Activity 的分发事件源码

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

getWindow().superDispathTouchEvent 就是用来分发事件到 DecorView 中。如果整个 ViewTree 没有消费事件,会调用 Activity 的 onTouchEvent

1.2. 主要方法

1.2.1. 概览

主要涉及到的 View 或 ViewGroup 的方法有:

dispatchTouchEvent该方法封装了事件分发的整个过程。是事件分发的 调度者指挥官 。的核心过程均在该方法中。下面的 onInterceptTouchEventonTouchEvent 的回调的调用就在该方法体中。是否传递事件到
onInterceptTouchEventonTouchEventdispatchTouchEvent 决定。

onInterceptTouchEvent该方法决定了是否拦截事件。只有 ViewGroup 有该回调。返回 true 表示拦截,返回 false 表示不拦截。自定义 View 的时候,可以重载该方法,通过一些特定的逻辑来决定是否拦截事件。如果拦截,接下来会调用该 ViewGroup 的 onTouchEvent 来处理事件。

onTouchEvent该方法处理了事件,并决定是否继续消费后续事件。该方法调用的前置条件:

  • 该 View 拦截了事件
  • 子 View 都不消费事件
  • 没有子 View

该方法正式处理 MotionEvent。返回 true 表示消费,返回 false 不消费。如果消费,接下来的事件还会传递到该 View 的 dispatchTouchEvent 中;如果不消费,后面的事件不会再传过来。

onTouchListeneronTouch 回调,和 onTouchEvent 一样,优先级比 onTouchEvent 高,如果有设置该监听,并且 onTouch 返回 true,就不会再调用 onTouchEvent 了。如果返回 false,事件还是会传递到 onTouchEvent 中。

<h4 id="1.2.2"> 1.2.2. dispatchTouchEvent 方法中的一些细节处理:</h4>

大部分手势的起点为 down 事件,dispatchTouchEvent 如果收到 down 事件,会重新设置一些变量和标记

重置变量和标记的源码

// 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();
}

实际的源码中,ViewGroup 继承于 View。 当子 View 不消费事件或者 ViewGroup 拦截了事件会传空值到 dispatchTransformedTouchEvent 中,内部会调用 super.dispatchTouchEvent,最终把事件传给 onTouchEvent 进行处理。

dispatchTransformedTouchEvent 关键部分

// 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);
}

也就是 dispatchTransformTouchEvent 完成了分发的最后过程:
a. 传入的 child 不为空,转化坐标为 child 的坐标系,调用 child.dispatchTouchEvent 向 child 分发事件
b. 传入的 child 为空,调用 super.dispatchTouchEvent 分发事件到 onTouchEvent

<h4 id="1.2.3"> 1.2.3 方法的主要关系 </h4>

对于一个 ViewGroup 来说,几个重要方法的关系如下

几个重要方法关系伪代码

public boolean dispatchTouchEvent(MotionEvent e) {
    boolean consumed = false;
    if (onInterceptTouchEvent(e)) {
        consumed = onTouchEvent(e);
    } else {
        for (View view: childs) {
            consumed = view.dispatchTouchEvent(e);
            if (consumed) {
                break;
            }
        }
        if (!consumed) {
            consumed = onTouchEvent(e);
        }
    }   
    return consumed;
}

这是事件分发过程的简单描述,具体远比这复杂的多。

1.3. 核心行为

View 或 ViewGroup 有两个核心的行为:拦截(intercept)消费(consume)。这两者是相互独立的,拦截不一定消费。是否要拦截看 onIntercepTouchEvent。是否要消费看 onTouchEvent

注意:是否拦截还有其他因素影响。如果不是 down 事件,并且 mFirstTouchTarget 为空值,就会直接拦截事件。

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;
}

从上面的源码可以看出,在不是 down 事件,并且 mFirstTouchTarget 为空的情况下,不会走 onInterceptTouchEvent 而是直接拦截。如果满足了,还会看 FLAG_DISALLOW_INTERCEPT 标记,如果不允许拦截(disallowIntercept 为 true),也不会走 onInterceptTouchEvent,直接标记不拦截。

处理调用 onTouchEvent 的源码

boolean result = false;

...
 
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;
}

可以看出,在该 View 为 ENABLE 的状态并且有 mTouchListener,会先调用 onTouch。在 onTouch 返回 false 时才会继续调用 onTouchEvent

onTouch 或者 onTouchEvent 的处理结果有:

  • 返回 true,会继续消费后续事件。意味着,后面的事件将会继续传递到该 View 的 dispatchTouchEvent 方法中进行调度。父 View 会为该 View 创建一个 TouchTarget 实例加入链表中,链表的第一项为 mFirstTouchTarget。后续的 move 和 up 事件会直接交给该 View 的 dispatchTouchEvent
  • 返回 false,不再消费后续事件。意味着,后面的事件将会被父 View 拦截,而不再传递下来。

1.4. 特殊情况

比较特殊的情况有,子 View 可以使用 requestDisallowInterceptTouchEvent 影响去父 View 的分发,可以决定父 View 是否要调用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用调用 onInterceptTouchEvent 来判断拦截,而就是不拦截。

该方法可以用来解决手势冲突。比如子 View 先消费了事件,但是后面父 View 也满足了手势触发的条件而拦截事件,导致子 View 手势执行一半后无法继续响应。可以使用 requestDisallowInterceptTouchEvent(true),这样后面的事件,父 View 不会走 onInterceptTouchEvent 回调来判断是否要拦截事件,而是直接把事件继续传下来。

2. 案例分析

下面举三个简单的例子,三个类 ViewGroup1st,ViewGroup2nd 和 View0,层级关系为

<ViewGroup1st>
    <ViewGroup2nd>
        <View0 />
    </ViewGroup2nd>
</ViewGroup1st>

这三个类有两层 ViewGroup,最底层为 View,这几个例子主要理解 消费 行为,所以不做事件的拦截。

2.1. 案例1:均不消费 down 事件

在触摸屏幕中 View0 的区域后,输出 log 信息如下

12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:false

当 down 事件从 DecorView 开始了分发过程:

ViewGroup1st 收到事件,执行 onInterceptTouchEvent 返回 false,不拦截,于是调用 ViewGroup2nd 的 dispatchTouchEvent 向 ViewGroup2nd分发。

ViewGroup2nd 收到事件,dispatchTouchEvent 重复 ViewGroup1st 的分发策略。因为都不拦截,所以调用了 View0 的 dispatchTouchEvent

View0 收到事件,而 View0 不是 ViewGroup 类型,所以把事件直接交给了 onTouchEvent

View0 不消费事件,onTouchEvent 返回 false,dispatchTouchEvent 方法因此也返回 false。

ViewGroup2nd 因为 View0 的 dispatchTouchEvent 返回 false,确定了子类不消费事件,于是把事件传递给 onTouchEvent。但本身也不消费事件,所以 onTouchEvent 也返回 false,继续把事件上抛到 ViewGroup1st。

ViewGroup1st 重复了 ViewGroup2nd 的过程。

随后,move 事件不会再往下传了,而是直接被 Activity 拦截。

2.2. 案例2:View0 消费 down 事件

首先是 down 事件的传递,log 如下

12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

ViewGroup1st 和 ViewGroup2st 的传递和案例1一样。区别在于 View0 onTouchEvent 返回 true 消费后续事件后,View0 的 dispatchTouchEvent 也返回 true,ViewGroup2nd 和 ViewGroup1st 不执行 onTouchEvent 也直接返回 true

然后稍微移动一下手指,move 事件往下传递

12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

过程和 down 事件的传递一样。因为同样会经过 ViewGroup2nd 的 onInterceptTouchEvent,如果这时候 ViewGroup2nd 有拦截行为,move 事件就不会传到 View0 了。要避免这种情况发生,需要调用 View0 的requestDisallowInterceptTouchEvent,可见 1.4 部分。

2.3. 案例3:ViewGroup2nd 消费 down 事件

首先是 down 事件的传递,log 如下

12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: onTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

由于 View0 不消费事件,dispatchTouchEvent 返回 false,所以执行了 ViewGroup2nd 的 onTouchEvent 方法。

ViewGroup2nd 消费事件,onTouchEvent 返回 true,之后 ViewGroup2nd 和 ViewGroup1st 的 dispatchTouchEvent 均返回 true。

动一下手指,move 事件接着传

2-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true

这时候,ViewGroup2nd 直接拦截了 move 事件,不再经过 onInterceptTouchEvent,也不再向 View0 分发,而是直接调用 onTouchEvent 进行处理。

3. down 事件分发图

在每个 View 都不拦截 down 事件的情况下,down 事件是这样传递的

down 事件的分发过程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容