前言
前段时间在开发功能时,遇到了许多特殊的需求,比如ViewPager嵌套Viewpager,ScrollView嵌套RecyclerView等,都产生了相应的滑动冲突,因此写这篇博客去深入学习一下View中的一些事件体系,以及View的工作原理。
View的一些基本知识
1. View的位置参数:
View的四个属性top、left、right、bottom分别对应mTop、mLeft、mRight、mBottom,它们都有对应的get方法。需要注意的是这四个坐标都是相对于View的父容器来说的,是一种相对坐标。
2. MotionEvent和TouchSlop
MotionEvent指手指接触屏幕产生的一系列事件,典型的事件有以下几种:
ACTION_DOWN 手指刚接触屏幕
ACTION_MOVE 手指在屏幕移动
ACTION_UP 手指从屏幕松开的瞬间
通过MotionEvent对象可以知道点击事件发生的坐标,有两组方法
getX/getY
和getRawX/getRawY
它们的区别是前者返回相对于view左上角的x和y坐标,后者返回相对于手机屏幕左上角的x和y坐标。
TouchSlop是系统能识别出的的被认为是滑动的最小距离,也就是说如果滑动的距离小于这个常量,那么系统就不认为你在进行滑动操作。这是一个常量,和设备有关,在不同设备上这个值可能不一样。获取方法为:ViewConfiguration.get(getContext()).getScaledTouchSlop()
3. VelocityTracker、GestureDetector和Scroller
- VelocityTracker 速度追踪,用于追踪手指在华东中的速度,包括水平和竖直方向的速度
- GestureDetector 手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
- Scroller 弹性滑动对象,用于实现View的弹性滑动。
View的事件分发机制
上面介绍了一些View的基本知识,接下来是比较重要的View的事件分发机制,项目中遇到的滑动冲突事件的解决基本上都是通过对View的事件的分发来完成的,比较重要。
1. 点击事件传递规则
首先要明白的一点是,我们的分析对象是MotionEvent,即点击事件。所谓的事件分发,其实就是对MotionEvent事件的分发过程,当一个MotionEvent产生之后,系统需要把这个事件传递给一个具体的View,这个传递的过程就是分发过程。其中涉及到3个很重要的方法来共同完成事件分发:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面分别介绍这3个方法。
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件分发,如果事件能够传递给当前View,那么会调用此方法,返回结果表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前的View拦截了某个事件,那么在同一个事件序列中,该方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
上述3者的关系可以用如下伪代码表示:
|
|
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true则表示要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的ontouchEvent方法就会被调用。如果onInterceptTouchEvent返回false则表示它不拦截当前事件,那么事件就会继续传递给子元素,然后子元素的dispatchTouchEvent方法被调用,如此反复直到事件被最终处理。
一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时候需要看onTouch的返回值,如果返回false,则当前View的onTouchEvent会被调用,反之则不调用。由此可见OnTouchListener的优先级高于onTouchEvent,而在onTouchEvent中会有OnClickListener的回调,从而可以看出OnClickListener的优先级最低,处于事件传递的尾端。
一个点击事件产生后,它的传递顺序如下:Activity -> Window -> View。分发机制就是上面所讲的。如果一个View的onTouchEvent返回false,那么它父容器的onTouchEvent将会被调用,以此类推。如果所有元素都不处理这个事件,那么这个事件将会被传递给Activity处理。
2. 一些结论总结
- 同一个事件序列是指手指接触屏幕那一刻起,到手指离开屏幕的那一刻结束。
- 一般来说一个事件序列只能被一个View拦截且消耗。
- 某个View一旦决定拦截,那么只一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理。
- 如果View不消耗除ACTION_DOWN之外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用。当前的View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都是false。
- View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
- onClick会发生的前提条件是当前View是可点击的,并且他收到了down和up的事件。
- 事件传递过程是由内向外的,即事件总是先传递给父元素,然后再由父亲元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
事件分发的源码分析
1. Activity对点击事件的分发过程
点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作是由Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。下面先来看Activity的dispatchTouchEvent:
|
|
首先是if语句中的getWindow().superDispatchTouchEvent(ev)
也就是交给了Activity所附属的Window进行分发,如果返回true,那么整个事件循环就结束了,返回false则表示事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
接下来看Window是如何将事件传递给ViewGroup的。由于Window是一个抽象类,而Window的superDispatchTouchEvent方法也是一个抽象方法,因此需要找到Window的实现类。这个实现类就是PhoneWindow,所以就可以看PhoneWindow是如何处理点击事件的,PhoneWindow的superDispatchTouchEvent方法如下:
|
|
可以看到,该方法直接调用了mDecor的superDispatchTouchEvent,这个mDecor是DecorView,也就是说PhoneWindow直接将事件传递给了DecorView。
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
这种方式可以获取Activity所设置的View,这个mDecorView显然就是getWindow().getDecorView()返回的View,我们通过setContentView设置的View是它的一个子View。目前事件传递到了DecorView这里,它继承自FrameLayout并且是父View,因此最总事件会传递给View。自此,事件已经传递到了顶级View,即在Activity中通过setContentView所设置的View,也叫做根View。
2. 顶级View的事件分发过程
大致梳理一下顶层View的事件分发过程:
- 首先点击事件到达顶层View(一般是ViewGroup)会调用ViewGroup的dispatchTouchEvent。
- 如果顶层ViewGroup拦截事件,即onInterceptTouchEvent返回true,则事件由ViewGroup处理,这种情况下,事件的优先级如下:
- 如果ViewGroup的mOnTouchListener被设置,则onTouch方法会被调用,否则就会调用onTouchEvent,也就是说如果两个都有的话,那么onTouch会屏蔽掉onTouchEvent。
- 在onTouchEvent中如果设置了mOnClickListener,则会调用onClick方法
- 总得说来优先级是onTouch -> onTouchEvent -> onClick
- 如果顶层的ViewGroup不拦截事件,即onInterceptTouchEvent返回false,则事件会传递到下一级的子View,这时子View的dispatchTouchEvent方法会被调用。接下来的过程和顶层的View的过程是一致的。
来看ViewGroup的dispatchTouchEvent方法,这个方法比较长,因此分段来看,首先看当前View是否拦截点击事件的逻辑:
|
|
if的条件中有两种情况会去判断是否需要拦截当前的事件:事件类型为ACTION_DOWN
或者mFirstTouchTarget != null
主要说一下mFirstTouchTarget这个变量。当事件由ViewGroup的子元素成功处理的时候mFirstTouchTarget会被赋值并指向子元素,也就是说当事件由当前ViewGroup拦截时,mFirstTouchTarget就为空,那么当ACTION_DOWN和ACRION_UP到来的时候,ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。
然后有一种特殊情况就是FLAG_DISALLOW_INTERCEPT
这个标记位,这个标记位是通过requestDisallowInterceptTouchEvent
来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他事件(ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位):
|
|
在ACTION_DOWN的时候,会调用resetTouchState()
用来重置FLAG_DISALLOW_INTERCEPT
接下来就是当ViewGroup不拦截事件的时候,也就是点击事件向下分发,这段的源码如下:
|
|
以上是截取的一段代码,是事件向下分发交给它的子View进行处理。首先是一个for循环遍历ViewGroup所有元素,然后判断子元素是否能够接受到点击事件。是否能够接收点击事件主要由两点来判断:(1)子元素是否在播动画(2)点击事件是否落在子元素的区域内。如果某个子元素没有在播放动画并且点击事件落在它的区域内,那么事件就会传递给它来处理。dispatchTransformedTouchEvent
方法实际上调用的就是子元素的dispatchTouchEvent方法,其中的一段如下:
|
|
child在上面的传递不是null,因此事件就交给了自元素处理了,从而完成了一轮事件分发。
如果子元素的dispatchTouchEvent返回了true,这时mFirstTouchTarget就会被赋值,同时跳出for循环,如下所示:
|
|
其中addTouchTarget
就是对mFirstTouchTarget的赋值,如果自元素的dispatchTouchEvent
返回了false,ViewGroup就会把事件分发给下一个子元素。
再来看一下addTouchTarget这个方法:
|
|
从addTouchTarget方法就可以看出mFirstTouchTarget是一个单链表结构。
如果遍历所有的元素之后事件都没有被处理,这包含两种情况:
- ViewGroup没有子元素
- 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这种情况一般是因为自元素在onTouchEvent中返回了false。
这时候ViewGroup会自己处理点击事件,代码如下:
|
|
可以看到第三个参数位null。从之前的分析可以知道,它会调用super.dispatchTouchEvent(event)
显然这里就赚到了View的dispatchTouchEvent方法,接下来点击事件就交给了View处理。
3. View的事件分发过程
首先这里的View已经不包含ViewGroup了,来看它的dispatchTouchEvent方法:
|
|
由于View(不包含ViewGroup)是一个单独的元素,它没有子元素,因此无法向下传递,只能自己处理事件。
从上面的代码可以看出View对点击事件的处理过程,会判断有没有设置OnToucListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用。由此可见OnTouchListener的优先级高于onTouchEvent,这样做的话可以方便在外界处理点击事件。
接下来看onTouchEvent的实现。首先看当View处于不可用状态下的点击事件处理过程:
|
|
很显然,不可用状态下的View照样会消耗点击事件。
接下来看一下onTouchEvent中对点击事件的具体处理,代码如下:
|
|
可以看出只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。然后就是当ACTION_UP发生时,会出发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用onClick方法:
|
|
View的LONG_CLICKABLE实行默认为false,而CLICKABLE属性是否为false和具体的View有关,确切来说是可点击的View的为true,不可点击的为false。通过setClickable和setLongclickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。另外setOnClickListener会自动将View的CLICKABLE设置为true,setOnLongClickListener会将View的LONG_CLICKABLE设为true。