Android View的事件分发机制

来源:互联网 发布:讯飞读屏软件下载 编辑:程序博客网 时间:2024/06/06 19:02

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的事件分发机制就能够对症下药,解决掉开发中遇到的各种奇怪冲突问题。



2 0
原创粉丝点击