为啥还在聊:事件分发?还不是因为不会!

1,410 阅读9分钟

前言

事件分发是一个老生常谈的话题,既然是一个“冷饭”,那为什么今天又开始“炒冷饭”了呢?说白了,还是自己高估了对事件分发的理解。

这里抛出几个问题:

  • 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事件的部分补上。

不着急,咱先把今天的文章唠明白。

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身