Android - 事件分发机制

android事件分发机制的学习告一段落,先写篇文章做个总结,如有新的认识,后续再进行补充。

首先从两个问题引出android 的事件分发机制:

如下图,绿色部分A代表应用的一个填充父窗体的view对象,B 是 A 的子view,C 是 B 的子view,D 又是 C 的子view。
 
1、如果我们点击了D中有手势标注的地方,那么,A、B、C 和 D 中到底可以有几个view对象响应此次事件?
从我们的实际需求分析,用户在点击屏幕时都有明确的目的 —— 长按一个图标、点击一个button或是一个区域(比如一个LinearLayout),好像不太可能有用户在点击 D 视图的时候希望 D 及其父视图 C 同时做出响应吧!所以,这个问题的答案很明确,当用户点击屏幕上的某一个点时,只能是包含这个点的所有view对象中的一个来做出响应。
2、接着上边的问题 —— 当我们点击了 D 中有手势标注的地方,A、B、C 和 D 中到底由哪个view对象来响应此次事件?
针对这个问题,可以有两种解决方案:
第一种解决方案:从包含点击坐标的最小view对象开始,看这个view对象是否响应了此次事件,如果响应了,此次事件结束,如果没有响应,看它的父view是否响应,如果它的父view响应了,此次事件结束,如果它的父view没有响应,看它的父view的父view是否响应 ... ... ,直到应用程序的根view。
第二种解决方案:从应用程序的根 view 开始,遍历所有包含点击坐标的 view 对象,直到某一个 view 对象响应了此次事件。
一般来说,我们应该优先让最里层的view响应事件。
第一种解决方案,貌似可以,但它的处理逻辑不太好,不如第二种从最外层进行遍历的好。
第二种解决方案呢,处理逻辑是好的,比如在android的源码中,我们就可以发现很多类似的遍历做法,比如 view 的measure、layout 等。但是第二种解决方案有一个问题,如果从根view开始,遍历到B的时候,事件被B消费掉了,那C和D岂不是连发生了什么事件都不知道了,这种解决方案相当于是让外层的view对象优先响应事件,与我们的需求不符合。

那怎么办呢?
我们将两种解决方案揉在一起,从应用程序的根view开始遍历,先(不作处理)将事件派发给包含点击坐标的所有view对象,直到最里层。然后看最里层的view消不消费,不消费再往外层派发。

这就是android事件分发机制的大体描述,在详细分析之前,我们先来处理 view对象响应事件的问题

我们知道,常用的事件有 onClick、onLongClick ,可是这两种(包含其他很多常用的事件 —— 暂且把它们叫做组合事件)都是由单一事件 down、move 和 up 组合而成的。
当用户触摸到屏幕,可能是想调用onClick方法,也有可能是在接触屏幕的瞬间记下触摸点的坐标(响应DOWN事件)然后在移动过程中根据新的坐标来做一些其他处理(响应MOVE事件),也有可能是想长按一个view。
 所以,view对象响应事件的问题 大致可以等同于 处理 view 的 down、move、up、 onClick 和 onLongClick 事件的问题。
 

那怎么处理 down、move、up、 onClick 和 onLongClick 事件的关系呢?android 中的做法是:
public boolean dispatchTouchEvent(MotionEvent event) {  
   if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}

