总览
这篇文章从源码角度着重讲述事件处理机制原理,会从以下几个方面(但不仅限于)进行深入讲解:
1.dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()详解
2.onTouch() 和 onClick() 有什么关系?
3.为什么手指按下 button 后再移出去 onClick() 不执行?
4.如何解决事件冲突?
⚡ 本篇文章有 一定难度,会将事件分发和消费讲透彻,请做好心理准备。
✔️ 本文阅读时间约为:15+分钟
由于涉及到的源码很多,为了便于理解我只展示核心代码,其它与重点内容不相关的在这里就省略了,建议大家看懂整个流程可以自己去翻翻源码看看细节。本篇文章是在
API 31基础下创作的。
分发机制
在讲第一个问题前我们首先要清楚这个事件是从哪里来的。在 Android驱动层 捕获到消息后,它会将消息发送给当前的 app,然后再由 ViewRootImpl 的 mInputEventReceiver 接收并层层分发下去,之后到我们的 Activity 中,再由 Activity 进行分发,我们的重点就是从这里开始的。
接下来就是带大家粗略看一看事件是怎么到 view 手中的。
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 关键代码,继续调用 PhoneWindow 的 superDispatchTouchEvent()
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity 接着调用 PhoneWindow 的 superDispatchTouchEvent() 方法。
// PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
// 关键代码,继续调用 DecorView 的 superDispatchTouchEvent()
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow 接着调用 DecorView 的 superDispatchTouchEvent() 方法。
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用 super 的 dispatchTouchEvent()
// 实际上调用的是 ViewGroup 的 dispatchTouchEvent()
return super.dispatchTouchEvent(event);
}
接着 DecorView 调用 super 的 dispatchTouchEvent(),我们知道 DecorView 的父类是 FrameLayout ,但是它没用重写该方法,因此就调用 ViewGroup 的 dispatchTouchEvent()。至此,事件终于和 View 产生联系了。
对上面的过程做一个总结,我们的事件分发是从 Activity -> PhoneWindow -> DecorView -> ViewGroup,最后由我们的 View 去消费事件。
我们要很清晰的认识一点,ViewGroup 中的 dispatchTouchEvent() 是用来 分发事件 的,而 View 中的 dispatchTouchEvent() 才是真正用来 消费事件 的。
View 的事件消费
说到事件的分发与消费,那必然涉及到最重要的三个方法 dispatchTouchEvent(),onInterceptTouchEvent(), onTouchEvent() ,我们一点一点来说。
事件都是从 父容器 -> 子view ,从 ACTION_DOWN -> ACTION_MOVE -> ACTION_UP 这么一个过程,也就是说会先经历 ViewGroup.dispatchTouchEvent() 分发事件,再走 View.dispatchTouchEvent()去消费事件。但是为了便于大家对整个流程的理解,这里我先讲 View.dispatchTouchEvent() 事件消费 。
View.dispatchTouchEvent()
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
// 关键变量,决定此事件是否被消费掉
// 如果被消费掉就不会再往下分发此事件
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 如果设置了 setOnTouchListener
// 并且实现了onTouch() 方法且返回值是 true
// 那么就会执行 onTouch() 里面的内容
// 这时返回值 result 改为 true,事件被消费掉
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果没有设置setOnTouchListener
// result 也还是 false
// 就会走 onTouchEvent()
// 相反如果设置了监听,实现了onTouch()并返回true 就会执行 onTouch()
// result 就会为 true
// 短路与 就不会执行 onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
...
// 如果到这里 result 还是 false 说明该事件未被此 view 消费 返回false
// 如果返回 true 代表已消费,不再将此事件继续向下分发
return result;
}
在这里就出现了和我们经常打交道的 onTouch() 和 onTouchEvent()。
我们先来看看 onTouch(),onTouch() 的意思是当事件到达 view 时应该处理的逻辑。onTouch() 其实就是一个接口方法,等待着我们去实现,当事件到达 view 层时会回调我们在 onTouch() 中写的逻辑。
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
现在我们再来看看 onTouchEvent() 。
View.onTouchEvent()
// View.java
public boolean onTouchEvent(MotionEvent event) {
// 获取当前手指 x 坐标
final float x = event.getX();
// 获取当前手指 y 坐标
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 如果在 ACTION_UP 中修改掉了 mPrivateFlags,那么 if 就不会执行
// 因此 onClick 就不会执行
// 这就做到了手指按下 view 再移出去 onClick事件 不会响应
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 这里进去就会调用 li.mOnClickListener.onClick(this);
// 紧接着下一行就将返回值 result 改成 true
// 如果我们实现了 onClick 方法,就会回调我们自己写的逻辑
performClickInternal();
}
}
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
...
case MotionEvent.ACTION_MOVE:
...
int touchSlop = mTouchSlop;
// 当前的手指移出此 view 时会走这段代码
if (!pointInView(x, y, touchSlop)) {
// 执行removeTapCallback() 内部会修改 mPrivateFlags
// 导致 ACTION_UP 中的 prepressed 条件不成立 不会执行 performClickInternal()
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
...
break;
}
return true;
}
return false;
}
onTouch() 和 onClick() 的关系
至此,我们知道 1️⃣如果实现了 onTouch() 且返回值是 false 并且也实现了 onClick() ,那么当事件到达此 view 的时候会先经历 onTouch() 再执行 onClick,执行完 onClick() 后这个事件就被消费了(因为调用 onClick() 后,result 就会改成 true)。2️⃣如果实现了 onTouch() 且返回值是 true,那么 result 就会为 true, onClick() 就不会执行,只会执行 onTouch()。
❤️ 此外,当手指按在 view 上再移出时,会执行 removeTapCallback() 修改 mPrivateFlags 的值,导致 performClickInternal() 外层的 if 条件不满足,因此不会执行 performClickInternal() ,进而 onClick() 也不会响应了。
这就是整个 view 的事件处理。接下来我们讲讲 事件分发 。
ViewGroup 的事件分发
搞懂 View 的事件消费,我们需要明白一点,如果此事件被消费,则返回 true,如果没有消费就返回 false,此事件继续向下分发。接下来让我们来看看 ViewGroup.dispatchTouchEvent() 。
ViewGroup 的事件分发是一个递归的过程,先来看一遍正常的分发流程。
ViewGroup.dispatchTouchEvent()
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 表示此事件是否分发处理完成
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 每次 down 都是一个新的动作
// 因此这里会将所有的旧状态全部清除
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 这个变量代表 ViewGroup 是否拦截此事件
// 正常的流程 ACTION_DOWN , ViewGroup 都是不会拦截的
// intercepted 为 false
final boolean intercepted;
// mFirstTouchTarget 是一个链表的第一个结点,代表按下的第一根手指和消费事件的 view
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// onInterceptTouchEvent() 是暴露给外部调用
// 用来设置 intercepted 值的方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 如果此事件不是 ACTION_DOWN 并且 mFirstTouchTarget 为空,父容器就拦截掉
intercepted = true;
}
// 检查是否取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
TouchTarget newTouchTarget = null;
// 重要变量,表示是否有 view 处理了 DOWN 事件
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// 注意这里只有 DOWN 事件才能进来,MOVE 是不会走这段代码的
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled()
// 拿到此父容器的所有 子view
final View[] children = mChildren
// 倒序遍历所有 子view,询问其是否处理 ACTION_DOWN 事件
// 为什么倒序?
// 最后面的 view 在屏幕的最顶层
// 我们总是希望点击最顶层的 view
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// dispatchTransformedTouchEvent() 内部会调用 View.dispatchTouchEvent()
// 如果 dispatchTransformedTouchEvent() 返回 true 代表消费此事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 这里会调用 addTouchTarget()
// 会给 mFirstTouchTarget 赋值,并将当前的 view 添加进 TouchTarget 中
// 为分发 ACTION_MOVE 和 ACTION_UP 事件做准备
// 谁处理了 ACTION_DOWN 就会将剩下的事件分发给谁
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 置为 true 表示 DOWN 事件有 view 处理过
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
}
if (mFirstTouchTarget == null) {
// 如果 子view 都没有处理当前事件
// mFirstTouchTarget 就会为空
// 父容器自己就来处理当前事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTarget 表示有无 view 处理此事件
// 如果处理过就直接放回 true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 如果没有 view 处理过此事件就会进入这里
// 正常流程 intercepted 为 false
// 因此 cancelChild 也是false
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// target.child 就是 消费 ACTION_DOWN 的 子view
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;
}
}
...
return handled;
}
❄️ 将整个 ViewGroup.dispatchTouchEvent() 分为三块代码,让我们来看看下面这张图,重新梳理一下思路:
请大家根据这幅图去理解上面的代码,搞懂正常的分发流程后才能清楚如何去解决事件冲突。
这是 ACTION_DOWN 的分发流程,前面说过 ACTION_MOVE 是不会走进第二块代码的。事实上,谁处理了 ACTION_DOWN 谁才有资格处理 ACTION_MOVE 和 ACTION_UP 。
现在继续对上面的过程进行解释说明,dispatchTransformedTouchEvent() 和 addTouchTarget() 出现很多次,对于我们理解 ViewGroup 的分发流程也很重要。
dispatchTransformedTouchEvent()内部调用的是 View.dispatchTouchEvent() 。
/*
* 重要方法 dispatchTransformedTouchEvent()
* 用来处理 子view 或者 父容器 的事件消费的
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// 老朋友了,代表此事件是否被消费
final boolean handled;
// 这里是后面解决事件冲突的重要方法
// 继续埋个坑,后面再讲
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
// child 为 null 表示父容器亲自来处理此事件
// 对应前面 mFirstTouchTarget==null 时,无子view处理,父容器自己处理
if (child == null) {
// 调用 View.dispatchTouchEvent()
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
// child 不为空
// 这里就是 子view 处理事件
// 调用 View.dispatchTouchEvent() 进行事件消费
// 这里忘记的同学去看看前面的View.dispatchTouchEvent()
handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
// 返回我们的老朋友
return handled;
}
这里有一个链表结构,主要是为了用来保存多根手指操作(用int来保存的),一根手指就用一位 比特位 来储存,因此理论上来讲最多能识别 32 根手指的操作。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
/*
* 处理了 DOWN 事件后会执行此方法 忘记的同学再去看看前面的分发流程
* TouchTarget 是一个结点,用来储存 按下屏幕的手指 和 处理事件的view
* 在 MOVE 事件处理时,就是从这里拿 view
* 如果是单指操作就不用在意其是否是链表
*/
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
// 这两行代码将target的next域 和 mFirstTouchTarget赋值
// 因为是在 DOWN 之后调用的此方法
// 在这里其实就是将链表的第一个结点值域赋值为 mFirstTouchTarget
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
ViewGroup 的正常的分发流程就讲完了,总的来说就是当事件传到 ViewGroup 后,它会倒序遍历自己的所有孩子,询问它们有没有要处理此事件的。
1️⃣ 如果有,就走 View.dispatchTouchEvent() 去消费事件并执行消费事件的逻辑。
2️⃣ 如果没有,父容器自己就会亲自执行 View.dispatchTouchEvent() 去消费事件。
一旦此事件被消费,也就是会返回 true,此事件便不会继续向下分发了。否则一直向下分发事件直到被消费。
如何解决事件冲突?
大家在开发中肯定会遇到事件冲突,只要两个 view 叠在一起,那冲突是必然的,那怎么解决呢?如何做到像 抖音 那样,上下左右 都能滑动呢?接下来,我们亲自来实现一下。
❗❗ 解决事件冲突前,建议先明白上面的 ViewGroup 正常事件分发流程。
❓❓ 有的同学可能会说,我写了那么多的 ViewPager 嵌套 RecyclerView 为什么没有出现过事件冲突啊?那是因为 google工程师 在内部帮我们处理了,任何叠在一起的 view,肯定是会产生事件冲突的。
⭐ 我们首先来看看 onInterceptTouchEvent(),这个大家应该很熟悉了吧。这是我们上面 ViewGroup.dispatchTouchEvent() 里 第一块代码 里出现过的,用来设置 intercepted 的值,决定父容器是否拦截此事件。
⭐ 而 requestDisallowInterceptTouchEvent() ,其内部是通过修改 mGroupFlags 的值来间接修改 intercepted 的值。
好了,现在我们来当一把 google工程师 吧!
这里为了方便,我自定义两个 View 继承自 RecyclerView 和 ViewPager,因为这样自己就不用再单独写事件的消费了😍。
// MyViewPager extends ViewPager
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
// 我们不能让父容器拦截 ACTION_DOWN 事件
// 因为如果 子view 连 DOWN 事件都接收不到
// 那么肯定是无法接收其它事件的
if (e?.action == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(e)
return false
}
// 拦截除 DOWN 事件的其他事件
return true
}
private var lastX = 0f
private var lastY = 0f
// MyRecyclerView extends RecyclerView
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val dx = ev!!.x
val dy = ev.y
when(ev.action) {
MotionEvent.ACTION_DOWN -> {
// 请求父容器不要拦截子view接收事件
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
// 如果 横向滑动的距离 大于 纵向滑动的距离
// 就请求父容器拦截此事件 不让子view接收到
if (abs(lastX - dx) > abs(lastY - dy)) {
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {}
}
lastX = dx
lastY = dy
return super.dispatchTouchEvent(ev)
}
那么究竟为什么写了这区区几行代码就能解决掉事件冲突,达到能够 上下左右 滑动的目的呢?
我之前是不是还留了个坑?现在是时候将它填上了,我们来看看 ViewGroup.dispatchTouchEvent() 最后那段代码。
// View.dispatchTouchEvent()
if (mFirstTouchTarget == null) {
// 别忘了,这是父容器自己处理事件
// 标记一下 这是父容器把处理事件的权利从子view手中抢过来的关键
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 重点关注的地方
// 之前的正常流程 cancelChild 是 false
// 现在因为我们在外部让父容器拦截了事件 intercepted 变为 true
// 因此 cancelChild 也变成true
// 接下来就是执行 dispatchTransformedTouchEvent()
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果cancelChild 为true
// 将 mFirstTouchTarget 置空,target 置空
if (cancelChild) {
if (predecessor == null) {
// next 是链表的下一个结点 MOVE时后面没有结点了 其实就是 null
// 这个变量置为 null
// 是父容器把处理事件的权利从子view手中抢过来的关键
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
✔️ ACTION_DOWN 事件和正常的流程一样,分发给了 子view,也就是 RecyclerView 。
✔️ 当 ViewPager 第一次分发到 ACTION_MOVE 事件时,这里因为我们在外部通过调用 parent.requestDisallowInterceptTouchEvent(false) ,让父容器拦截了 MOVE 事件,因此这时候 intercepted 为 true,导致 cancelChild 为 true 。
// 还记得这个方法吗,我在这里也留了个坑
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// 之前正常的流程,cancel 是 false
// 现在父容器拦截了事件,cancel 是 true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 执行 View.dispatchTouchEvent()
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
✔️ 现在,当 ACTION_MOVE 事件再次分发到 ViewPager 时,因为前面已经将 mFirstTouchTarget 置为空,因此 if 条件满足,这里就让父容器(ViewPager)亲自去处理事件了。
至此成功将事件从 子view 手中抢过来,解决了滑动事件冲突。
我们再来对上面 ViewPager 将事件从 RecyclerView 手中抢过来的过程总结一下,当我们手指开始左右滑动时:
1️⃣ 事件 ACTION_DOWN 初次传递到 ViewPager 时,会走正常流程,询问子view(RecyclerView) 是否处理,紧接着RecyclerView 处理掉,ACTION_DOWN 事件被消费。
2️⃣ 紧接着事件 ACTION_MOVE 传递给 ViewPager ,我们在外部将 intercepted 置为 true,导致 cnacelChild 为 true,在 RcyclerView 消费掉此事件后,mFirstTouchTarget 置为空。
3️⃣ 最后事件 ACTION_MOVE 再次传递到 ViewPager,父容器依然拦截着此事件,但由于 mFirstTouchTarget 为空,所以满足 if 条件,轮到父容器(ViewPager)去处理事件。
写在最后
✈️✈️ 篇幅稍稍过长,但是耐心看完的同学一定会有收获。这篇文章多看两遍,大概率是能填补关于 View 这块的事件分发和消费的知识点的空缺的。
欢迎大家在评论区提出问题或者自己的观点。