Touch 事件分发机制

原文:http://xiazdong.me/2015/09/19/touch-dispatch-mechanism/

前言

Touch 事件分发机制是面试中非常常见的问题,也是非常重要的问题。网上有很多关于这方面的文章,但是感觉写的不是特别清晰易懂。

基本概念

Touch 事件分发机制分发的是 MotionEvent 对象,取值如下:

  • ACTION_DOWN: 按下事件。
  • ACTION_MOVE: 移动事件。
  • ACTION_UP: 抬起事件。

一个事件序列包含 ACTION_DOWN->ACTION_MOVE->...->ACTION_MOVE->ACTION_UP,即用户触摸屏幕,移动一些距离,然后抬起。

  1. 下面说的 view "处理" 了某个事件,表示 view 调用了 onTouchEvent()。
  2. 下面说的 view "消费" 了某个事件,表示 view 调用了 onTouchEvent() 并返回 true。因此某个 view 可以处理但不消费某个事件。

Touch 事件分发机制涉及三个方法:

  • dispatchTouchEvent(MotionEvent ev): 如果某个触摸事件传递给了某个 View 或 ViewGroup(设为 v),则一定会调用 v.dispatchTouchEvent(),如果 v 是 ViewGroup,则内部会调用 onInterceptTouchEvent() 或 onTouchEvent();如果 v 是 View,则内部会调用 onTouchEvent()。
  • onInterceptTouchEvent(MotionEvent ev): 这个方法只有 ViewGroup 才有,判断是否要拦截该事件并且自己处理,如果返回 true,则拦截;如果返回 false,则不拦截。
  • onTouchEvent(MotionEvent ev): 处理 Touch 事件的核心方法,如果返回 true,表示消费了事件;如果返回 false,则表示没消费该事件。

ViewGroup 的 dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent() 的基本关系如下:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){  //是否拦截
        consume = onTouchEvent(ev); //如果拦截,则自己处理
    }
    else{  //如果没拦截,则事件分发给孩子
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

上面的代码只是基本概括了整个事件分发的核心流程,具体实现细节会在下面介绍。

总体分发流程

当用户发起触摸事件后,首先触摸事件从 Activity 的 dispatchTouchEvent() 开始,该方法实现如下:

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

解释:

  • 其中 getWindow().superDispatchTouchEvent() 会调用 DecorView 的 dispatchTouchEvent() 开始分发,接着 DecorView 会调用根 View 进行事件分发。
  • 如果 getWindow().superDispatchTouchEvent() 返回 true,表示有子 View 消费了该触摸事件;如果返回 false,表示没有任何子 View 消费该事件,会调用 Activity 的 onTouchEvent(),即 Activity 自己处理 Touch 事件,并返回 false。

View Touch 事件分发

因为 ViewGroup 也是继承自 View,因此此处分两种情况讨论。

  • 如果是最底层的 View,一旦将触摸事件分发给他,就会调用下面的 dispatchTouchEvent();
  • 如果是 ViewGroup,则默认并不会调用下面的 dispatchTouchEvent(),而是会调用 ViewGroup 自己的 dispatchTouchEvent(),只有当 ViewGroup 在自己的 dispatchTouchEvent() 方法中经过判断,发现需要自己处理触摸事件时,才会通过 super.dispatchTouchEvent(ev) 的形式调用下面的 dispatchTouchEvent()。

View 的 dispatchTouchEvent() 实现如下:

public boolean dispatchTouchEvent(MotionEvent event) 
{
    if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) 
    {
        return true;
    }
    if (onTouchEvent(event)) 
    {
        return true;
    }
    return false;
}

从上面看出:

  • onTouchEvent() 不一定会被调用。如果设置了 OnTouchListener, View 是 enabled,并且 OnTouchListener 的 onTouch() 返回 true,则不会调用 onTouchEvent()。
  • 如果 View 是 DISABLED,则 onTouch() 不会被调用。
  • 如果 OnTouchListener 的 onTouch() 或 View 的 onTouchEvent() 返回 true,则 dispatchTouchEvent() 返回 true;否则返回 false。

接着我们看看 onTouchEvent() 的实现:

public boolean onTouchEvent(MotionEvent event) {
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                performClick();   //执行 mClickListener.onClick() 方法
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                break;
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}

从上面可以看出:

  • onTouchEvent() 内部在 ACTION_UP 事件中调用了 onClick()(即在一个事件序列中,只有在 ACTION_UP 才会调用 onClick(),但是 onTouch() 在每个事件都会被调用),即如果同时注册了 OnTouchListener 和 OnClickListener,则 OnTouchListener 优先级高于 OnClickListener,如果 onTouch() 返回 true,则 onTouchEvent() 不会执行,也就意味着 onClick() 不会执行。
  • 在 onTouchEvent() 中,只有 View 是 Clickable 的,才能进入 if 语句,而且一旦进入 if 语句就返回 true。比如 Button 是 Clickable 的,因此 onTouchEvent() 一定返回 true,ImageView 是不可点击的,因此 onTouchEvent() 一定返回 false。

ViewGroup Touch 事件分发

ViewGroup 的 dispatchTouchEvent() 比较复杂,下面我们分析一下。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    // 1、如果是 ACTION_DOWN 动作,则清除标志位。
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();  // 清除 FLAG_DISALLOW_INTERCEPT 标记位
    }

    // 2、判断是否要拦截该事件
    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);
        } else {
            intercepted = false;
        }
    }
    else{
        intercepted = true;
    }

    // 3、如果不拦截,则传给子 View
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
        if (actionMasked == MotionEvent.ACTION_DOWN){
              final View[] children = mChildren;
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = children[childIndex];
                //如果该子 View 不在触摸范围内,则略过
                if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                    continue;
                }
                //如果子 View 的 dispatchTouchEvent 返回 true,
                //          则表示有子 View 处理了该事件,则设置 mFirstTouchTarget
                //如果子 View 的 dispatchTouchEvent 返回 false,
                //          则表示没有子 View 处理了该事件,则不设置 mFirstTouchTarget
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //该方法中设置了 mFirstTouchTarget = child
                    newTouchTarget = addTouchTarget(child, idBitsToAssign); 
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }
    }

    // 4、判断是否有子 View 处理了事件
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } 
    else {
        if (alreadyDispatchedToNewTouchTarget){
             handled = true;
        }
        else{
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                handled = true;
            }
        }
    }

    // 5、如果是一个事件序列的最后一个操作(ACTION_UP 或 ACTION_CANCEL),则把状态清空
    if (canceled 
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    }
}

上面的代码中多处调用了 dispatchTransformedTouchEvent(),其中第三个参数有 null(第 52 行) 或者 child(第 40 行、第 59 行)。这个方法的实现如下:

public boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits){
    boolean handled = false;
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    return handled;
}

从上面代码可以看出,如果第三个参数传入 null,则调用 View 的 dispatchTouchEvent() 即自己处理事件;如果第三个参数传入 child,则将事件分发下去,即调用 child.dispatchTouchEvent()。