提供 OnTouchListener 接口,让码农们在onTouch方法中来处理单一事件down、move和up(当然也可以重写onTouchEvent方法来处理),在onTouchEvent方法中将down、move和up组合成onClick 和 onLongClick 事件(相关的源码分析网上有很好的文章可以参考,这里就不详细写了)。并且onTouch方法优先于onTouchEvent方法被执行onTouchEvent方法是否被执行又取决于onTouch方法的返回值,这是比较巧妙的,从事件发生的时间上来看,down事件刚发生时,是不可能触发onClick 和 onLongClick 的,move发生之后,也就不可能产生onLongClick了,而up发生后,才可能会有onClick。码农们可以让onTouch方法返回true,表示我只需要处理单一事件,不用再把down、move和up组合成onClick 和 onLongClick 。这种情况下,dispatchTouchEvent方法返回true,表示事件被响应了。如果onTouch方法返回false,才再去执行onTouchEvent方法,这种情况下dispatchTouchEvent方法的返回值就是onTouchEvent方法的返回值,只有onTouchEvent方法返回true才代表此次事件被响应了

 
接下来是onTouchEvent方法返回值的问题。
源码就不贴了,比较长,而且网上有不少分析得比较好的文章,这里就写结论吧:
我们在上文中说过,onTouchEvent方法的主要作用就是将down、move和up组合成onClick 和 onLongClick 组合事件。所以,一个clickable或者longclickable的View在onTouchEvent方法中是一定会返回true的,而一般的View既不是clickable也不是longclickable的(Button是clickable的),我们可以通过setClickable()或setLongClickable()来设置View为clickable或longClickable,或者如果我们为view设置了OnLongClickListener()或OnClickListener(),该view在onTouchEvent方法中也是会返回true的。
所以,综上所述,下列两种情况下,表示一个view对象响应了一次事件 :
一、设置了OnTouchListener,onTouch方法返回true
二、没有设置OnTouchListener或者设置了OnTouchListener但是onTouch方法返回false,并且onTouchEvent方法返回true(当View为clickable或longClickable

在处理完view的事件响应问题之后,我们来分析android的事件分发机制

当我们点击了屏幕,就会触发Activity的dispatchTouchEvent方法见源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    	// public void onUserInteraction(){}是一个空方法
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

getWindow().superDispatchTouchEvent()方法最终调用的是Window的子类PhoneWindow的superDispatchTouchEvent方法

public boolean superDispatchTouchEvent(KeyEvent event) {  
    return mDecor.superDispatchTouchEvent(event);  
// 执行的是DecorView类的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {  
    // 执行的是DecorView的父类FrameLayoutdispatchTouchEvent方法
    return super.dispatchTouchEvent(event);  
}
}
(关于DecorView及应用窗口层级关系参考Android - View的绘制流程一(measure)
我们看到,DecorView的SuperDispatchTouchEvent方法执行的是其父类FrameLayout中的dispatchTouchEvent()方法,
而FrameLayout中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法
(源码经过简化,只保留大致逻辑):
接收到一个ACTION_DOWN事件时,第3行的条件判断成立,于是:
第4到第8行,将mMotionTarget 设为null
第11行,判断是否为  不允许拦截或者允许拦截但不拦截,如果成立,则:
遍历子view,如果子view满足 VISIBLE 或者 正在执行动画 两个条件中的一个,进一步判断子view是否包含触摸点坐标,如果包含,则在第22行调用子view的dispatchTouchEvent()方法。

如果遍历完都没有哪个子view的dispatchTouchEvent()方法返回true,则代表ACTION_DOWN事件没有得到任何子view的响应,在这种情况下,就不会再接收到ACTION_MOVE和ACTION_UP事件了,mMotionTarget也就为null,于是target为null,执行39行DecorView的父类view的dispatchTouchEvent()方法,上文已经分析过,如果一个view没有设置OnTouchListener或设置了OnTouchListener但是onTouch方法返回false,并且这个view既不是clickable也不是longclickable的话,执行到39行就会返回false,于是Activity的onTouchEvent方法就会执行。

如果遍历过程中有子view的dispatchTouchEvent()方法返回true,则将该子view赋值给mMotionTarget,代表找到了一个响应ACTION_DOWN事件的子view对象,然后直接返回true,表示此次事件被响应了。
于是,Activity的dispatchTouchEvent方法也就返回true,而Activity的onTouchEvent方法就不会得到执行。
紧接着,在接收到ACTION_MOVE和ACTION_UP事件时,在第35行将mMotionTarget赋值给target后直接进入到第46行的判断:
如果允许拦截并且拦截了ACTION_MOVE和ACTION_UP事件,则将ACTION_CANCEL事件分发给target,也就是之前响应ACTION_DOWN事件的子view对象,然后直接返回true,表示已经响应了该次事件。
如果ACTION_MOVE和ACTION_UP事件没有被拦截,则直接派发给target,在target的dispatchTouchEvent()方法中进行处理,并根据其返回值来决定Activity的onTouchEvent方法要不要执行。

ViewGroup的dispatchTouchEvent()方法是android事件分发机制中一个很重要的方法,我们分成两条线路来进行分析:
 
第一条、是我们上边已经分析过的——从Activity的dispatchTouchEvent方法到DecorView的dispatchTouchEvent()方法
第二条、我们着重分析ViewGroup的dispatchTouchEvent()方法中遍历的过程
就像文章开头的图片所展示的一样,一般来说,android中的布局都为 ViewGroup嵌套ViewGroup和ViewGroup嵌套View两种形式,而根据上边的分析我们知道,在研究android事件传递机制时,更重要的是在一个嵌套的布局中是否有clickable或是longclickable的视图存在。
就用文章开头的图片,A、B、C都为ViewGroup对象,假设D为View对象,
下边来分析D在响应和未响应事件两种情况下A、B、C、D之间的事件传递:
当我们点击了有手势标注的地方,B 的dispatchTouchEvent()方法得到执行(从DecorView到A就不分析了,道理一样的)
 
上图中,最左边部分的序号和代码与上文中ViewGroup的dispatchTouchEvent()方法的源码所对应,红色、蓝色和紫色箭头分别代表DOWN、MOVE和UP事件的处理流程,关于方法的执行顺序,上图标注得比较清楚了,首先DOWN事件派发到B,如果不拦截,则走C的dispatchTouchEvent()方法,如果C也不拦截,则走D的dispatchTouchEvent()方法我们假设D为一个View对象,如果D是ViewGroup的话,在没有子view的情况下,会走到第39行,执行其父类View的dispatchTouchEvent()方法),
如果D的dispatchTouchEvent()方法返回true,则C的dispatchTouchEvent()方法也返回true,进而B的dispatchTouchEvent()方法也返回true ... ...
如果D的dispatchTouchEvent()方法返回false,那么C的dispatchTouchEvent()方法不会执行第27行,而是走第39行,执行其父类View的dispatchTouchEvent()方法,
如果返回true,C的dispatchTouchEvent()方法返回true,B的dispatchTouchEvent()方法也就返回true ... ...
如果返回false,C的dispatchTouchEvent()方法返回false,BdispatchTouchEvent()方法不会执行第27行,而是走第39行,执行其父类View的dispatchTouchEvent()方法 ... ... 以此类推

