您的位置:首页 > 产品设计 > UI/UE

ViewGroup的触摸事件的传递源码详细解析

2017-05-08 21:32 218 查看
在Android的UI的事件派发,我们只需要掌握两种:Vew和ViewGroup;其中ViewGroup是继承自view,只不过ViewGroup可以包含子View,对于View的事件传递的派发上一篇文章已经详细介绍分析了一遍,View的事件传递源码详细解析 本篇文章我们就仔细分析ViewGroup的事件派发机制;

ViewGroup的触摸事件传递的伪代码分析总结:
disallowIntercept:是有子类调用getParent.requestDisallowInterceptTouchEvent(boolean)
来决定,意思是孩子View让不让自己拦截;默认这个值是false允许拦截
这个设置在按下的时候对于3.0之前和之后有差异,3.0之前设置之后有效,3.0之后无效,后面源代码有解释;
onInterceptTouchEvent是有自己内部拦截方法;意思是拦截不

private View child;//孩子View
private ViewGroup parent;//父亲ViewGroup

private void dispatchTouchEvent(MotionEvent ev){
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if(disallowIntercept || !onInterceptTouchEvent(ev){
//寻找处理这次事件的child
if(找到的话){
child!=null;
child.dispatchTouchEvent(ev);
}else{
child=null
//没有找到的话自己处理
onTouchEvent(ev);
}
}else{
child = null;
//自己处理
onTouchEvent(ev);
}
break;
case MotionEvent.ACTION_MOVE:
if(child==null){
//自己处理
onTouchEvent(ev);
}else{
if(!disallowIntercept&&onInterceptTouchEvent){
child=null;
//child会收到取消的通知,拦截的这下parent不会处理这次事件
}else{
child.dispatchTouchEvent(ev);
}
}
break;
case MotionEvent.ACTION_UP:
if(child==null){
//自己处理
onTouchEvent(ev);
}else{
if(!disallowIntercept&&onInterceptTouchEvent){
child=null;
//child会收到取消的通知,拦截的这下parent不会处理这次事件
}else{
child.dispatchTouchEvent(ev);
}
}
break;
default:
break;
}
}先说一下,本来还是想用2.2的源码从头到尾仔细分析一遍,在看源码的过程之中,发现对于ViewGroup的事件派发来说,Android3.0之前和之后是存在差异的;
具体差异就是孩子View不允许父亲View拦截所有事件的话,3.0之前是所有的事件都不拦截(不再调用onInterceptTouchEvent());而3.0之后是ActionDown(手指按下的时候)还是要询问一下父亲View要不要拦截这个触摸事件(要调用onInterceptTouchEvent),其余的事件不会调用;其他的区别到不是很大,2.2的源代码简单明了,方便阅读;虽说有点古董,但是看起来还是很好的;

源码2.0的和5.0的,我一般就看这两个版本的Android源代码,一个是Android3.0之前,一个是Android3.0之后的;

先走一遍2.2的吧;其实ViewGroup的事件传递还是从dispatchTouchEvent开始的,就这么一个方法,相信只要你我真心读码,看下来统统没问题;明白也是没有问题的;

老套路下面开始2.2的源代码,为了视觉上一眼望尽而不晕,我们按照代码顺序一小段一小段截取和分析:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}

第10行 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 其中FLAG_DISALLOW_INTERCEPT是一个常量的标志,意思是对于父ViewGroup向内部的子View传递事件不允许拦截的标记;默认的mGroupFlags对应的位置的值是0,在View的初始化代码里面我们也会发现mGroupFlags并没有初始化相应位置的值;

什么意思?其实在View的事件传递中有过解释,这在稍稍解释一下 FLAG_DISALLOW_INTERCEPT的值是 十六进制 0x80000  转化成二进制是 下面一的格式:

0000 0000 0000 1000 0000 0000 0000 0000,总共32位,而mGroupFlags & FLAG_DISALLOW_INTERCEPT位运算之后,0的位置全部变成0,对于那个1的位置,mGroupFlags,对应是位置是1,才是1,否则是0,查看源码,在View的初始化的时候,mGroupFlags并没有在那个位置初始化,其实View中存在很多这样的标志,专门用于对某一个二进制位置上0和1的变换;
例如:

 mGroupFlags |= FLAG_DISALLOW_INTERCEPT;这个时候mGroupFlags在FLAG_DISALLOW_INTERCEPT中1的位置就变成了1;

mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;这个时候mGroupFlags在FLAG_DISALLOW_INTERCEPT中1的位置就变成了0;

这有什么用?当然有用,其实相当于ture和flase 对应于二进制相应位置上的1和0的切换,二进制的其他位置不用去管啦,这是为了便于状态的切换;比如源码里面有这么个方法,你看了,就会明白了:

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// 设置mGroupFlags标记不允许拦截孩子View事件
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// 设置mGroupFlags标记允许拦截孩子View事件
}

// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);// 让自己的父亲View也对事件做同样的事情
}
}

总结:

mGroupFlags变量经过mGroup|=FLAG_DISALLOW_INTERCEPT运算之后,相应位置变成了1,代表不允许拦截

mGroupFlags变量经过mGroup&=~FLAG_DISALLOW_INTERCEPT运算之后,相应位置会变成0,代表允许拦截

上面的代码相信你已经懂了;明白这一点对于我们理解事件传递是有好处的;我们在接着前面一段代码的第10行代码接着开始分析 

 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 现在知道这个disallowIntercept 的结果了吧,没有外力影响(确切的说是孩子View)这个值就是false; 因为这个mGroupFlags & FLAG_DISALLOW_INTERCEPT之后的结果一定是0;孩子View怎么干扰到这里从而改变父亲的行为;就是通过上面那个方法

getparent.requestDisallowInterceptTouchEvent(boolean),

参数传递是true的话,这里的 disallowIntercept 结果就是true,意味着不允许拦截

参数传递是false的话,这里的disallowIntercept结果就是false,意味着允许拦截

第12~19行代码,在用户手指按下的时候为了让mMotionTarget为null,为什么?因为手指从按下到移动最后抬起是一整套事件序列,这个地方在MotionEvent.ACTION_DOWN(用户重新按下的时候),将mMotionTarget置为null,mMotionTarget就是我们要派发新一轮触摸事件让一个新的View来处理;找到处理这次事件的View,就赋值,没找到就置null;

上一段代码分析完毕,按顺序粘贴接着上一段代码的部分,接着往下看

if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;//上面已经分析了,这里是为了看到和下面的if条件一样都是在ACTION_DOWN条件下的判断
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev))  {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
第7行代码 if (disallowIntercept || !onInterceptTouchEvent(ev)) { 这个条件很关键!,什么意思,其实源代码里面也有注释,翻译的意思就是:

假如我们不允许拦截这个触摸事件或者允许我们拦截但是我们并没有拦截;程序就绪进入到这个if条件

disallowIntercept 对应的是允不允许我们拦截,之前分析了一般情况值是false;

onInterceptTouchEvent()方法对应的是要不要拦截,这个方法在ViewGroup中默认的实现是false:即不拦截,源码如下,除非你重写

public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
默认情况这个ViewGroup中 disallowIntercept 的值是false(上面分析过),就是允许我们拦截,这个时候还会调用一下onInterceptTouchEvent方法,要不要拦截,默认也是false;

那么这个条件if (disallowIntercept || !onInterceptTouchEvent(ev)) 的结果就是true;执行内部的代码

第16~39行代码是遍历这个ViewGroup内部的孩子View,找到要处理这次事件的孩子,并将这个孩子View赋值mMotionTarget;

另外从遍历的方法可以知道,它是从他内部最上面的孩子开始寻找,

18·19行代码是孩子View可不可见或者孩子View有没有设置了动画效果;

20行代码是把这个孩子View的left、top、right、bottom放到一个矩形Rect里边,贴下方法源代码

public void getHitRect(Rect outRect) {
outRect.set(mLeft, mTop, mRight, mBottom);
}
那要干什么?我们接着往下看
21行代码 很关键 意思是,我们手指按下的位置区域在不在上面的View的left、top、right、bottom的坐标区域内部或是按下的位置在不在View区域上:贴下方法源代码

public boolean contains(int x, int y) {
return left < right && top < bottom  // check for empty first
&& x >= left && x < right && y >= top && y < bottom;
}
27~31行代码 
if (child.dispatchTouchEvent(ev))  {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
看到这里的if (child.dispatchTouchEvent(ev))这样就把MotionEvent.ACTION_DOWN事件传递给孩子View去处理,但这是并不一定给到这个孩子View,如果dispatchTouchEvent方法返回false,意味着孩子View不消费这次事件,那么for循环会继续找下一个孩子View,直到找到为止,如果最终没有孩子View来处理这次事件呢?那么ViewGroup自己来处理了;为什么,接着看下面的代码吧;

boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);

if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
对于这次的ACTION_DOWN事件来说,1~12行代码没有什么关系,
13行代码if条件是成立的 因为上面mMotionTarget没有赋值,ViewGroup没有找到合适的孩子View来处理这次的事件;

21行代码,这时候会返现super.dispatchTouchEvent(ev)调用相当于自己继承的父View控件的事件分发,相当于自己消费处理了;并且代码不再往下执行了;

这里我们思考一下:

如果在ACTION_DOWN事件的时候这个ViewGroup返回了false,接下来的一波事件还会让着ViewGroup来处理吗?肯定不会啦,因为它的父亲ViewGroup要是找到处理这次事件的ViewGroup的话,事件就会给另一个ViewGroup处理了,如果没有找到,那么他自己会处理了,就不会再派发事件给到这个ViewGroup来处理了;

也就说在Down的时候没有当前的这个孩子子View来处理这次按下事件,那么只能自己处理,如果自己也不处理(super.dispatchTouchEvent(ev)返回false)那么它的父容器ViewGroup就会在找另一个ViewGroup来处理了;为什么?就是我们上面分析的过程,还有ViewGroup的子类如果没有覆盖dispatchTouchEvent方法,当事件传递的时候都会走这个方法;

DOWN按下事件的分发流程图:



这里我们在分析找到了处理这个事件的View(即mMotionTarget被赋值了)

接着dispatchTouchEvent的下一个代码片段:

// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
当下一波事件MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP事件到来的时候走调用这里,看代码

第3行代码 !disallowIntercept为true,而onInterceptTouchEvent(ev)方法,默认都是false,除非重写;这行的意思允许我们拦截,在看要不要拦截;

这个if是不会执行的,可以让它执行,必须重写onInterceptTouchEvent(ev)方法方法,让它返回true;

我们分析拦截之后的逻辑执行:

第7行代码 ev.setAction(MotionEvent.ACTION_CANCEL);把这个事件设置成取消Cancel,

第9行代码 target.dispatchTouchEvent(ev)告知孩子View,事件被取消了,你父亲要拦截处理了;

第14行 把 mMotionTarget = null;

第18行 return true; 代表父类消耗处理这次事件

        此处不执行的时候会往下面执行他这次事件交给目标View来处理:

if (isUpOrCancel) {
mMotionTarget = null;
}

// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);

if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}

return target.dispatchTouchEvent(ev);
}


