Android工资翻倍篇之事件分发机制


1 案例描写叙述

我们在开发中常常会遇到滑动冲突和点击冲突的情况,比方ScrollViewListView的滑动冲突。listViewbutton点击事件和ListView本身的点击事件冲突等。这些问题都是开发中会常常遇到的,处理起来也比較棘手。以下我们来具体的分析View的事件分发机制,从原理上弄清楚究竟是什么导致了冲突事件的发生。


2 案例分析

2.1 冲突事件重现

比方下图的蓝色区域是水平方向的scrollView,在scrollView中嵌套了listView(黄色区域),scrollVIew须要左右滑动,而listView须要上下滑动,这个时候假设你滑动屏幕,究竟是谁来处理这个滑动的事件,这个情况下就会出现滑动冲突了。想要实现两者都能滑动。就必须了解View的事件分发机制。

2.2 了解分发的事件MotionEvent

要了解View的事件分发机制。我们须要先了解MotionEvent事件,由于View的事件分发事实上就是分发的MotionEvent

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

ACTION_DOWN 手指刚接触屏幕

ACTION_MOVE 手指在屏幕上滑动

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

正常情况下,一次手指触摸屏幕的行为会触发一系列的点击事件。以下的两种情况

点击屏幕后松开,事件顺序为DOWN-UP;

点击屏幕有滑动一段距离后松开DOWN-MOVE-MOVE-MOVE……-UP

当中的MOVE事件会一直被触发,这是一个完整的事件序列。

同一时候能够通过MotionEvent对象得到点击事件发生的xy坐标,系统提供了两组方法getX/gety和getRawX/getRawY,分别返回相对于当前View左上角的xy坐标和相对于屏幕左上角的xy坐标,非常多事件的筛选推断须要用到这两组方法。

2.3 MotionEvent的传递规则

所谓点击事件的分发实际上就是对MotionEvent事件的分发过程,即当一个MotonEvent产生了以后。系统须要把这个事件传递给一个详细的View。而这个传递的过程就是分发的过程,点击事件的分发过程由三个非常重要的方法来共同完毕:dispatchTouchEvent、onInterceptTouchEventonTouchEvent

2.3.1 public boolean dispatchTouchEvent(MotionEvent ev)

这种方法用来进行事件的分发,假设事件可以传递给当前View,那么此方法一定会被调用,返回结果受当前ViewonTouchEvent和下级ViewdispatchTouchEvent方法的影响。表示是否消耗当前事件。

看以下View源代码中的dispatchTouchEvent方法。


从源代码中能够看出,当这个View设置了setOnClickListenersetOnTouchListener,他返回true,也就是说当View有自己的点击事件的时候他会消耗掉这个MotionEvent事件。

也就是说onTouchonClick事件是优先于onTouchEvent

2.3.2 public boolean onInterceptTouchEvent(MotonEvent ev)

该方法在dispatchTouchEvent方法中调用,用来推断是否拦截某个事件。假设当前View做了拦截。那么在同一个事件序列其中。此方法不会被再次调用,返回结果表示是否拦截当前事件。

以下看下ViewGroup中的该方法源代码,能够看出ViewGroup默认对全部事件都不会进行拦截。


2.3.3 public boolean onTouchEvent(MotonEvent ev)

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

2.3.4 三者之间的关系

以下我们通过一段伪代码来分析他们三者之间的关系


从上面的代码大致理解一下点击事件的传递规则:对于一个根ViewGroup来说。点击事件产生后。首先会传递给他,这时他的dispatchTouchEvent就会被调用,假设这个ViewGrouponInterceptTouchEvent方法返回true就表示他要拦截当前事件,这个事件就会交给这个ViewGroup来处理。他的onTouchEvent方法就会被调用,假设这个ViewGrouponInterceptTouchEvent返回false,他不进行拦截,这时当前事件就会继续传递给他的子元素。接着子元素的dispatchTouchEvent方法就会被调用。如此重复直到事件被终于处理

2.4 事件分发流程

当一个点击事件产生后,他的传递过程遵循例如以下顺序:Activity-Window-View

事件首先传递给activity,然后activity传递给window,最后window在传递给顶级的View。顶级View接收到事件后。就会依照事件的分发机制去分发事件。

比方这样的情况,当一个ViewonTouchEvent返回false,那么他的父容器的onTouchEvent将会被调用,依次类推,全部的元素都不处理这个事件。那么这个事件终于会交给ActivityonTouchEvent处理。就好像领导分发任务给经理,经理在把任务给组长,组长分发任务给组员(类似于在不做不论什么拦截的情况下将MotionEventViewGroup一直分发到最上层的子View)。假设组员处理不了(最上层子ViewonTouchEvent返回false)就会反馈给组长,组好处理不了就反馈给经理。经理处理不了就反馈给领导。View事件的分发机制如此类似。

