上篇我们讲到Activity是怎样把点击事件传递到ViewGroup中的,这篇我们将继续讲解点击事件在ViewGroup中是怎样传递的。
由于Android 5.0之后ViewGroup的源码发生了变化(更加复杂),但原理是相同的。因此我们本着可阅读性和方便理解性,采用5.0之前的源码进行讲解。
ViewGroup的事件分发源码:
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) { mMotionTarget = null; } if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); 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)) { 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)) { mMotionTarget = child; return true; } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; if (target == null) { 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); } } public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
我们分两步去讲分发流程,然后再去处理疑惑点。
一、当ViewGroup不拦截点击事件时
1.我们首先看第二个if的disallowIntercept判断条件,这个条件是干嘛的呢?disallowIntercept = 是否禁用事件拦截的功能,默认条件是false。当我们的ViewGroup不拦截时,!onInterceptTouchEvent(ev)就会取反
走到if方法中
2.在这个if条件中,做了三件事,第一件是遍历ViweGroup的子View;第二件是找到被点击的View;第三是调用被点击View的dispatchTouchEvent方法,返回true。
3.至此ViewGroup的分发流程结束,点击事件被传递到子View中
二、当ViewGroup拦截事件
1.我们知道,当ViewGroup拦截事件后,ViewGroup会自己处理事件。从源码上看,肯定走了if(target == null)这个条件,然后调用super.dispatchTouchEvent方法。
流程图大概是这个样子
看完这个流程图,你是不是有几个疑问?
1.如果ViewGroup不拦截,ViewGroup遍历子View时,是如何找到被点击的子View的?
2.当ViewGroup拦截时,为什么if(target == null)时去调用super.dispatchTouchEvent,target是什么?
3.为什么要调用父类View的dispatchTouchEvent?
下面我们一个问题一个问题来解决。
1.如何找到被点击的子View?
①我们首先看ViewGroup遍历子View时的第一个if条件判断(child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)
这个条件判断的意思是,判断当前子View是否可点击或者正在播放动画。如果可以点击或者没有播放动画,那么这时候这个子View就算初步如选了。
②拿到初步如选的子View的点击范围,即点击域
③通过获取按下点的坐标系(X,Y)进行判断 = "frame.contains(scrolledXInt, scrolledYInt)"。如果按下点坐标在这个子View的点击范围内,说明就是当前这个子View的点击事件
2.target是什么?为什么target == null 时去调用super.dispatchTouchEvent?
从上面我们可以看出,target其实就是个View,是什么View呢?其实就是ViewGroup的子View。
我们要搞懂这个问题,首先需要看一个变量mMotionTarget,只有了解它,才能明白整个事件流程。
mMotionTarget有什么用?
其实它的作用是记录一个View及其对应分发的触控点列表,且可以通过next与其他实例形成链表。
它把消耗事件的View以链表方式保存,且记录各个View对应的触控点列表,以实现后续的事件派分处理。
是不是还是有点懵?没事,再简单点说它相当于一个可以被触摸的对象,它中记录了接受触摸事件的View。
当我们首次down时,mMotionTarget被置成null.
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
}
如果ViewGroup不拦截,在View Group遍历的时候接受点击事件的子View就被它记录了
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
如果拦截,这时候mMotionTarget就没有记录任何子View,这时候程序会把它赋值给Target,
final View target = mMotionTarget;
所以这时候target == null,自然就走了super.dispatchTouchEvent方法。
3.问什么要调用父类的dispatchTouchEvent方法?
这是因为当不存在消耗ACTION_DOWN事件的目标控件时,后续事件的拦截标记intercepted将会越过用户处理表现为true,可以理解为ViewGroup退化成View,事件处理将交给super.dispatchTouchEvent进行
好了,以上就是整个ViewGroup的分发流程,由于篇幅过长内容过多,看起来会迷糊,不好消化。所以View的分发流程,我将会在下篇讲述。