Android|谈一谈Android中的事件分发

Android中的事件分发对很多人来说并不陌生,它可以说是Android的重点难点,也是面试经常会问的基础知识,很多人都搞不太清楚事件分发的过程。今天我们就来谈一谈Android中事件分发的过程原理。

事件
什么叫做一个事件?手指按下到抬起的一个过程称之为事件。在Android中把事件定义为,手指按下(DOWN),到手指抬起(UP),中间由零个、一个或者无数个移动(MOVE)连接起来称之为一个事件。主要发生Touch事件的有以下四种:
MotionEvent.ACTION_DOWN:按下View,即事件开始。
MotionEvent.ACTION_UP:抬起View,即事件结束。
MotionEvent.ACTION_MOVE:移动View。
MotionEvent.ACTION_CANCEL:事件取消,非人为因素造成事件取消。

事件分发作用的对象以及方法

事件分发作用的三个对象:
Activity:一般来说为系统层面上,其为最高级别的view。
ViewGroup:能够包裹子控件的都可以称之为ViewGroup,例如我们常使用的三大布局,LinearLayout、RelativeLayouy以及FrameLayout都可以称之为ViewGroup这个级别。
View:子控件,内部只能有自己本身,不能在包含其他控件。例如常用的TextView、Button以及ImageView都属于View这个级别。
当一个点击事件作用在view上时候,事件作用对象的过程为:Activity -> ViewGroup -> View

事件分发的三个方法:

  • dispatchTouchEvent():用于分发传递事件,当点击事件能够传递给当前View时候,此方法被调用。
  • onInterceptTouchEvent():用于判断事件是否拦截不往下传递(注意,此方法只在ViewGroup中才有,在Activity与View中并不存在此方法)。并且此方法在dispatchTouchEvent()中才会调用。
  • onTouchEvent():用于处理点击事件,此方法仍然是在dispatchTouchEvent()中调用。
如果以上三个方法按照优先级别排列,那么dispatchTouchEvent > onInterceptTouchEvent >onTouch。等于说只有在dispatchTouchEvent中事件同意进行分发,后续的事件才会传递到onInterceptTouchEvent与onTouchEvent中。这里用一段伪代码来理解三者之间的关系。
// 点击事件产生后,会直接调用dispatchTouchEvent()方法 public boolean dispatchTouchEvent(MotionEvent ev) {//代表是否消耗事件 boolean consume = false; if (onInterceptTouchEvent(ev)) { //如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件 //则该点击事件则会交给当前View进行处理 //即调用onTouchEvent ()方法去处理点击事件 consume = onTouchEvent (ev) ; } else { //如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件 //则该点击事件则会继续传递给它的子元素 //子元素的dispatchTouchEvent()就会被调用,重复上述过程 //直到点击事件被最终处理为止 consume = child.dispatchTouchEvent (ev) ; }return consume; }

onInterceptTouchEvent与onTouchEvent都包含在dispatchTouchEvent中,当if判断中的onInterceptTouchEvent返回为true(即代表拦截)之后才会执行onTouchEvent事件,否则继续传递给子元素的dispatchTouchEvent事件。

事件分发流程分析:
事件分发作用的无非就是三个对象Activity、ViewGroup以及View,作用的方法无非为三个方法dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。具体作用方法入下图所示:
Android|谈一谈Android中的事件分发
文章图片


而整个事件分发机制的流程如下图所示
Android|谈一谈Android中的事件分发
文章图片

其中:
  • super:调用父类方法
  • true:消费事件,事件不在继续往下分发
  • false:不消费事件,事件交给父控件的onTouchEvent去处理或向下传递

下面逐一分析流程。

dispatchTouchEvent()
作用对象为Activity、ViewGroup以及View。当事件点击时,首先被调用的就是这个方法,再根据这个方法的返回值来判断事件是直接消费掉还是向下分发。
  • true:代表消费掉这个事件,事件不会再往下分发而是直接结束掉。
  • super:向下传递给onInterceptTouchEvent,如果作用在Activity上,由于Activity没有onInterceptTouchEvent方法所以直接传递给子类的dispatchTouchEvent方法;如果作用在View上,则直接传递给自己的onTouchEvent方法去处理。
  • false:代表不消费掉这个事件,事件则被传递给父类的onTouchEvent去处理。由于Activity中没有父类,所以直接停止掉这个事件。
