事件分发是一个老生常谈的话题,既然是一个“冷饭”,那为何今天又开始“炒冷饭”了呢?说白了,仍是本身高估了对事件分发的理解。java
这里抛出几个问题:api
onTouchEvent()
不会被响应? -> 答案在:方法展开2部分。onTouchEvent()
返回了true,为啥它下层的View就不再会响应任何事件回调了? -> 答案在:方法展开1部分onTouchEvent()
并返回了true,那么它的onInterceptTouchEvent()
还会被回调吗? -> 答案在:1.二、部分总结部分。dispatchTouchEvent()
并直接返回true,会怎么样?-> 答案在:方法展开2部分。若是各位小伙伴能够很是清晰的回答这些问题,那么这篇文章就不用看了,左上角点X,唱、跳、Rap、打会篮球什么的...固然若是你愿意留下来点点广告,那也是极好的~哈哈学习
既然叫作事件分发,那么本质其实就是分发。我猜你们刚开始了解这一块内容时,确定绕不开三个方法:dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
。不过我真以为,扯上后边俩个方法,反而把问题复杂化。动画
对于事件分发来讲,核心就是dispatchTouchEvent()
的实现,onInterceptTouchEvent()
、和onTouchEvent()
只是让咱们参与到分发流程当中来的接口而已。this
所以,这篇文章的核心就在于梳理、阅读ViewGroup和View的dispatchTouchEvent()
方法实现。相信我,阅读完这篇文章绝对有收获~~spa
源码基于api-28code
关于dispatchTouchEvent()的逻辑,这里主要分为俩个大部分,前半部分侧重于事件消费对象的肯定(1.1部分);后半部分侧重于对事件消费对象的后续分发(1.2部分)。orm
这部分代码逻辑主要为了:cdn
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的逻辑
}
复制代码
此时总结并解释一下开头写的:一、找到并记录命中消费事件的View;二、对各层View的DOWN事件分发。 一、找到并记录命中消费事件的View: 当DOWN来到ViewGroup的时候,若是自身不拦截,那么就会尝试分发。最终将根据命中View是否消费(重写onTouchEvent()
/onTouch()
/重写dispatchTouchEvent()
)来决定是否对mFirstTouchTarget进行赋值(记录命中消费事件的View)。 二、对各层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,那这种想法能不能实现呢?答案是不能,为何请阅读:事件分发额外阅读。
此部分逻辑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;
}
// 取消的逻辑暂时不作考虑
}
}
}
复制代码
此部分代码较少,并且逻辑清晰。主要就在于俩个分支,一个是没有找到可以消费的View,那么分发给本身,直接super.dispatchTouchEvent()
。本身的onTouchEvent()处理。不然经过mFirstTouchTarget,分发后续产生的事件。
此部份内容,请结合1、ViewGroup中的dispatchTouchEvent()“食用”
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个问题)白话文:背锅的已经找到,此事无序再追查。哈哈~
// 能够看到,对于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()返回值。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
复制代码
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事件的部分补上。
不着急,咱先把今天的文章唠明白。