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方法中,简略的逻辑代码如下
从上面的代码可以看出,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事件的处理。
从上面的源码分析,我们可以得出结论,当ViewGroup决定拦截事件后,那么后续的事件就会默认交由它处理并且不会再次调用onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不在拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件。总结一下可以得出连个结论:
1.onInterceptTouchEvent不是每次事件都会被调用,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup中。
2.FLAG_DISALLOW_INTERCEPT这个标记位的作用,可以用来处理事件的滑动冲突。
接着在看当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理,源代码如下:
上面的代码逻辑很清晰,首先遍历ViewGroup的所有子View,然后判断子元素是否能够接受到点击事件。是否能够接受到点击事件主要由两点蓝衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足上述两个条件,那么事件就会传递给它来处理。可以看到,dispatchTransformedTouchEvent事件上调用了子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此他会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。
如果子元素的dispatchTouchEvent返回true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环,如下所示:
这几行代码完成了mFirstTouchTarget的赋值并且终止对子View的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素。
其实mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,从下面的addTouchTarget方法的内部可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一事件序列中的所有的点击事件。
如果遍历所有的子元素事件都没有被合适地处理,这包含两种情况:
1.ViewGroup没有子元素
2.子元素处理了点击事件,但是在dispatchTouchEvent中返回了false(一般是因为子元素在onTouchEvent中返回了false)
在这两种情况下,ViewGroup会自己处理点击事件。
注意,这里的第三个参数child 为null,从前面的分析可以知道,它会调用super.dispatchTouchEvent,很显然,这里就转到了View的dispatchTouchEvent方法,有关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的事件分发,请关注后面的博文。
相关文章推荐
- Android自定义ViewGroup打造各种风格的SlidingMenu
- Android编程重写ViewGroup实现卡片布局的方法
- Android 自定义View实现画背景和前景(ViewGroup篇)
- 自定义ViewGroup (1)支持margin,gravity以及水平,垂直排列
- 自定义ViewGroup (2)支持滑动,并处理多指触摸可能产生的跳动问题
- 自定义ViewGroup (3) 与子View之间 Touch Event的拦截与处理
- 封装横向滚动View
- android之自定义ViewGroup和自动换行的布局的实现
- Android Touch事件的分发机制
- Android 自定义ViewGroup手把手教你实现ArcMenu
- 4000 android UI之View和ViewGroups简介
- android 事件分发机制完全解析 从源码开始(上)
- android 事件分发机制完全解析 从源码开始(下)
- TouchEvent分发过程一:TouchEvent在ViewGroup中的分发过程
- 自定义ViewGroup实现水平布局空间不足自动换行的效果
- cocos2dx事件监听器
- 谈谈对dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent的理解
- android中点击viewgroup时,子控件也会变成被点击的状态的原因及解决方法
- [知识点整理]Android事件传递机制
- Android分发事件