Android中View的事件分发机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyz_zyx/article/details/80214868

简介

前面我们介绍过View的工作原理以及如何自绘一个View,这些都是View在屏幕上展现的一些故事,今天我们继续来谈谈View的另外方面的同样重要的知识,那就是事件分发,或者叫事件传递规则。也就是说,当我们在屏幕上绘制出看到一个View后,想对对它做一些点击、滑动等操作中,系统是如何把这个事件传递到这个View中,这个传递过程就是分发过程。

认识MotionEvent

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

ACTION_DOWN

手指刚接触屏幕

ACTION_MOVE

手指在屏幕上移动

ACTION_UP

手指从屏幕上松开的一瞬间

上述三种情况是典型的事件序列,同时通过MotionEvent对象可以得到点击事件发生的x和y坐标。为此,系统提供了两组方法:getX/getY(返回相对于当前View左上角的坐标)和getRawX/getRawY(返回相对于手机屏幕左上角的坐标)。

decor view

decor view又叫底层View、顶级View 或 根View。一般就是当前界面的底层容器,也就是我们在Activity中的onCreate中使用setContentView所设置的View的父View。

通过下面代码可获取decor view:

View view = ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

点击事件的传递规则

所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程。当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window ->decor View -> View -> 子View ->子Viewr的子View…,如果一个decor View接收到事件后,就会按照事件分发机制去发分事件。事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEventonInerceptTouchonTouchEvent

public booleandispatchTouchEvent(MotionEvent ev)

         用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

 

public boolean onInterceptTouchEvent(MotionEventevent)

         在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

 

public boolean onTouchEvent(MotionEventevent)

         在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

它们三个方法的关系可以用如下伪代码表示:

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

说明:

例如,对于一个ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截(处理)当前事件,即它的onTouchEvent方法就会被调用;如果onInterceptTouchEvent方法返回false,就表示不拦截当前事件,这时当前事件会继续传递给它的子元素。

那么,如果事件传递到最终的子View后,View不想处理,即View的onTouchEvent返回false时又当如何?这时有意思的是,它的父容器的onTouchEvent将会被调用,依此类推。如果所有的上层View都不处理这个事件,那么最终传递还给Activity处理,即Activity的onTouchEvent方法会被调用。

事件传递的机制,请记住如下总结:

1、同一个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2、正常情况下,一个事件序列只能被一个View拦截且消耗。但是通过特殊手段可以做到同一个事件序列中的事件可分别由两个View同时处理,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

3、某个View一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的onInterceptTouchEven不会被调用,因为View决定拦截事件后,系统会把同一个事件序列内的其他方法都直接交给它来处理,因此不必再调用onInterceptTouchEven去询问。

4、某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理。

5、 如果View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEven并不会被调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件传递给Activity处理。

6、ViewGroup默认不拦截任何事件。源码中ViewGroup的onInterceptTouchEvent方法默认返回false。

7、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

8、View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。longClickable默认为false。clickable要分情况,比如Button为true,TextView为false。

9、View的enable属性不影响ouTouchEvent的默认返回值。哪怕是disable状态,只要它clickable和longClickable有一个为true,那么onTouchEvent就返回true。

10、onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件

11、事件传递过程是由外向内的,即总是先传递给父元素,然后再分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外。关于requestDisallowInterceptTouchEvent的说明,我们会在后面文章中再介绍。

源码验证

Activity对点击事件的分发过程

上面讲过,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行派发,具体工作由Windows来完成。然后Windows会将事件传递给decor view。那我们就从Activity的dispatchTouchEvent开始,打开Activity源码可见如下代码片段:


可以看到,事件开始是交给了Activity所附属的Window进行分发,如果返回true,整个事件循环就结束,返回false意味着事件没人处理,即所有view的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。

Window对点击事件的分发过程

Window是一个抽象类,而它的唯一实现类是android.policy.PhoneWindow,所以接下来,我们继续看看PhoneWindow类的superDispatchTouchEvent方法:


这里的逻辑就比较简单清晰了,就是将事件传递给mDecor,而mDecor便是DecorView对象。

DecorView对点击事件的分发过程

从源码可见,DecorView其实是一个ViewGroup,DecorView的superDispatchTouchEvent方法代码:


ViewGroup对点事事件的分发过程

这里代码比较多也比较复杂,我们且来看下上面圈红的地方,那就是我们上面提到的onInterceptTouchEvent方法,该方法返回一个boolean类型变量,若为true,则事件由ViewGroup自己处理,否则事件传递给它所在的点击事件链上的子View,继续看dispatchTouchEvent方法下面的代码,会发现在intercepted变量为false时决定的一个分支代码中有一个dispatchTransformedTouchEvent方法:

继续看下去,dispatchTransformedTouchEvent代码如下:

我们终于找到了child.dispatchTouchEvent,事件就是通过这样又再次被传递到子View中去。

View对点击事件的处理过程

继续到View中去看dispatchTouchEvent代码:


这样大概可以看到,当mOntouchListener.onTouch方法返回false时,那么onTouchEvent才会被调用。由于往下走整个流程源码实在太多,至于onTouchEvent里做了什么事情,这样就不再咯嗦,大家有空可自行查阅源码。

到此,我们已经大概清楚知道了点击事件的一个分发过程了!


 ——本文内容参考自《Android开发艺术探索》


猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/80214868