第2行代码  mMotionTarget = null; target并不指向null,

第11行代码只要target指向的是目标View,赋值的时候做过一次处理child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;所以此处也不会执行;

第17行代码,目标View执行相应的dispatchTouchEvent方法;

MOVE和UP事件的流程图如下:



总结一下ViewGroup的事件传递:

1.(MotionEvent.ACTION_DOWN)按下的时候

1.1;ViewGroup没有孩子View的话,自己不消费按下事件的话,后续事件不会让它处理了;

1.2.ViewGroup有孩子View的话,

孩子不允许拦截的话,ViewGroup自己拦截方法不会调用;孩子会调用自己的dispatchTouchEvent方法来处理这次事件;孩子要是不处理的话,ViewGroup会自己来处理;

孩子允许拦截的话,自己的拦截方法可拦截或者不拦截;

      拦截的话,ViewGroup会自己来处理这次事件;

      不拦截的话,孩子会调用自己的方法来处理这次事件;孩子要是不处理的话,ViewGroup会自己来处理;

2.(MotionEvent.ACTION_MOVE)移动的时候;

自己不消耗但还是会收到后续的事件的,因为在按下的时候,你被选中了专门用来处理事件;

同理即使你拦截了事件,但是你不消耗的话,后续事件还是会让你来处理;你拦截的话,子View会收到一个ACTION_CANCEL

3.(MotionEvent.ACTION_UP)抬起的时候,拦截的话,子View会收到一个ACTION_CANCEL事件;

孩子View通过getParent.requestDisallowInterceptTouchEvent(boolean f)方法可以影响父容器ViewGroup对事件的拦截;

f = true的话,ViewGroup任何时候都不会调用自己的onInterceptTouchEvent的方法拦截事件;

f = false的话,任何时候都会询问 onInterceptTouchEvent()方法;

最后这一点和5.0的时候是有差异的;在5.0的源代码中对于按下的时候,子View要求父亲ViewGroup不允许拦截,那么父容器还是会询问自己的onInterceptTouchEvent方法拦不拦截,拦截的话,事件不会传递到子View哪里;

为什么,我们在分析一下5.0的源代码吧~~

// 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();
}

// Check for interception.
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); // restore action in case it was changed
} else {
intercepted = false;
}
}
在02~08行代码中特别对ACTION_DOWN按下的时候有过特殊的处理;

第7行代码:重新设置触摸的状态,进入这个方法的源码看一下:

private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
看到第4行代码是不是很眼熟, mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;意思是允许ViewGroup拦截触摸事件;

再看上面第14行disallowIntercept的值是不是false,意思是允许拦截,往下执行的时候,是不是调用了onInterceptTouchEvent();

这就是3.0之前和之后的事件传递唯一的一点小区别;3.0之前只要子类不允许父ViewGroup直接不再调用onInterceptTouchEvent();

可能是认为那样不太合理,所以3.0之后在MotionEvent.ACTION_DOWN的时候,还是问一下,你真的不拦截了?

好了,以上就是我对ViewGroup事件传递的理解,有不对的地方,希望指正;相互学习,共同进步!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息