在上述过程中,如果D的dispatchTouchEvent()方法返回false,表示DOWN事件没有被响应,则不会再接收到后续的MOVE和UP事件。
如果D的dispatchTouchEvent()方法返回true,表示DOWN事件D响应了,则在C的dispatchTouchEvent()方法的第26行将D赋值给C的变量mMotionTarget,在BdispatchTouchEvent()方法的第26行将C赋值给B的变量mMotionTarget ... ... ,当MOVE和UP事件派发到B时,如果在第46行不被拦截的话,则在第61行将MOVE和UP事件派发给target — C处理,当然,如果C不拦截的话,又会在第61行将MOVE和UP事件派发给target — D处理 ... ...

从上文的分析可以看出,android事件分发机制的大致逻辑是:
当屏幕上接收到触屏事件后,不着急处理,
先从应用程序的根view开始遍历,将触屏事件分发给所有包含触屏点坐标的子view(代码的11到32行,当然这个过程中上层的view可以进行拦截,相关分析本文略过了)优先让最里层的子view来处理,如果最里层的子view不处理,再向外层抛,在这个过程中如果有哪一层做出了响应,则代表这次事件被消费了。
 

 (和上文内容重复)
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 这里是ACTION_DOWN的处理逻辑
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            // 每次ACTION_DOWN时,都将mMotionTarget 设为null
        	// mMotionTarget 是一个比较重要的变量,它不为null则表示找到了响应事件的view对象
            mMotionTarget = null;
        }
        // If we're disallowing intercept or if we're allowing and we didn't intercept
        // 如果不允许拦截或者允许拦截但不拦截,则执行下边的逻辑
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            // We know we want to dispatch the event down, find a child who can handle it, start with the front-most child.
        	// 将down事件分发下去,遍历,找到一个可以处理事件的子view
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                // 子view必须要是VISIBLE的或者正在执行动画才可以响应事件
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                	// 如果子view包含触摸点的坐标
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        if (child.dispatchTouchEvent(ev))  {
                        	// 调用子view的dispatchTouchEvent方法,如果返回true,则将child赋值给mMotionTarget 
                        	// 代表找到了一个响应事件的view对象,然后直接返回true
                            mMotionTarget = child;
                            return true;
                        }
                        // 如果执行到这里,说明ACTION_DOWN事件还没有被响应
                    }
                }
            }
        }
    }
    final View target = mMotionTarget;
    if (target == null) {
        // target == null,意味着没有找到能响应事件的子view,则调用ViewGroup父类View的dispatchTouchEvent方法
        return super.dispatchTouchEvent(ev);
    }
	// 无论 target 是否为 null ,ACTION_DOWN事件的处理都不能走到这里,在之下都是ACTION_MOVE和ACTION_UP的逻辑
	// 如果执行到这里,说明有响应ACTION_DOWN事件的view对象,这就看我们是否被允许拦截和要不要拦截了
	// 如果允许拦截并且拦截了ACTION_MOVE和ACTION_UP事件,则将ACTION_CANCEL事件分发给target
	// 然后直接返回true,表示已经响应了该次事件
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        if (!target.dispatchTouchEvent(ev)) {
            // target didn't handle ACTION_CANCEL. not much we can do but they should have.
        }
        return true;
    }
    // 如果没有拦截ACTION_MOVE和ACTION_UP事件,则直接派发给target
    return target.dispatchTouchEvent(ev);
}

  

 

posted on 2016-04-18 12:03  快乐的码农  阅读(229)  评论(0编辑  收藏  举报