解释:

  • 第 2 行: handled 变量作为 dispatchTouchEvent() 的返回值。
  • 第 4-7 行: 如果是 ACTION_DOWN 操作,则将其状态清空,包括 FLAG_DISALLOW_INTERCEPT。我们可以通过 requestDisallowInterceptTouchEvent() 将 FLAG_DISALLOW_INTERCEPT 设置为 true,表示该 ViewGroup 禁止拦截操作(即直接将 intercepted 设为 false,不调用 onInterceptTouchEvent()),但是这个设置对于 ACTION_DOWN 无效,因为第 4-7 行会将该状态清除,即使设置了该状态,ACTION_DOWN 操作还是会调用 onInterceptTouchEvent()。
  • 第 10-22 行: 每个 ViewGroup 都会带有 mFirstTouchTarget 变量,这个变量只有在 ACTION_DOWN 事件时才能设置,这个能从第 28 行的 if 语句看出来,因为设置 mFirstTouchTarget 是在第 40-45 行(只有第 28 行的 if 语句为 true 才能执行第 40-45 行代码),可以看出如果有某个子 View 消费了该事件(这里不一定是直接子 View 消费了该事件,比如有 View 关系: v1->v2->v3,当前调用了 v1.dispatchTouchEvent(),如果 v3 消费了该事件,则表示 v1 的某个子 View 消费了该事件,并将 v1 的 mFirstTouchTarget 设置为 v2,将 v2 的 mFirstTouchTarget 设置为 v3),这样才能使第 40 行的 dispatchTransformedTouchEvent() 返回 true,并设置 mFirstTouchTarget。
  • 第 25-48 行: 如果当前是 ACTION_DOWN 事件(第 28 行)并且没有拦截(第 27 行,onInterceptTouchEvent() 返回 false),则会将 ACTION_DOWN 事件分发给合适的在触摸点的直接子 View,在第 40 行的 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 中会调用 child 的 dispatchTouchEvent()。如果 child.dispatchTouchEvent() 返回 false,表示 child 或 child 的子 View 没有人消费 ACTION_DOWN 事件,这样并不会给当前 ViewGroup 设置 mFirstTouchTarget。如果 child.dispatchTouchEvent() 返回 true,即表示 child 或 child 的子 View 有人消费 ACTION_DOWN 事件,则为当前 ViewGroup 设置 mFirstTouchTarget = child,并设置 alreadyDispatchedToNewTouchTarget = true,这个变量在后面会用到,表示是不是在这个方法中刚刚设置了 mFirstTouchTarget,因为只有 ACTION_DOWN 操作才能设置 mFirstTouchTarget,如果 alreadyDispatchedToNewTouchTarget == true && mFirstTouchTarget != null,则表示当前是 ACTION_DOWN 事件并且在该方法中刚刚设置了 mFirstTouchTarget;如果alreadyDispatchedToNewTouchTarget == false && mFirstTouchTarget != null,则表示 mFirstTouchTarget 并不是该方法中刚刚设置的,即当前不是 ACTION_DOWN 事件。
  • 第 51-53 行: 有两种情况会进入第 51 行的 if 语句,(1)当前是 ACTION_DOWN 操作,并且没有子 View 处理该事件 (2)当前不是 ACTION_DOWN 操作,并且在前面的 ACTION_DOWN 操作时没有子 View 处理该事件。第 52 行的第三个参数为 null,因此会执行 super.dispatchTouchEvent(),即执行 View 的 dispatchTouchEvent(),表示自己处理该事件。
  • 第 55-57 行: 因为只有当当前为 ACTION_DOWN 操作并且有子 View 处理了该事件时,alreadyDispatchedToNewTouchTarget 才为 true,这时直接将 handled 设为 true,不需要做额外的操作。
  • 第 59-61 行: 能够进入第 58 行的 else 语句意味着当前事件不是 ACTION_DOWN 并且在前面的 ACTION_DOWN 事件存在子 View 处理了该事件(即 mFirstTouchTarget != null,即在前面的 ACTION_DOWN 事件执行过第 40-46 行代码)。此时就执行 dispatchTransformedTouchEvent(),内部会调用 mFirstTouchTarget.dispatchTouchEvent()。
  • 第 66-69 行: 做收尾工作。

一些结论的验证

在网上有很多关于 Touch 事件的结论,这些结论其实都可以通过分析上面的代码得出。这里举几个例子:

  • "某个 view 一旦开始处理事件,如果不消费 ACTION_DOWN 事件,则同一事件序列中的其他事件不会再交给它来处理": 如果 view 处理但不消费 ACTION_DOWN 事件,则表示执行了第 40 行代码,但是返回 false(这个 view 作为 child 传入),设这个 view 的父 view 为 v0,此时就没设置 v0 的 mFirstTouchTarget。因此接下来的事件(比如 ACTION_MOVE) 一旦分发到 v0,因为他的 mFirstTouchTarget == null,因此会执行第 52 行代码,即 v0 自己处理该事件。
  • "某个 view 一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会被调用": 因为 view 拦截了事件,因此第 27 行的 if 语句进不去,也就设置不了 view 的 mFirstTouchTarget,接下来的事件分发给 view 时,执行到第 11 行的 if 语句,因为该事件不是 ACTION_DOWN 并且 view 的 mFirstTouchTarget == null,因此 if 语句返回 false,即执行第 21 行设置 intercepted = true,因此不会调用 onInterceptTouchEvent;接着执行到第 51 行,因为 view 的 mFirstTouchTarget 为 null,因此执行第 52 行代码,即自己处理该事件。

参考文献

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

推荐阅读更多精彩内容