Android ViewGroup/View 事件分发机制详解

博客原文链接:https://zhujun2730.github.io/2015/11/08/touchevent/

对于大多数Android开发者来说,Android的事件分发机制一直以来都是一块心头病。似懂非懂的状态,应该是大多数人的真实写照。最近在看任玉刚老师写的《Android开发艺术探索》,算是做个读书笔记吧,希望能提供多一点启发、多一点角度的理解。

一、事件分发机制的一些概念

事件分发的本质:其实就是对MotionEvent事件的分发过程。

1.1 为什么要有事件机制 ?

当你在一个布局中,有一个LinearLayout,里面又有一个小的LinearLayout,然后在这个小的LinearLayout中又有一个View,这个时候。这个时候,你点击这个View,为什么LinearLayout不会响应?其实你点击的也是LinearLayout的区域啊。带着这个疑问,就可以猜想到了,事件分发机制,其实就是为了统一协调这些view的事件。

在这里安利一篇爱哥的《Android事件分发完全解析之为什么是她》,这篇较生动的讲解了事件机制的由来,十分推荐一看。

1.2 MotionEvent 主要分为以下几个事件类型:

  1. ACTION_DOWN 手指开始触摸到屏幕的那一刻响应的是DOWN事件
  2. ACTION_MOVE 接着手指在屏幕上移动响应的是MOVE事件
  3. ACTION_UP 手指从屏幕上松开的那一刻响应的是UP事件

所以事件顺序是: ACTION_DOWN -> ACTION_MOVE -> ACTION_UP

1.3 事件分发机制的三个主要方法:

  • public boolean dispatchTouchEvent(MotionEvent event) —— 分发事件

    作用是用来进行事件的分发。一般在这个方法里必须写 return super.dispatchTouchEvent 。如果不写super.dispatchTouchEvent,而直接改成return true 或者 false,则事件传递到这里时便终止了,既不会继续分发也不会回传给父元素。

  • public boolean onInterceptTouchEvent(MotionEvent event) —— 拦截事件

    只有ViewGroup才有这个方法。View只有dispatchTouchEvent和onTouchEvent两个方法。因为View没有子View,所以不需要拦截事件。而ViewGroup里面可以包裹子View,所以通过onInterceptTouchEvent方法,ViewGroup可以实现拦截,拦截了的话,ViewGroup就不会把事件继续分发给子View了,也就是说在这个ViewGroup中的子View都不会响应到任何事件了。onInterceptTouchEvent 返回true时,表示ViewGroup会拦截事件。

  • public boolean onTouchEvent(MotionEvent event) —— 消费事件

    onTouchEvent 返回true时,表示事件被消费掉了。一旦事件被消费掉了,其他父元素的onTouchEvent方法都不会被调用。如果没有人消耗事件,则最终当前Activity会消耗掉。则下次的MOVE、UP事件都不会再传下去了。

需要注意的一些事项:

  • 一般我们在自定义ViewGroup时不会拦截Down事件,因为一旦拦截了Down事件,那么后续的Move和Up事件都不会再传递下去到子元素了,事件以后都会只交给ViewGroup这里。
  • 一个Down事件分发完了之后,还有回传的过程。因为一个事件分发包括了Action_Down、Action_Move、Action_Up这几个动作。当手指触摸到屏幕的那一刻,首先分发Action_Down事件,事件分发完后还要回传回去,然后继续从头开始分发,执行下一个Aciton_Move操作,直到执行完Action_Up事件,整个事件分发过程便到此结束。

1.4 事件分发机制的三个主要方法的关系:

【注:ViewGroupA、ViewGroupB、View的布局结构参考下面的布局图】

当事件分发到ViewGroupA时,会执行到ViewGroupA的dispatchTouchEvent方法。刚刚提到了。在这里必须写成return super.dispatchTouchEvent(ev);因为事件的分发需要ViewGroupA 在父类ViewGroup的dispatchTouchEvent中才能进行事件分发。否则不这样写,事件根本无法继续分发下去。

class ViewGroupA {

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

}

在ViewGroup的dispatchTouchEvent源码中,简单化的归纳了事件分发的整个流程。该代码出自任老师之手。
【当consume 返回 true 时,表明事件已经被消费了。】

class ViewGroup {

    public boolean dispatchTouchEvent(MotionEvent ev){
        
        boolean consume = false;
        
        if(onInterceptTouchEvent(MotionEvent ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
        
        return consume;
    }

}

当ViewGroupA 的 onInterceptTouchEvent 方法返回true时,表示它要拦截事件,此时会执行它自己的onTouchEvent方法。当返回false时,表明它不想拦截,则事件会传递给子View child。于是开始执行child.dispatchTouchEvent(ev)。

我们来看View的dispatchTouchEvent方法。

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

从View的dispatchTouchEvent方法中可以得出一个结论:
事件一旦分发到了View,则默认一定会执行它的onTouchEvent方法,除非符合了if的三个条件

所以View的 onTouchEvent 方法如果返回true,则它的dispatchTouchEvent的返回值也会返回true。在ViewGroup 的dispatchTouchEvent 中则 consume 的值为true,表示事件被消费。

结论:View / ViewGroup 事件消费是在onTouchEvent方法中被消费的。

二、事件分发机制的流程

下面通过demo案例来演示,详细的说明事件分发的流程。ViewGroupA包裹ViewGroupB,ViewGroupB里面又包裹一个View。我们现在来分析下它的事件分发执行的流程。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">

    <me.anany.ViewGroupA
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_bright">

        <me.anany.ViewGroupB
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="@android:color/holo_green_dark">

            <me.anany.CustomView
                android:id="@+id/btn"
                android:text="Button"
                android:background="@android:color/holo_red_dark"
                android:layout_width="100dp"
                android:layout_height="100dp"
                />

        </me.anany.ViewGroupB>
    </me.anany.ViewGroupA>
</RelativeLayout>

2.1 点击View区域但View不消耗事件

当一个事件产生后,它的传递流程是从:Activity -> Window ->View

下图描述了,当点击View时,事件分发的执行流程、以及事件回传的流程。


流程图解析:

  • 事件分发

当在屏幕上点击一个View时,首先执行到的是MainActivity的dispatchTouchEvent方法,这里便是事件分发的起点。红色箭头流向便是事件分发的流向。

事件传递到ViewGroupA时,因为它不拦截事件,所以它要先去问它的子控件ViewGroupB是否要消费事件,然后将事件分发给ViewGroupB。事件到了ViewGroupB时,它不拦截事件,所以它也要先去问它的子控件们要不要消费事件,然后将事件分发给View。事件到了View时开始执行dispatchTouchEvent,因为已经到了最底层了,View接下来便开始执行onTouchEvent方法来决定是否消费事件。

  • 事件回传

由于View没有消费事件,所以它开始回传信息,(紫色箭头的流向便是事件回传方向),以告诉ViewGroupB我不消费事件了,view 的 onTouchEvent 便return false。然后ViewGroupB才开始有权利决定我是否要开始消费事件(因为它已经问过它的子控件是否要消费事件了,而它的子控件并没有消费),所以开始执行ViewGroupB的onTouchEvent方法,由于ViewGroupB也不消费事件,所以它也 return false 。事件继续回传给ViewGroupA,这个时候它终于开始有权利决定我是否要消费事件了,所以开始执行ViewGroupA的onTouchEvent方法,由于ViewGroupA也不感兴趣不消费事件,所以它也return false。最终你们这些孩儿们都不消费事件,那事件最终只能扔给MainActivity去消费了。

下面这段Log,便记录了事件分发的整个过程。由于ViewGroupA、ViewGroupB、View 在 Action_Down事件时,就没有消费事件。所以后续的事件MOVE、UP都只由MainActivity来处理了。


2.2 点击View区域且View消耗事件

流程图解析:

  • 事件分发

当在屏幕上点击一个View时,首先执行到的是MainActivity的dispatchTouchEvent方法,这里便是事件分发的起点。红色箭头流向便是事件分发的流向。

事件传递到ViewGroupA时,因为它不拦截事件,所以它要先去问它的子控件ViewGroupB是否要消费事件,然后将事件分发给ViewGroupB。事件到了ViewGroupB时,它不拦截事件,所以它也要先去问它的子控件们要不要消费事件,然后将事件分发给View。事件到了View时开始执行dispatchTouchEvent,因为已经到了最底层了,View接下来便开始执行onTouchEvent方法来决定是否消费事件。

  • 事件回传

由于View消费了事件,所以它开始回传,(紫色箭头的流向便是事件回传方向),以告诉ViewGroupB我已经消费事件了,view 的 onTouchEvent 便return true。然后ViewGroupB 收到了View return true 就知道事件已经被View消费掉了,所以不会执行ViewGroupB的onTouchEvent方法,只能往上回传 return true 去告诉ViewGroupA 事件已经被消费掉了,你没机会了 。然后事件继续回传给ViewGroupA,A收到return true 便知道 事件被消费了,所以它也return true。最终事件回传到了MainActivity,由于事件被消费了,所以不会执行MainActivity的onTouchEvent方法。接下来又开始执行Move事件了,流程又和之前的一样重新开始处理。

下面这段Log,便记录了事件分发的整个过程。当Down事件被View消费后,事件会重新开始从ViewGroupA、ViewGroupB 这样下来进行分发,直到UP事件结束。

结论:onTouchEvent被View消费后,ViewGroupA、ViewGroupB的onTouchEvent都不会执行

2.3 点击ViewGroupB区域但不消耗事件

这里的流程就不细说了,前面已经详细描述了两遍了。总的来说就是,ViewGroupB和ViewGroupA都不消费事件,那最终只能交给老大MainActivity去消费事件。

先看Log,为什么这里ViewGroupB并没有拦截View 但是View完全接受不到事件呢?


我们来看ViewGroup的dispatchTouchEvent源码

public boolean dispatchTouchEvent(MotionEvent ev) {  
    ...
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ... 
            if (isTransformedTouchPointInView(x,y,point))  {  
               if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
            }   
          }  
                   
        }  
    }  

从源码中可以看到,执行到if (isTransformedTouchPointInView) 这行代码时,就是去判断当前点击的坐标是否属于View的区域内,假如是,就开始执行View的dispatchTouchEvent方法。很显然在这里点击的ViewGroupB区域,并不在View的范围内,所以事件也不会分发到View。

2.4 点击View区域,View消耗事件,但设置了View.onTouchListener

设置View.onTouchListener中的onTouch()方法 return true。


当事件分发到View时,我们先来看View的dispatchToucnEvent源码:

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

看到没,当mOnTouchListener.onTouch(this, event)这个条件为true的时候,View的dispatchTouchEvent方法将直接return true。后续也不会执行View的onTouchEvent方法了。

结论:View的mOnTouchListener.onTouch方法优先于View的onTouchEvent方法被执行。

附上log:


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

推荐阅读更多精彩内容