前言
事件分发是一个老生常谈的话题,既然是一个“冷饭”,那为什么今天又开始“炒冷饭”了呢?说白了,还是自己高估了对事件分发的理解。
这里抛出几个问题:
- 1、对一个View进行setOnTouchListener操作,并且onTouch()返回true,为啥它的
onTouchEvent()
不会被响应? -> 答案在:方法展开2部分。 - 2、一个View的
onTouchEvent()
返回了true,为啥它下层的View就再也不会响应任何事件回调了? -> 答案在:方法展开1部分 - 3、如果一个ViewGroup只重写了
onTouchEvent()
并返回了true,那么它的onInterceptTouchEvent()
还会被回调吗? -> 答案在:1.2、部分总结部分。 - 4、重写
dispatchTouchEvent()
并直接返回true,会怎么样?-> 答案在:方法展开2部分。
如果各位小伙伴可以非常清晰的回答这些问题,那么这篇文章就不用看了,左上角点X,唱、跳、Rap、打会篮球什么的...当然如果你愿意留下来点点广告,那也是极好的~哈哈
正文
既然叫做事件分发,那么本质其实就是分发。我猜大家刚开始了解这一块内容时,肯定绕不开三个方法:dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
。不过我真觉得,扯上后边俩个方法,反而把问题复杂化。
对于事件分发来说,核心就是dispatchTouchEvent()
的实现,onInterceptTouchEvent()
、和onTouchEvent()
只是让我们参与到分发流程当中来的接口而已。
因此,这篇文章的核心就在于梳理、阅读ViewGroup和View的dispatchTouchEvent()
方法实现。相信我,阅读完这篇文章绝对有收获~~
一、ViewGroup中的dispatchTouchEvent()
源码基于api-28
关于dispatchTouchEvent()的逻辑,这里主要分为俩个大部分,前半部分侧重于事件消费对象的确定(1.1部分);后半部分侧重于对事件消费对象的后续分发(1.2部分)。
1.1、mFirstTouchTarget的首次赋值
这部分代码逻辑主要为了:
- 1、找到并记录命中消费事件的View
- 2、对各层View的DOWN事件分发
public boolean dispatchTouchEvent(MotionEvent ev){
// 记住这个mFirstTouchTarget,很关键
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
// 如果子View没有调用requestDisallowInterceptTouchEvent(true),则调用自身的onInterceptTouchEvent()
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
// 如果不是DOWN事件,并且mFirstTouchTarget == null,那么就直接认定当前View拦截
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false; // 注意一下这个局部变量,会用到
if (!canceled && !intercepted) {
// 省略部分代码
if (newTouchTarget == null && childrenCount != 0) {
// 遍历View(这里的顺序可以通过重写setChildrenDrawingOrderEnabled() + getChildDrawingOrder()自定义顺序)
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 如果当前的View出在动画;或者x、y不在View区域内直接continue
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 该方法会遍历TouchTarget,但是初始的target需要通过mFirstTouchTarget进行赋值,此时为null。具体实现细节可查看:方法展开4
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 多指操作,暂时忽略
break;
}
// DOWN事件一定会走到此,因为newTouchTarget == null,此方法逻辑见:方法展开1
// 此方法便开始向其他层级的View进行分发事件,此方法的返回值决定了是否走if的逻辑。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 当子View选择消费这个事件时,那么将会走接下来的代码。这里主要的内容就是给newTouchTarget和mFirstTouchTarget进行赋值。(此方法逻辑见:方法展开3)
// 也就是说,如果代码走到这,那么mFirstTouchTarget将不再为null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
}
}
}
}
// 截止到此是intercepted位false的逻辑
}
1.2、部分总结
此时总结并解释一下开头写的:1、找到并记录命中消费事件的View;2、对各层View的DOWN事件分发。
1、找到并记录命中消费事件的View:
当DOWN来到ViewGroup的时候,如果自身不拦截,那么就会尝试分发。最终将根据命中View是否消费(重写onTouchEvent()
/onTouch()
/重写dispatchTouchEvent()
)来决定是否对mFirstTouchTarget进行赋值(记录命中消费事件的View)。
2、对各层View的DOWN事件分发:
这部分代码里,我们第一个遇到了dispatchTransformedTouchEvent()
方法,这个方法会调用child或者super的dispatchTouchEvent()
,最终通过View的onTouchEvent()
/onTouch()
等方法的返回值来决定dispatchTransformedTouchEvent()
的返回值。因此拿到返回值的时候,其实这个事件已经在所有的View中分发了一遍。
此时如果mFirstTouchTarget不为null,那么后续的MOVE和UP事件将重走这一套流程(if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null))。
或者intercepted直接为true;直接交给自己处理。
这里解答开篇的第三个问题,通过代码我们可以看到只要mFirstTouchTarget不为null,并且子View不调用requestDisallowInterceptTouchEvent(true)
,那么当前ViewGroup的onInterceptTouchEvent()
一定会调用,它和onTouchEvent()
的返回值没有任何关系。
解答完这个问题,不知道有没有小伙伴想到一个点:那就是如果ViewGroup的onInterceptTouchEvent()
在满足条件下,一定会调用。那么我是不是可以在某一层View消费了一定的事件后,然后再通过一些条件判断让ViewGroup中的onInterceptTouchEvent()
返回true。这样就可以做到事件没消费完继续分发给其他View,那这种想法能不能实现呢?答案是不能,为什么请阅读:事件分发额外阅读。
1.3、MOVE/UP事件分发的关键
此部分逻辑DOWN也会触发,但更多的是为了分发MOVE/UP
public boolean dispatchTouchEvent(MotionEvent ev){
// 此逻辑分析承接上半部分
// 如果mFirstTouchTarget == null有俩种可能,一个是的确没有找到能够命中的View,另一个是自己直接拦截
if (mFirstTouchTarget == null) {
// 此时child这个字段传null,也就是说直接调自己的super.dispatchTouchEvent()分发给了自己。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 能走到此方法说明mFirstTouchTarget已经不会null,也就是找个了可以去分发的View
// 省略部分条件
TouchTarget target = mFirstTouchTarget;
while (target != null) {
// 这里用到了alreadyDispatchedToNewTouchTarget,很简单对于DOWN事件来说,其实分发已经走了一遍,并且为mFirstTouchTarget赋了值,如果此处不过滤掉那么分发流程就会走俩遍。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 否则向其他View分发事件,其实我猜大家应该都明白了,MOVE/UP事件会通过此逻辑完成分发
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 取消的逻辑暂时不做考虑
}
}
}
1.4、部分总结
此部分代码较少,而且逻辑清晰。主要就在于俩个分支,一个是没有找到能够消费的View,那么分发给自己,直接super.dispatchTouchEvent()
。自己的onTouchEvent()处理。否则通过mFirstTouchTarget,分发后续产生的事件。
二、方法展开
此部分内容,请结合一、ViewGroup中的dispatchTouchEvent()“食用”
2.1、方法展开1:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 省略部分代码
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 如果事件命中了某个View,此时将调用这个View的dispatchTouchEvent()。当然如果此时的View是一个ViewGroup那么会不断进行上述的过程,此时的返回值就是super.dispatchTouchEvent(event),也就是View的dispatchTouchEvent(此方法逻辑见:方法展开2)。
// 不过这里肯定有同学会问如果我当前的View重写了```dispatchTouchEvent()```,并return true会怎么样?-> 看一下 方法展开2 就会明白
handled = child.dispatchTouchEvent(event);
}
return handled;
}
对于此方法来说,一旦handled返回了true,那么对于ViewGroup的dispatchTouchEvent()
来说就可以确定mFirstTouchTarget。有了mFirstTouchTarget,意味着消费的View已经被确定,无需要在将事件往下分发。(这也就解答了开篇抛出来的第2个问题)白话文:背锅的已经找到,此事无序再追查。哈哈~
2.2、方法展开2:View中的dispatchTouchEvent()
// 可以看到,对于View来说dispatchTouchEvent()的返回值,依赖onTouchEvent()的返回值、onTouch()返回值。
// 并且这也说明了一个严重的问题:那就是onTouchEvent等事件的调用是在View的dispatchTouchEvent之中,如果我们重写了某个View的dispatchTouchEvent直接return会了true,那么就意味着onTouchEvent等方法将再也没有机会执行了。(这也就解答了开篇抛出来的第4个问题)
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略
if (onFilterTouchEventForSecurity(event)) {
// 省略
ListenerInfo li = mListenerInfo;
// 此处可以看到,如果listener不为null,并且onTouch()返回true,那么result这个局部变量就会为true。那么就对于下边的判断条件来说第一个条件就不满足,因此就不会再调用onTouchEvent()了。(这也就解答了开篇抛出来的第1个问题)
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;
}
总结方法展开1 + 方法展开2:
如果我们某个View重写了dispatchTouchEvent()
并且直接返回true,那么对于dispatchTransformedTouchEvent()
这个方法来说,将直接得到true;否则将依赖View中
onTouchEvent()
的返回值、onTouch()返回值。
2.3、方法展开3:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
2.3、方法展开4:
private TouchTarget getTouchTarget(@NonNull View child) {
// 因为mFirstTouchTarget的默认值是null,因此首次调用此方法一定return null。也就是DOWN来的时候,此方法return null。
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
三、事件分发额外阅读
上文产生的这个问题,首先明确答案是不行。因为我们已经看罢了通篇的源码。当事件已经开始被某个View消费,那么就意味着mFirstTouchTarget不为null,那么```getTouchTarget(child)``````也将不为null,因此将不会重新分发此事件。同一个事件序列只会继续分发给mFirstTouchTarget。
对于当前的dispatchTouchEvent()
来说。事件已经被其他View消费,木已成舟。此时再想改变onInterceptTouchEvent()
为true,已经“无力回天”。
尾声
本篇文章到此就结束了,可能有朋友会问,关于CANCEL事件还没讲!没错,为啥没聊呢?因为我还没看。有机会的话,会把关于CANCEL事件的部分补上。
不着急,咱先把今天的文章唠明白。
网友评论