2.5 事件分发的一些结论

以下给出一些结论,这些结论能够更好的帮助我们理解整个传递机制,这些结论都是从网上找的,是通过源代码解析得出的结论。

(1) 同一个事件序列是从MotionEventdown開始到up结束。中间含有数量不定的Move事件。

(2) 正常情况下,一个事件序列仅仅能被一个view拦截并消耗,由于一旦某个元素拦截了某个事件,那么同一个事件序列内的全部事件都会直接交给它处理,而且该元素的onInterceptTouchEvent方法不会再被调用了。

(3) 某个view一旦開始处理事件。假设它不消耗ACTION_DOWN事件(onTouchEvent反回了false),那么同一事件序列的其它事件都不会再交给它来处理。而且事件将又一次交给它的父容 器去处理(调用父容器的onTouchEvent方法),就好比领导交给你的第一件事你没有处理好,那么领导兴许的事情就不敢让你继续处理了;假设它消耗ACTION_DOWN事件。可是不消耗其它类型事件,那么这个点击事件会消失,父容 器的onTouchEvent方法不会被调用。当前view依旧能够收到兴许的事件,可是这些事件最后都会传递给Activity处理。

(4) 某个View一旦决定拦截。那么这一个事件序列都仅仅能由他来处理,而且他的onInterceptTouchEvent不会再被调用,这条的意思是说当一个View决定拦截一个事件后。那么系统会把同一个事件序列内的其它方法都直接交给他处理,因此就不会再调用这个View的onInterceptTouchEvent去询问他是否要拦截了。

(5) ViewGroup默认不拦截不论什么事件,能够看源代码

 

(6) View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。能够从2.3.4的伪代码去进行理解。

(7) ViewonTouchEvent默认都会消耗掉事件(返回true),除非它是不可点击的。

(8) 事件传递的过程是由内向外的。即事件总是先传递给父元素。然后再有父元素分发给子View,通过requestDisallowInterceptTouchEvent方法(请求父元素不同意拦截事件。意思就是请求将事件传递给自己)能够在子元素中干预父元素的事件分发过程,可是ACTION_DOWN事件除外。

3 解决过程

在回头看開始的滑动冲突问题时,我们再回头梳理一遍事件的分发机制。

点击事件首先到达顶级View(通常是一个ViewGroup,会调用ViewGroupdispatchTouchEvent方法,然后逻辑是这种:假设顶级的ViewGroup拦截事件onInterceptTouchEvent返回true,则事件由ViewGroup处理,这时假设ViewGroupmOntouchListener被设置则onTouch会被调用,否则onTouchEvent会被调用。假设顶级ViewGroup不拦截事件,则事件会传递给他所在的点击事件链上的字View,这时子ViewdispatchTouchEvent会被调用,然后循环以此类推完毕整个事件的分发。

3.1 冲突原因分析

了解了View的分发机制之后我们再来看开头提到的滑动冲突事件。因为首先接收到滑动事件的是顶级的ViewGroup,即ScrollView。我们能够看scrollView的源代码,发现拦截事件onInterceptTouchEvent方法对滑动事件进行了拦截。scrollView处理掉了滑动事件,那么listView就无法接收到滑动时间了。导致listView无法正常的实现滑动。

3.2 解决方法

知道了原因之后就非常好解决这个问题了。

当我们左右滑动的时候须要让HorizontalScrollView滑动,当上下滑动的时候须要让ListView滑动。所以我们能够依据滑动是水平滑动还是竖直滑动来推断究竟是由谁来拦截。我们能够通过getX/getY方法返回的坐标来得到水平和竖直方向滑动的距离,通过滑动的距离来分析能够推断是水平还是竖直滑动,假设水平距离大于竖直距离就判定为水平滑动,否则为竖直滑动。

 

设想父容器ScrollView在拦截方法里推断出水平滑动。然后就将滑动事件进行拦截。否则就不进行拦截。让子元素listView进行处理。

这样就能够解决滑动冲突问题了。

我们能够重写ScrollViewonInterceptTouchEvent方法。在内部做对应的拦截就可以。

 

伪代码:


4 解决结果

全部的滑动冲突都能够依据相应的业务需求用上述方法进行拦截。这里给出的仅仅是简单的事件冲突。可是解决这个问题的根本还是离不开上述的方法。

5 总结

理解了View的事件分发机制就行对症下药,解决掉开发中遇到的各种奇怪冲突问题。