这篇文章只介绍ViewGroup事件分发机制,因为Activity和View的事件分发,相对很简单,网上随便找一篇文章基本就能理解了,但是对于ViewGroup的事件分发机制,我真的看了不下20篇文章,没有一篇讲的到位的,没有一篇告诉你重点关注哪个点,最后只是告诉你一堆结论,或者给你看一堆流程图,最气人的是没有一篇文章告诉你ACTION_CANCEL怎么回事,ACTION_CANCEL时系统其实做了一个很重要的操作,这个操作你理解了,你之前的疑惑也许就都 理解了,后面会提到,其实对于ViewGroup的分发源码,你看Android2.3的源码就可以了,真的很简单,甚至让你看了后一辈子都忘不了,肯本不需要记任何结论或画任何复杂的流程图,高版本的源码太过复杂但是原理和Android2.3基本一样。
重点要关注的点,我会在涉及到的时候强调。
一:Android事件分发整体流程简图
这是个关注点
这个图很重要,可能你也懂这个图,但是我只是想提醒你看源码的时候心里想着这个图,这样逻辑才清晰。
二:ViewGroup事件分发Anroid2.3源码
我先把源码帖出来,这个源码跟高版本相比已经很简单了,但是我们看的时候还是可以简化一些,我下面会贴出重点代码
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);
}
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)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
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);
}
上面的代码我们再简化一下,并做 一些说明:
pirvate View mMotionTarget;
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
//是否允许拦截
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {//只有ACTION_DOWN时才走这个if语句
//ACTION_DOWN时重置mMotionTarget为null,mMotionTarget指向处理并且消费掉当前ACTION_DOWN的子View。
//如果不为null说明指向上次处理并消费了ACTION_DOWN的子View,因此要重置为null
if (mMotionTarget != null) {
mMotionTarget = null;
}
//disallowIntercept默认为false,可通过requestDisallowInterceptTouchEvent(true)修改其值
//高版本anroid中我有看到,在ACTION_DOWN时有一个对FLAG_DISALLOW_INTERCEPT重置的操作,避免requestDisallowInterceptTouchEvent(true)造成的影响
//所以我在这里加这句代码和高版本一致
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
//不拦截有两种情况:1,不允许拦截;2,虽然允许拦截,但是并没有拦截。
//既然不拦截就找对应的子View处理这个ACTION_DOWN.就是for循环判断 点击区域落到哪个子View身上。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
for (所有子View) {
final View child = 找到的那个可以接收ACTION_DOWN的子View;
if (child.dispatchTouchEvent(ev)) {
//交给这个子View处理,如果子View消费掉了,就让mMotionTarget指向这个子View,且直接return,本次ACTION_DOWN事件分发结束。
//如果这个子View虽然处理了但是没有消费掉该ACTION_DOWN,则继续向下执行。
mMotionTarget = child;//唯一给mMotionTarget赋值的地方,谨记!!!
return true;
}
}
//没有找到合适的子View处理该ACTION_DOWN,例如点击了空白区域,即没有子View的区域等等。继续向下执行
}
}
//targt和mMotionTarget指向同一个对象,需要注意的是,只有在ACTION_DOWN时ViewGroup不拦截且子View消费掉ACTION_DOWN时,才给mMotionTarget,只有这一个地方,谨记!!!
final View target = mMotionTarget;
//target为null,说明没有可接收事件的子View,需要ViewGroup自己处理该事件
//target为null的原因有很多:
//1,ACTION_DOWN时,可能是之前的逻辑中onInterceptTouchEvent()返回true即拦截了,或者虽然没拦截但是没有找到合适的子View处理该事件,
//或者虽然找到子View处理该事件了但是子View没有消费掉该事件,这些都导致mMotionTarget没有被赋值(参考前面的for循环部分)。
//2,非ACTION_DOWN时,则说明之前的事件序列都是ViewGroup自己处理的(参考1)或者之前ViewGroup拦截了某次事件(后面的代码可看到拦截时会将mMotionTarget置为null)
if (target == null) {
//ViewGroup自己处理事件的逻辑
return super.dispatchTouchEvent(ev);//若执行了这里则后续代码不再执行
}
//能走到这里的不可能是ACTION_DOWN,且target不是null,说明子View有处理能力同时ViewGroup也可以拦截,体会一下。
//如果拦截则给子View(target)一个ACTION_CANCEL事件,同时mMotionTarget置为null,本次分发结束,下次分发的事件就交给ViewGroup自己处理,
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_CANCEL);
if (!target.dispatchTouchEvent(ev)) {
}
//下面两行可看到,所谓的拦截其实并不是拦截,而是让子View执行ACTION_CANCEL事件,但是ViewGroup并不处理拦截下来的这个事件,而是直接消费掉(true),只是说下次事件
//由我ViewGroup来处理。
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
//如果不拦截,则由子View处理
return target.dispatchTouchEvent(ev);
}
这段代码你肯定看的懂,没必要告诉你一堆结论,给你一堆流程图,记结论很累,你的理解就是结论。
这里并没有详细介绍onInterceptTouchEvent(ev)和requestDisallowInterceptTouchEvent(true)两个方法,这两个方法很好理解,也有很多文章介绍,,简单说下就是:
requestDisallowInterceptTouchEvent(true)用于修改FLAG_DISALLOW_INTERCEPT标记为,为true说明禁止Viewgroup拦截,这样也就不会调用onInterceptTouchEvent(ev)判断是否拦截了,为false说明允许拦截,注意,允许拦截不代表肯定拦截,还要调用onInterceptTouchEvent(ev)判断最终拦截不拦截。
关注点*:再次强调看源码时一定要结合事件分发大体过程看,结合最上面的图看,脑子里要有这个分发的过程,提醒 自己时间是随着手指边移动边不断分发的。且是不断调用dispatchTouchEvent(MotionEvent ev) 的
关注点1:mMotionTarget默认为null,唯一给它赋值的地方在有子View消费掉ACTION_DOWN时,注意是唯一。
关注点2:target就是mMotionTarget
关注点3:ViewGroup拦截子View的某个非ACTION_DOWN事件时,会给这个子View一个ACTION_CANCEL事件同时把mMotionTarget置为null,这样下次分发的事件就由ViewGroup处理了。因为mMotionTarget为null了,这个事件序列之后的事件就全部交给ViewGroup处理了。
如果对您有用,请打赏仨瓜俩枣的!