之前分析了一下Android中的消息传递机制,不知道对各位有没有帮助!哈哈,别怪我写的太垃圾了......也不要说的太多的废话了,直接进入今天的主题--Android 事件分发机制。还是那样,文章如有错误,请各位指正,本文参考资料:
1.任玉刚老师的《Android 开发艺术探索》
2.徐宜生老师的《Android 群英传》
注意,本文的所有代码都是 API 26,如果是其他的版本,会做特别说明!
1.概述
我们还是继承一下《Android 消息处理机制》的格式,先来概述一下今天的内容,假装符合面向对象的继承特性。。。
在事件传递机制中,必须讲解的三个方法:
1.public boolean dispatchTouchEvent(MotionEvent ev)方法,这个方法作用主要是用来分发事件。也就是说,当一个事件传递当前View的dispatchTouchEvent方法里面,这个方法可以决定将事件分发到哪里去,这里的分发到哪里去表示有两个意思:1.将事件分发到子View(如果有子View的话);2.将事件分发到分发到自己的onTouchEvent方法里面去消耗。
2.public boolean onInterceptTouchEvent(MotionEvent ev)方法,这个方法的作用是用来决定当前的View或者ViewGroup是否拦截这个事件,如果返回true的话,那么就表示拦截;反之,表示不拦截。前排预警一下,这个方法有很多的坑,不是返回一个true或者false那么简单。
3.public boolean onTouchEvent(MotionEvent event)方法,这个方法是具体消耗事件的方法,如果返回true的话,表示当前的View已经将这个事件消耗了。
可能大家看我写了这些,还是觉得一脸懵逼。这三个方法的意思大家都懂,说这些有什么用。大哥,不要急,我们来慢慢的分析。
当前一个事件发生了,事件传递的流程是从上层依次传递到下层,直到这个事件被处理,例如:
上图中,当在事件发生点发生了事件,它的传递顺序是:ViewGroupA ->ViewGroupB ->View。然后我们在结合上面的三个方法来更加形象的展示一下,事件分发的顺序:
这里从图中可以看出来,事件是从ViewGroupA开始的,先调用A的dispatchTouchEvent方法,进行分发,同时还会调用A的onInterceptTouchEvent方法,如果onInterceptTouchEvent方法返回的是false,表示ViewGroupA不拦截此事件,于是将事件传递给ViewGroupB,ViewGroupB也进行跟ViewGroupA一样的操作。如果ViewGroupB也不进行拦截的话,那么首先就会传递到View的dispatchTouchEvent方法,由于View再没有子View了,所以不能进行向下分发,所以只能传递到View的onTouchEvent方法里面来。如果View消耗了这个事件的话,那么这个事件传递的流程就在这里结束,不会继续将事件传到ViewGroupB的onTouchEvent方法里面去;反之如果View不消耗这个事件的话,那么就继续往上传递。
上面只分析了ViewGroup不对事件进行拦截的情况,下面来分析一下当一个ViewGroup拦截了事件的情况。例如:
一旦,ViewGroupA对事件进行拦截,直接将事件传递给ViewGroupA的onTouchEvent方法里面去。
这个大的流程差不多就是这样的,可能中间有非常多的细节没有提及到,但是不急,待会的源码分析有你们好受的!!!哈哈,开玩笑!
2.ViewGroup的事件分发
(1).DecorView
当我们用手指在屏幕点击时,事件首先被传递到Activity的dispatchTouchEvent方法。对的哦!你没有看错,Activity也有dispatchTouchEvent方法。我们来看看Activity的dispatchTouchEvent方法代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可见,当Activity的dispatchTouchEvent方法接收到了一个事件之后,Activity会将这个传递到Window里面去,我们再去看看:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
哦豁,我们发现superDispatchTouchEvent所在的Window类是一个抽象类,怎么办?不急,在Window类解释中,google爸爸给我们这么说的(代码根据 api 26):
The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.
这里说的是,Window抽象类的唯一实现类在是android.view.PhoneWindow。然后我们到PhoneWindow里面去看看相应的方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
好嘛,又继续跳,然后我们就到了DecorView类的superDispatchTouchEvent方法里面来了。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView又是什么鬼?DecorView其实我们界面的顶级容器,也就是我们视图树的根,是被添加到Window的。而DecorView作为顶级View,一般情况下,它内部会类似于LinearLayout的竖直布局,在这个布局里面有上下两个部分,上面是标题栏,下面是Content View部分,在Activity 的setContView所设置的布局文件就是添加到Content View的部分。如图:
通常来说,我们可以通过如下代码来我们自己设置的ContentView对象
ViewGroup viewGroup = getWindow().getDecorView().findViewById(android.R.id.content);
从这里,我们知道DecorView肯定是一个ViewGroup对象,我们继续点击dispatchTouchEvent方法,发现进入到了ViewGroup的dispatchTouchEvent方法里面来了。
好嘛,费了半天的劲,我们终于看到了重头戏了。好了好了,我们整装待发,准备好好的来看一下这个方法!不过我们先来总结,我们获取了哪些信息:
1.一个事件首先会被传递到Activity的dispatchTouchEvent方法里面,然后最终会传递DecorView中去,最后通过DecorView调用ViewGroup的dispatchTouchEvent方法来进行事件的分发。
2.DecorView是一个Activity的根本局,实际上他也是一个ViewGroup。
(2).ViewGroup对View事件的分发
由于dispatchTouchEvent方法源代码太多了,所以我就不像消息机制那篇文章贴出完整的代码,在这里知识贴出部分代码来进行理解。
首先,我们来看看这段代码:
// 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;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
这段代码的作用是非常明显的,就是check当前的ViewGroup是否需要拦截当前的事件。我们发现在这段代码里面发现了另一个比较眼熟的方法onInterceptTouchEvent方法。从代码中,我们可以看出,ViewGroup判断一个事件是否需要判断实在dispatchTouchEvent方法里面对方法进行调用。
然后我们再看看调用onInterceptTouchEvent方法的条件。首先,action为ACTION_DOWN的话,需要判断当前的是否拦截,这个非常好理解。但是mFirstTouchTarget是什么什么意思?实际上呢,这个从后面的代码逻辑中可以看出来,当ViewGroup的子元素成功处理一个事件的时候,mFirstTouchTarget会被赋值并指向该子元素。换一句话说,当ViewGroup不拦截事件,将事件交由给子元素来处理时,mFirstTouchTarget就不为null了。也就是说,当事件序列的开始--ACTION_DOWN来到时,这时候mFirstTouchTarget是为null(因为这是第一次来,所以事件还没有传递给它的子元素),如果此时ViewGroup在onInterceptTouchEvent返回为true的话,表示拦截这个事件序列,然后后面的ACTION_MOVE和ACTION_UP来到时,由于此时调用onInterceptTouchEvent方法的条件不符合,所以onInterceptTouchEvent不会再被调用。为什么这里调用onInterceptTouchEvent方法的条件不符合呢,因为第一次的down事件被ViewGroup拦截了,从而导致down事件没有被传递到子View,所以mFirstTouchTarget肯定为null,当ACTION_MOVE和ACTION_UP两个事件来到,actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null肯定为false的!
从而,我们从这段里面得到一个结论,一旦一个ViewGroup在onInterceptTouchEvent方法里面对ACTION_DOWN事件进行拦截,属于同一个事件序列的后续事件也会被拦截,同时onInterceptTouchEvent方法只会被调用一次,也就是对ACTION_DOWN进行拦截的那一次!
说到这里,那么有没有办法对其他事件进行需求性的拦截呢?有的,这个问题,我们后续再讲!现在就讲的话,就不能显示我牛逼了!哈哈,开玩笑的,应该时时刻刻记住自己就是一个菜鸡!
在刚刚的那段代码中,我们还发现有这个判断
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
......
}
其中,我们需要关注的是FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过ViewGroup里面的requestDisallowInterceptTouchEvent方法来设置的,一般用于子View。一旦FLAG_DISALLOW_INTERCEPT被设置了,也就是说,我们在子View里面调用父布局的requestDisallowInterceptTouchEvent方法,那么ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。
这里为什么时候是ACTION_DOWN以外的点击事件呢?这是因为,ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位,导致子View设置的这个标记位无效。我们来看看代码:
// 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();
}
从dispatchTouchEvent的代码看来,上面这段代码在我们之前那段代码的前面,所以在ViewGroup在判断事件是否需要拦截之前,就会重置FLAG_DISALLOW_INTERCEPT,从而导致我们的子View调用requestDisallowInterceptTouchEvent方法失效!
经过上面的代码,如果ViewGroup不对事件进行拦截,那么就会将这个事件分发到能够接收到这个事件的子View。
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition 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 into 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);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
根据任玉刚老师在《Android 开发艺术探索》中对这段代码的解释,一个子View是否能够接收到点击事件主要由两点来衡量:子View是否是否在播放动画和点击事件是否落在子View的的区域内。如果这两个事件能够满足的话,那么事件就会交给它来处理。
这里将会详细的讲解一下,事件到底是怎么传递到子View。ViewGroup是通过dispatchTransformedTouchEvent来将事件分发到子View的!
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into 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);
alreadyDispatchedToNewTouchTarget = true;
break;
}
这个是事件分发代码,其中,我们会发现,如果当前子View会消耗这个事件,也就是说dispatchTransformedTouchEvent方法返回true,那么将会将当前的View添加target的链表,而我们说的mFirstTouchTarget就是指向这个链表的头!这个就相当于完成的分发了吗?
NO!NO!没有那么的简单,我们会发现前面有段代码:
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
如果当遍历第一个子View的时候,这里的newTouchTarget就会返回的不是null,岂不是下面的dispatchTransformedTouchEvent根本就来不及调用。像这种情况,应该怎么办?我们发现,只要在这段代码里面break,最后会执行这段代码:
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
如果说,之前已经将事件分发下去了,alreadyDispatchedToNewTouchTarget && target == newTouchTarget这个条件肯定为true。所以,如果在dispatchTransformedTouchEvent方法之前break,从而导致跳出循环,alreadyDispatchedToNewTouchTarget肯定是为false的,因为这个变量在调用了dispatchTransformedTouchEvent方法之后会被置为true。这行代码在之前循环遍历子View里面。
alreadyDispatchedToNewTouchTarget = true;
所以,只要在之前没有调用dispatchTransformedTouchEvent方法就break,肯定会进入else的代码里面。现在的关键是理解resetCancelNextUpFlag是什么意思?我们先来看看这个方法:
/**
* Resets the cancel next up flag.
* Returns true if the flag was previously set.
*/
private static boolean resetCancelNextUpFlag(@NonNull View view) {
if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}
这里,我是看不懂代码的。但是可以从方法的注释来看他的意思,这个方法作用是,如果之前这个View的flag被重置过,那么就返回true,反之返回false。简而言之,相对于同一个View来说的话,如果第一次调用这个方法的话,返回的是false;反之则返回的true。
所以,在这里,我们就可以理解到了,只要是在调用dispatchTransformedTouchEvent方法之前就break的话,resetCancelNextUpFlag返回的肯定是true。这个是为什么呢?因为只要getTouchTarget返回的不是null,表示的意思就是当前的View已经被添加到了mFirstTouchTarget所在的链表中,也就是说在当前这个事件之前,有可能有个事件传递到当前的这个View,并且执行了,所以被添加到链表中的。因为这段代码在dispatchTransformedTouchEvent方法为的true才执行的:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
从而得知,只要newTouchTarget不为null的话,resetCancelNextUpFlag方法返回的肯定是true。而这里cancelChild变量还由intercepted变量来决定,这个待会再细讲,因为变量太特么的坑了!
这样我们就能得知,如果一个View对一个事件序列的事件进行处理,但是后续如果有一个事件不会处理的话,那这个View会收到一个ACTION_CANCEL类型的事件!
以上就是ViewGroup对子View的事件分发大概的解释,不敢说特别详细!下面来总结一下:
1.当一个事件传递到ViewGroup里面的话,首先会根据事件类型或者mFirstTouchTarget 是否null来判断是否调用onInterceptTouchEvent方法,当然这个过程中还要考虑FLAG_DISALLOW_INTERCEPT标记位。简而言之,当前DOWN事件来到时,ViewGroup首先询问onInterceptTouchEvent是否需要拦截。这里需要注意的是,如果有子View处理这个事件了,会导致mFirstTouchTarget不为null,从而可以形成一种父ViewGroup可以拦截非ACTION_DOWN事件的局面!还需要注意的是,整个询问拦截的过程还需要考虑子View调用requestDisallowInterceptTouchEvent方法来请求不要我的事件!哎,感觉子View好可怜,动不动就会ViewGroup折磨!!!!
2.当ViewGroup不对事件进行拦截时,ViewGroup会将相应的事件传递到子View里面!
3.如果整个事件序列的ACTION_DOWN没有子View来处理,最终会传递到ViewGroup方法里面处理。因为当mFirstTouchTarget为null时,会调用ViewGroup自己的onTouchEvent方法!但是这里需要的注意,整个事件序列,除了ACTION_DOWN会传递到子View的onTouchEvent之外,后续的事件都只会到达子View的dispatchTouchEvent方法,不会到达onTouchEvent方法里面。这个原因待会再讲View的方法来解释!
4.当一个事件序列中间(记住这里是中间,开始的情况参考 3 ,结尾可以参考这个)的某个事件没有子View来处理的话,那么在TouchTarget链上的所有View都会收到一个ACTION_CANCEL事件,并且会将这些子View从链上recycle掉。从而得知,只要一个子View不对一个事件进行处理,那么在这个事件序列上的其他类型的事件都不会交给它来处理。
5.如果一个事件序列从ACTION_DOWN开始,就被拦截了。这个事件序列的所有事件不会在传递的到子View。因为ACTION_DOWN来的时候,mFirstTouchTarget本来为空,由于onInterceptTouchEvent方法返回true,所以导致if (!canceled && !intercepted)语句进入不了,进而导致mFirstTouchTarget在整个事件序列都为空,所以一直在调用这个代码,从而导致整个事件序列的都不能传递下去:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
(3).ViewGroup对View事件的拦截
还记得我在前面挖的两个坑吗?第一个是在概述里面说的,前排预警一下,onInterceptTouchEvent方法有很多的坑,不是返回一个true或者false那么简单;第二个是在(2)里面的,有没有办法对其他事件进行需求性的拦截?
到这里来看看,这两个坑好像就像是一个问题,都是关于onInterceptTouchEvent方法。
其实在之前我们简单的介绍onInterceptTouchEvent方法的作用和使用,但是只是粗略的介绍,在这里将稍微详细的解释。
A.onInterceptTouchEvent方法的调用时机
先说明一下,这里先不考虑FLAG_DISALLOW_INTERCEPT标记位的影响。
在dispatchTouchEvent方法,我们知道,当一个事件序列的开始,也就是ACTION_DOWN来到时,会调用onInterceptTouchEvent方法来询问是否需要拦截此事件序列!这种情况下,应该非常容易的理解!
另一种情况便是mFirstTouchTarget 不为null的时候。那mFirstTouchTarget不为null究竟是什么情况呢?我们从dispatchTouchEvent方法里面可以看出来,当一个事件被子View消耗了,那么会将当前的这个View封装成一个Target对象,然后添加到一个链表的链头,而mFirstTouchTarget则是指向这个链表的链头。也就是说,当前mFirstTouchTarget不为null的时候,表示在同一个事件序列,当前事件前面的事件被子View消耗掉了!mFirstTouchTarget不为null表示的就是这个意思!
如上的情况下,我们可以形象的解释,将你的妈妈比喻为ViewGroup,而子View当成你,你开始打游戏表示一个事件序列的开始。如上的情况就是这样的,你开始打游戏的时候,你妈妈没有拦截你的行为,因此你可以顺利的打开游戏,开心的吃鸡,如果中途你妈妈叫你去打酱油,可是此时你正在决赛圈说你没空,你妈妈就生气了,把你的网线拔了,相当于是拦截你的行为,导致你的吃鸡梦想泡汤了!这个比喻能够说明上面的情况,也就是说,当子View在ACTION_MOVE的非常开心的时候,父ViewGroup有资格让子View不开心!哈哈哈哈!!!
上面的解释就是,当不考虑FLAG_DISALLOW_INTERCEPT标记位时,onInterceptTouchEvent方法的调用时机。
那么我们现在来考虑FLAG_DISALLOW_INTERCEPT标记位。
首先说一下,标记位对事件序列的开始事件--ACTION_DOWN无效的!只有当子View在ACTION_MOVE的非常开心的时候,才有资格向父ViewGroup申请不要拦截我的事件!这个请求是有效的!
如上便是onInterceptTouchEvent方法的调用时机。这里对onInterceptTouchEvent方法的调用时机做一个简单的总结:
1.ViewGroup有资格一开始ACTION_DOWN,即使子View调用requestDisallowInterceptTouchEvent方法来申请不拦截也没有用的。一旦拦截了,整个事件序列就都失去了向下传递的能力,直接进入ViewGroup的onTouchEvent方法去处理。
2.View有资格不拦截ACTION_DOWN,而是拦截ACTION_MOVE和ACTION_UP事件。还是跟拦截ACTION_DOWN的情况比较类似,但是还是有点区别!
B.onInterceptTouchEvent方法对非ACTION_DOWN的事件进行拦截
如果ViewGroup只能对ACTION_DOWN进行拦截的话,这样也太暴力了!因为这样会导致整个事件序列都只能被传递ViewGroup。所以,ViewGroup对ACTION_MOVE和ACTION_UP事件还是有必要的。其实这种需求很好的实现,例如:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP: {
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
是不是瞬间来了一句卧槽!这么简单。对!就是这么简单,但是简单的背后大有玄机所在了!例如:
这是ViewGroup的代码:
public class MyViewGroup extends LinearLayout {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("pby123", "1");
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP: {
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("pby123", "2");
return true;
}
}
这是View的代码:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("pby123","3");
return true;
}
}
此时,我对View进行ACTION_DOWN和ACTION_UP的事件产生,然后打印的log却是这样的:
我们发现,当ACTION_DOWN事件产生时,传递到子View很正常,但是我们对ACTION_UP事件进行拦截的,为什么还是会传递子View里面去呢?是不是onInterceptTouchEvent对ACTION_UP事件是无效的呢?瞎猜是没有用的,此时我们来看看dispatchTouchEvent的代码。(其实这种情况,我在分析dispatchTouchEvent的时候已经非常小声的说过了哦!!!!)
// 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;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
上面这段代码,我们当ACTION_UP事件来到,由于mFirstTouchTarget不为null,最终会调用onInterceptTouchEvent来进行询问是否需要拦截,我们在onInterceptTouchEvent方法里面返回的是true,所以在intercepted肯定为true。然后代码往下走,最终进入这段代码:
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
是不是感觉又回来了?又来分析这个方法了,我们知道cancelChild返回的肯定是true,所以dispatchTransformedTouchEvent这一步会给子View发送一个ACTION_CANCEL事件,然后就行将这个target回收了。
到这里我们知道了,第二次ACTION_UP事件根本没有传递到子View里面,传递过去的是一个ACTION_CANCEL事件!大家如果不信的话,可以去试试!
这里我们不满足只是ACTION_DOWN和ACTION_MOVE事件,我们使其也产生ACTION_MOVE事件。我们来看看这种情况下的log日志:
哈哈没错,第二次之所以将事件传递给子View,那么是因为ACTION_MOVE事件被拦截了,从而传递过去一个ACTION_CANCEL事件过去,而不是ACTION_MOVE事件。
好了,onInterceptTouchEvent方法分析的差不多了,现在该解决在留的两个问题。首先,onInterceptTouchEvent方法坑在于onInterceptTouchEvent方法的调用时机,待会再总结里面会总结一下,这里就不再多余的说了;其实onInterceptTouchEvent的坑还有就是ACTION_DOWN和ACTION_UP,谁又能想到传递子View的根本不是ACTION_UP事件呢?。其次,就是对非ACTION_DOWN的拦截,假设我们从ACTION_MOVE开始拦截,需要注意的是第一个ACTION_MOVE事件是不会传递子View,也不会传递到ViewGroup,只有经过这次的处理,后面的事件ViewGroup才算是能够接收到!
又该对上面的知识点做一个总结:
1.一个ViewGroup的调用时机是:1.ACTION_DOWN的来到;2.事件序列中间的ACTION_MOVE事件来到,需要注意是这样情况下,必须保证在同一个事件序列中, 当前事件的前面的事件有被子View消耗过的,也就是,mFirstTouchTarget不能为null。
2.调用时机还需要的是:如果一个事件被拦截了,在这个事件序列里面,onInterceptTouchEvent不会再被调用。
3.如果我们想要对非ACTION_DOWN事件进行拦截,必须保证同一个事件序列的前面所有事件都子View执行了。
4.对非ACTION_DOWN事件进行拦截,是对下次的事件进行拦截,当前的事件会被变为ACTION_CANCEL传递到子View中去。
3.View对事件的处理
由于View是没有子View的,所以View不能继续对事件继续的分发。相较于ViewGroup,View少了一个onInterceptTouchEvent方法。所以说,如果一个事件到达View,肯定会处理,注意的处理表达意思是:它可以调用onTouch或者onTouchEvent方法来处理,或者不处理,最后这个事件被它的ViewGroup分发ViewGroup自己进行处理。
所以,View对事件的处理分成两种情况:一种是自己处理;一种是不处理,父ViewGroup会自己处理,处理的代码也是调用View的,因为ViewGroup继承于View。我们一个一个的分析。
(1).View事件处理流程
事件首先会被传递View的dispatchTouchEvent方法里面,我们来看看,不要怕哦!View的dispatchTouchEvent代码非常的简单。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
以上的代码,我删除了部分我认为不重要的代码,是不是非常的简单?其实意思也非常的简单,首先如果设置了OnTouchListener监听的话,onTouch方法是否消耗该事件,如果消耗的话,事件传递就结束了;反之,则将事件传递到onTouchEvent方法里面去。
从这里,我们可以看出,onTouch的优先级比onTouchEvent的高!
我们再来看看onTouchEvent,由于onTouchEvent方法代码太长了,这里只看部分:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
从以上的代码中,我们看出来,如果一个View的enable属性是Disable的话,它仍然能够消耗事件,只是不会做出任何的反应而已,正如注释所说的。
我们继续往下看,我们发现switch-case语句被这段代码包裹:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
......
}
而clickable是什么呢?我们来看看:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE
也就是说,只要CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一个为true的话,就会对事件进行消耗!
在switch-case里面,我们不看ACTION_DOWN和ACTION_MOVE事件,我们来看看ACTION_UP事件有个非常眼熟的东西:
if (!post(mPerformClick)) {
performClick();
}
我们再来看看performClick方法里面有什么东西呢
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
哎呀,这个不是我们喜闻乐见的OnClickListener吗?开不开心,激不激动?哈哈哈!!!
从这里,我们可以得出,在一个View中,onTouch的优先级是最高的,其次是onTouchEvent,最后才是onClick方法!
(2).ViewGroup对事件的处理
ViewGroup对事件的处理在dispatchTransformedTouchEvent方法里面进行的,由于dispatchTransformedTouchEvent方法的代码比较长,这里只看他是怎么调用onTouchEvent方法:
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
我们上面的代码中发现调用super.dispatchTouchEvent(event)方法,从而完成了自己对事件的处理,事件处理的流程跟View对事件的处理流程比较相似!
4.总结
终于写完了,我们还是来对我们所有的内容做一个总结: