在上一篇博客中,我们讲解了View的事件分发过程,这稿博客将分析ViewGroup的事件分发过程,并且在最后给出相应的流程图。
在”Android触摸事件分发机制(一):Activity篇”的最后,我们说到ViewGroup的触摸事件分发是从dispatchTouchEvent()方法开始的,下面我们就看一下这个方法:
|
|
由于ViewGroup继承自View,而View中也有dispatchTouchEvent()方法,所以ViewGroup中的dispatchTouchEvent()是覆写了View中的dispatchTouchEvent(),也说明了它们对触摸事件的处理有所不同。
为了详细地了解整个触摸事件传递过程,这里从ACTION_DOWN开始进行分析。
首先是onFilterTouchEventForSecurity(),这个在View的事件分发中已经讲过,就不再赘述;还有由于MotionEvent.ACTION_DOWN是触摸事件的开始,所以如果是ACTION_DOWN事件的话就需要重置状态。开始时是ACTION_DOWN状态,所以进入cancelAndClearTouchTargets()中,下面是该方法:
|
|
resetCancelNextUpFlag()和dispatchTransformedTouchEvent()到后面再讲,这里暂时不影响我们理解,就先看clearTouchTargets();该方法如下:
|
|
显然,会将所有的TouchTarget都回收,并且使mFirstTouchTarget为null.
回到dispatchTouchEvent()方法中,虽然此时mFirstTouchTarget为null,但是actionMasked==MotionEvent.ACTION_DOWN成立,所以会进入到第一个分支中。
下面首先看mGroupFlags的值。我们看一下它在哪里被赋值,找了一下,发现是在requestDisallowInterceptTouchEvent()方法中,如下所示:
显然,如果disallowIntercept为true的话,mGroupFlags的相应位就与FLAG_DISALLOW_INTERCEPT一致,否则与FLAG_DISALLOW_INTERCEPT相反。假设在ViewGroup的外部调用了requestDisallowInterceptTouchEvent(true);则这里intercepted始终为false,否则进入到onInterceptTouchEvent()中,该方法如下:
|
|
这个方法异常的简单,即默认情况下返回false,如果用户重写了该方法,比如返回true,则ViewGroup会截取之后的事件,在ViewGroup中的子View就接收不到后面的事件了。
还有一个分支就是如果不是ACTION_DOWN事件,并且mFirstTouchTarget==null,则表示没有相应的事件接收目标,自然也就没必要再往下传递事件了,所以此时intercept=true;
接着往下看,是确认事件是否被取消,如果是的话就不会进行后续的处理。
然后是获取一个boolean变量split来标记是否把事件分发给多个子View,它的赋值其实跟上面的requestDisallowInterceptTouchEvent()类似,是在setMotionEventSplittingEnabled()中,该方法如下所示:
|
|
如果事件既没有被取消,也没有被当前ViewGroup截取,那就会进入下面复杂的处理了。
由于现在是ACTION_DOWN事件,那么actionIndex为0,后面那些idBitsToAssign和removePointersFromTouchTargets()不影响我们理解,暂时先不讲。
现在newTouchTarget为null,如果ViewGroup中有子View的话,则进入该分支中。
首先是获取到x和y坐标,然后是通过前序遍历的方法来构建所有的子View,之所以采用前序遍历,是因为preorderedList中的顺序是按照addView或者XML布局文件中的顺序而来的,后addView添加的子View,会因为Android的UI后刷新机制显示在上层。假如点击的地方有两个子View都包含在其中,那么后添加进去的那个子View会先响应事件,这样其实也是符合人的思维方式的,因为后被添加的子View会在图层的上层,自然我们点击时也是希望上层的View最先响应。
结合上面的分析,就能理解for循环采用倒序遍历的原因了。之后通过canViewReceivePointerEvents()和isTransformedTouchPointInVieww()方法来判断当前child是否能够接受事件,并且点击位置是否在child的范围内,如果不满足则continue.
一直遍历到找到第一个符合上述条件的child为止,找到之后利用进入getTouchTarget()方法中,如下所示:
|
|
显然,getTouchTarget()是遍历以mFirstTouchTarget为首的TouchTarget链条,看child是否存在某个TouchTarget中。
前面分析过,此时为ACTION_DOWN事件,所以mFirstTouchTarget为null,从而getTouchTarget()返回null.所以暂时不会break,而是执行到dispatchTransformedTouchEvent()处,这个方法非常重要。下面是该方法体:
|
|
首先判断是否要取消事件,如果要取消的话则进行判断,如果child为null则执行super.dispatchTouchEvent(),即当前ViewGroup作为View的dispatchTouchEvent()方法,关于View的dispatchTouchEvent()方法,在上一篇博客中详细讲过,此处不再赘述;如果child不为null,则调用child.dispatchTouchEvent(),注意这里其实就进入递归了,因为dispatchTransformedTouchEvent()是在ViewGroup#dispatchTouchEvent()中被调用,而这里又调用child.dispatchTouchEvent()方法,如果child也是ViewGroup,则进入ViewGroup的dispatchTouchEvent(),否则调用的是View的dispatchTouchEvent()方法。
如果不取消事件或者不是ACTION_CANCEL事件,则分两种情况,一种是newPointerIdBits==oldPointerIdBits,此时说明当前的事件和之前的事件相同,因而不需要变形,可直接调用super.dispatchTouchEvent(event)或者child.dispatchTouchEvent(event),否则需要调用MotionEvent.obtain(event)获得transformedEvent,之后对变形后的事件进行类似的处理。
如果事件被child或其子View消费掉,则dispatchTransformedTouchEvent()会返回true,从而进入下面的分支:先获取到mLastTouchDownTime,mLastTouchDownIndex和mLastTouchDownX,mLastTouchDownY,之后通过addTouchTarget()得到一个TouchTarget对象,下面是该方法:
|
|
之后跳出循环,后面的处理其实主要是针对非ACTION_DOWN的情况,可以看到后续还有dispatchTransformedTouchEvent()方法的调用,即为非ACTION_DOWN情况下也进行类似的递归调用,只要有一个child消费了该事件,就会使handled=true;从而返回true.
非ACTION_DOWN情形要简单得多,主要就是160行之后的处理,上面已经分析过,就不再赘述了。
至此,我们可以得到ViewGroup的触摸事件分发机制的流程图:
实例
在工作中遇到的两个触摸事件冲突的问题,我觉得是很好的实例。
1)RecyclerView中第一个item是ViewPager,在下拉这个item时,会将下拉变成点击ViewPager中的点击事件,从而进入电影详情页。
显然这是触摸事件冲突导致的,本来应该是RecyclerView的下拉事件,由于被ViewPager消费,从而变成点击事件,那知道了原因,解决的方法也就很简单了。
首先定义一个手势识别对象:
|
|
即如果发现Y反向的移动量大于X方向的移动量,就返回true.
之后,在AutoScrollViewPager的dispatchTouchEvent中的代码如下:
|
|
上面将无关的代码省略了,可见发现Y方向的移动量大于X方向的移动量时,就调用getParent().requestDisallowInterceptTouchEvent(false);让parent(即RecyclerView)截获事件。
2)图片查看器,单击图片放大,双击退出
类似的,需要一个GestureDetector,其初始化如下:
|
|
而onDoubleTap方法如下:
|
|
即双击时放大。
那单击退出是如何实现的呢?还记得我们在前面分析过,触摸事件是从Activity开始分发的吗!那就简单了,重写Activity中的dispatchTouchEvent()方法,在里面进行手势判断,但是需要注意不要消费事件,否则PhotoViewAttacher接收不到触摸事件!
故代码如下: