您的位置:首页 > 移动开发 > Android开发

Android事件分发机制源码剖析(2)—顶层View对点击事件的分发过程

2016-03-13 15:39 781 查看
点击事件到达顶层View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法,然后逻辑是这样的:如果顶层ViewGroup拦截事件,即onInterceptTouchEvent返回true,则事件交由ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说,如果都提供的话,mTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件莲上的子View,这是子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

接下来从源码的角度剖析一下ViewGroup的事件分发过程,其主要实现是在ViewGroup的dispatchTouchEvent方法中,简略的逻辑代码如下

//check for intercetion.
final boolean intercepted;

if(actionMasked==MotionEvent.ACTION_DOWN
||mFirstTouchTarget!=null){
final boolean disallowIntercept=(mGroupFlags & FLAG_DISALLOW_INTERCEPT)!=0;
if(!disallowIntercept){
intercepted=onInterceptTouchEvent(ev);
ev.setAction(action);
}else{
intercepted=false;
}
}else{
//There are no touch targets and this action is not an initial down
//so this view group continues to intercept touches.
intercepted=true;
}


从上面的代码可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。mFirstTouchTarget是什么呢?我们从后面的代码逻辑可以看出,当事件由ViewGroup得子元素处理成功后,mFirstTouchTarget会被赋值并指向子元素,换种方式来说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget!=null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget!=null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked==MotionEvent.ACTION_DOWN || mFirstTouchTarget !=null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一个序列中的其他事件都会默认交给他处理。

当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总会调用自己的onInterceptTouchEven方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。

//Handle an initial down.
if(actionMasked == MotionEvent.ACTION_DOWN){
//Throw away all previous state when starting a new touch gesture.
//The framework may have dropped the up or cancel event for the previous gesture.
//due to an app switch,ANR,or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}


从上面的源码分析,我们可以得出结论,当ViewGroup决定拦截事件后,那么后续的事件就会默认交由它处理并且不会再次调用onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不在拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件。总结一下可以得出连个结论:

1.onInterceptTouchEvent不是每次事件都会被调用,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup中。

2.FLAG_DISALLOW_INTERCEPT这个标记位的作用,可以用来处理事件的滑动冲突。

接着在看当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理,源代码如下:

final View[] children=mChildren;
for(int i=childrenCount-1;i>=0;i++){
final int childIndex=customOrder?getChildDrawingOrder(childrenCount,i):i;
final View child=(preorderedList==null)?children[childIndex]:preorderedList.get(childIndex);
if(!canViewReceivePointerEvents(child) ||
!isTransformedTouchPointInView(x,y,child,null){
continue;
}
newTouchTarget=getTouchTarget(child);
if(newTouchTarget!=null){
//Child is already receiving touch within its bounds.
//Give it the new pointer in addtion to the ones it is handling.
newTouchTarget.pointerIdBits|=idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)){
//Child wants to receive touch within its bounds.
mLastTouchDownTime=ev.getDownTime();
if(preorderedList!=null){
//childIndex points to presorted list,find original index
for(int j=0;j<childrenCount;j++){
if(children[childIndex]==mChildren[j]){
mLastTouchDownIndex=j;
break;
}
}
}else{
mLastTouchDownIndex=childIndex;
}
mLastTouchDownX=ev.getX();
mLastTouchDownY=ev.getY();
newTouchTarget=addTouchTarget(child,idBitsToAssign);
alreadyDispatchToNewTouchTarget=true;
break;
}
}


上面的代码逻辑很清晰,首先遍历ViewGroup的所有子View,然后判断子元素是否能够接受到点击事件。是否能够接受到点击事件主要由两点蓝衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足上述两个条件,那么事件就会传递给它来处理。可以看到,dispatchTransformedTouchEvent事件上调用了子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此他会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。

if(child==null){
handled=super.dispatchTouchEvent(event);
}else{
handled=child.dispatchTouchEvent(event);
}


如果子元素的dispatchTouchEvent返回true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:

newTouchTarget=addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget=true;
break;


这几行代码完成了mFirstTouchTarget的赋值并且终止对子View的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素。

其实mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,从下面的addTouchTarget方法的内部可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一事件序列中的所有的点击事件。

private TouchTarget addTouchTarget(View child,int pointerIdBits){
TouchTarget target=TouchTarget.obtain(child,pointerIdBits);
target.next=mFirstTouchTarget;
return target;
}


如果遍历所有的子元素事件都没有被合适地处理,这包含两种情况:

1.ViewGroup没有子元素

2.子元素处理了点击事件,但是在dispatchTouchEvent中返回了false(一般是因为子元素在onTouchEvent中返回了false)

在这两种情况下,ViewGroup会自己处理点击事件。

if(mFirstTouchTarget ==null){
//No touch targets so treat this as an ordinary view.
handled=dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}


注意,这里的第三个参数child 为null,从前面的分析可以知道,它会调用super.dispatchTouchEvent,很显然,这里就转到了View的dispatchTouchEvent方法,有关View的事件分发,请关注后面的博文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息