美文网首页进阶Android 干货程序猿
Android ViewGroup/View 事件分发机制详解

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

作者: 明镜本清净anany | 来源:发表于2016-06-03 22:26 被阅读1190次

    博客原文链接: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时,事件分发的执行流程、以及事件回传的流程。


    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:


    相关文章

      网友评论

      • 相互交流:楼主问个问题,,你分析的是 某个方法最终返回的true ,,楼主有没有试过某个方法,,比如onInterceptTouchEvent,或者其他的某个方法,比如在down返回true,在move返回false,up 的时候,不同的返回会有是什么情况呢?合适会执行MOVE_CANCAL?
        有兴不虚昧:这个问题问的好,onInterceptToucEvent()在down返回ture,在move时返回false,这个viewGroup的子控件会只接收到一个cancel事件,并且这次move事件和后面的up事件都只会给这个viewGroup,不会往下传了
      • lxacoder:写得很好。问一下作者,五颜六色的Log怎么设置的啊
      • dodo_lihao:作者写得很好,学习到很多。
        谢谢。

        自己提出一个问题
        dispatchTouchEvent 如果直接return true,点击子View不会回传。
        但是,return false, 会回传。

        或者自己有不对的地方,您可以指出。
        KIMDA:@dodo_lihao 默认就是return false 啊, 然后执行onInterceptTouchEvent()方法啊兄弟
        dodo_lihao:@明镜本清净anany 我的意思是直接return false的情况
        明镜本清净anany:@dodo_lihao 我在文章后面提到了啊。View的dispatchTouchEvent方法如果直接return true,后续也不会执行View的onTouchEvent方法了,所以点击子View是无效的,因为事件没有传递到子View,所以也就没有回传。你可以再看看 View的dispatchTouchEvent源码。
      • Razerdp:不错^ω^
      • 苏村的南哥:老司基,学习了。

      本文标题:Android ViewGroup/View 事件分发机制详解

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