当down触发了这个事件之后,不管到底返回的是哪个,最后的MOVE或者UP都还会再经过一次这个方法,这点和onTouchEvent与onInterceptTouchEvent不一样。
onTouchEvent()
作用对象为Activity、ViewGroup以及View。只有当拦截器onInterceptTouchEvent返回true进行拦截或者dispatchTouchEvent返回false,才会触发这个方法。不过这里需要注意,onInterceptTouchEvent拦截之后是触发自身View的onTouchEvent方法,而dispatchTouchEvent返回false则是触发父类的onTouchEvent方法。
  • true:代表处理这个事件。则MOVE与UP操作才会被调用
  • super/false:代表不处理或者说没有能力处理这个事件。则传递给父类的onTouchEvent去处理。
这里需要明确一点,我们平时在使用click事件作为点击的时候,其实click事件是包含在onTouch事件里面的。在onTouch事件里面其实包含了onTouchEvent、performClick以及Click事件,而我们一般使用的click事件其实优先级别是最低了,只有当前面的事件返回false不处理才会轮到click去触发处理,如果前面有一个返回了true那么事件都会被消费掉,不会传递给click去处理。具体操作我们后面再讲。

onInterceptTouchEvent()
作用对象仅为ViewGroup。意为拦截操作。事件传递过来之后,根据返回值的不同来判断是否需要进行拦截操作。
  • true:代表拦截这个事件。事件不会再往下传递,而是交给了自身的onTouch去处理。此方法返回true时,一个事件只调用一次,等于说后面的MOVE与UP不会再执行onInterceptTouchEvent方法,而是直接去执行onTouch方法。
  • super/false:代表不拦截这个事件,事件传递给子类View的dispatchTouchEvent去处理。
在使用拦截方法onInterceptTouchEvent的时候,需要注意两种特殊情况。
  1. 一次事件中,该方法一旦被调用返回了true就不会在调用了。假设在Down事件被调用时返回了true,那么Move与Up事件便不会再去调用这个方法,而是直接去执行onTouchEvent方法。
  2. 假设ViewGroup没有拦截Down事件,而是拦截了Move事件,那么这个Down会直接传递给子类View,而后面的Move由于被拦截了,但是他并不会传递给自身的onTouchEvent方法,而是被作为一个Cancel方法传递给子类的View,而后面再来的Move则会被传递给自身的View去处理,而子类的View也不再会收到后续事件。

讲了这么多理论,下面来实际操作一下。
首先先写一个MyActivity作为Activity覆写其中的dispatchTouchEvent与onTouchEvent方法;
再写一个MyLinearLayout作为ViewGroup并覆写dispatchTouchEvent、onInterceptTouchEvent与onTouchEvent方法;
最后再写一个MyView作为View覆写dispatchTouchEvent与onTouchEvent方法。如下图所示:
Android|谈一谈Android中的事件分发
文章图片

当我们点击MyView时候,来看下Log日志的输入情况。
Android|谈一谈Android中的事件分发
文章图片

可以看到,当点击MyView的时候,首先调用的是MyActivity中的dispatchTouchEvent;然后向下分发调用MyLinearLayout中的dispatchTouchEvent与onInterceptTouchEvent方法;接下来到了MyView的dispatchTouchEvent与onTouchEvent方法中,在onTouchEvent中返回false或者super代表不处理这个事件,事件则向上传递给父类方法去处理,最后再传递回MyActivity的onTouchEvent去处理。

这里的事件解释起来貌似很复杂,其实理解起来并不复杂。这里做个形象的比喻,我们可以把Activity比喻为公司经理,ViewGroup为部门组长,而View为底层员工。当点击View的时候代表公司要开始开发一个新项目,这个时候公司经理先把需求交给部门组长(dispatchTouchEvent)——>部门组长接到任务之后查看是否需要自己来开发(onInterceptTouchEvent是否返回true,true代表交给自己处理,false代表自己不处理交给下属去开发)——>下属得到组长的新项目任务之后仔细审核了需求,如果觉得自己能够做则自己处理(onTouchEvent返回为true),如果发现自己的能力不够则交给比自己厉害的组长去开发(onTouchEvent返回false向上传递)——>组长接到了下属传递过来不能处理的任务之后看看自己能不能处理,能处理则自己处理(onTouchEvent返回true),如果发现这个任务太难了自己也不能处理那么就交给经理去处理(此时TouchEvent就返回false)——>最后经理拿到任务之后如果能做就自己做,不能做那么这个任务就作废因为公司能力有限做不了,到此整个分发结束。

在面试当中,面试官常常会考察ViewGroup中事件分发的几种情况处理,下面我们分别来谈论事件分发的几个情况。
在ViewGroup中当dispatchTouchEvent拦截了Down事件之后,后续的MOVE与UP事件如何执行?
我们先将dispatchTouchEvent中的代码改造如下:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.i("MyLinearLayout======","dispatchTouchEvent"); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: Log.i("MyLinearLayout======","MotionEvent.ACTION_DOWN"); return true; case MotionEvent.ACTION_MOVE: Log.i("MyLinearLayout======","MotionEvent.ACTION_MOVE"); case MotionEvent.ACTION_UP: Log.i("MyLinearLayout======","MotionEvent.ACTION_UP"); } return super.dispatchTouchEvent(ev); }

在dispatchTouchEvent中,在监听到Down事件的时候我们返回true,代表事件不分发,直接结束掉,此时我们移动手指头看看MOVE与UP事件如何处理。
Android|谈一谈Android中的事件分发
文章图片

可以看到,当点击Down返回true之后,事件会传递给上层Activity中的dispatchTouchEvent,之后的MOVE与UP事件仍然会继续传递到本ViewGroup中的dispatchTouchEvent方法里面处理。等于说,dispatchTouchEvent的返回值不论是true还是fasle并不影响后续MOVE与UP的事件操作,后续事件仍然会经过dispatchTouchEvent方法。特别注意这点与onTouchEvent和onInterceptTouchEvent方法不一样。

在onTouchEvent中返回true或者false对于后续MOVE、UP方法的影响?
【Android|谈一谈Android中的事件分发】我们将View中方法改造如下:
@Override public boolean onTouchEvent(MotionEvent event) { //Log.i("MyView======","onTouchEvent"); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: Log.i("MyView======","ACTION_DOWN"); return true; case MotionEvent.ACTION_MOVE: Log.i("MyView======","ACTION_MOVE"); return false; case MotionEvent.ACTION_UP: Log.i("MyView======","ACTION_UP"); } return super.onTouchEvent(event); }

myView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("onTouchListener======","onTouchListener"); return false; } }); myView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("setOnClickListener======","setOnClickListener"); } });

点击View看下如何执行。
Android|谈一谈Android中的事件分发
文章图片

我们看到执行顺序为onTouch ——> onTouchEvent ——> onTouchListener。而此时将onTouch中的返回值设置为true,再点击一下,输出结果
Android|谈一谈Android中的事件分发
文章图片

可以看到,此时只执行了onTouch事件,后面的时间并没有跟随执行。而我们吧onTouch设置为false,把onTouchEvent设置为true再来看看
Android|谈一谈Android中的事件分发
文章图片

看到执行了onTouch与onTouchEvent事件,onclick事件并没有执行。由此可见,在点击事件中,事件的优先级为 onTouch > onTouchEvent > onClick,当优先级高的事件返回true之后,优先级低的事件就不会再执行了,如果想要执行,必须优先级高的事件返回false才可以执行。

在onInterceptTouchEvent中返回true时,MOVE与UP事件如何执行?
onInterceptTouchEvent在使用的时候必须要知道两点内容:
  1. onInterceptTouchEvent一旦返回true,也就是被拦截之后后续的MOVE、UP方法就不会在执行这个方法了,而是直接去执行自身的onTouch方法。onInterceptTouchEvent一旦被拦截,一次事件只执行一次,后续MOVE与UP不再执行。
  2. 假设此时有一个ViewGroupA,还有一个子ViewB;而当ViewGroupA在进行拦截的时候,并没有去拦截Down事件,而是去拦截了ViewGroupA的Move事件,此时Down事件会直接去ViewB,而第一个MOVE事件不会传给自身的onTouchEvent事件,而是以一个Cancel传递给ViewB,而后面的MOVE才传递给自身的onTouch事件。
到此,事件分发所有内容讲解完毕~

Android|谈一谈Android中的事件分发
文章图片


    推荐阅读