Android | 彻底理解 View 的事件分发机制

前言

网络上有很多关于 View 事件分发机制的文章,我本人也阅读过不少,大部分一上来就是各种结论和源码分析,暂不评价这些分析和结论是否正确,单从这样的出发点去学习 View 的事件分发机制就不恰当。

Android View 的事件分发过程情况多样,本文会带领读者在不看一行源代码的情况下,通过一个实例和十几张事件分发流程图来完整分析 View 的事件分发机制。话不多说,实践是检验真理的唯一标准,让我们开始学习吧。

本文大纲

View 事件分发思维导图.png

准备工作

View 的事件分发是由外向内的,总是从父 View 传递给子 View。在这里我写了三个自定义 View 来直观的分析这个过程,这种方式也最好理解。

这三个 View 是 OuterViewGroupInnerViewGroupMyView。分别继承自 ViewGroupViewGroupView。对于 OuterViewGroupInnerViewGroup 来说,需要重写 onMeasure 方法测量所有子 View,同时重写 onLayout 方法来确定子 View 相对自身的摆放位置,Kotlin 代码如下。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    measureChildren(widthMeasureSpec, heightMeasureSpec)
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    for (i in 0 until childCount){
        val child = getChildAt(i)
        child.layout(0, 0, child.measuredWidth, child.measuredHeight)
    }
}

上述代码在 onMeasure 中调用 measureChildren 测量了所有的子 View,在 onLayout 中将每一个子 View 放置在自身的左上角位置。然后我们在 TestActivity 的布局文件中将这三个 View 显示出来,xml 代码如下。

<?xml version="1.0" encoding="utf-8"?>
<cn.blogss.core.test.OuterViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/green">
    <cn.blogss.core.test.InnerViewGroup
        android:layout_width="@dimen/dp_300"
        android:layout_height="@dimen/dp_400"
        android:background="@color/gray">
        <cn.blogss.core.test.MyView
            android:layout_width="@dimen/dp_200"
            android:layout_height="@dimen/dp_200"
            android:background="@color/burlywood"/>
    </cn.blogss.core.test.InnerViewGroup>
</cn.blogss.core.test.OuterViewGroup>

最终的显示效果如下。

有读者可能会问,直接用两个 LinearLayout 套一个 Button 不是一样的效果吗,何必多此一举。其实不然,系统自带的这些控件内部很多都对 View 的事件做了拦截(消费)处理,不便于我们得到一个正确的结果,所以自定义 View 继承 ViewGroup 或 View来进行分析,最后的结果一定是最准确、纯净、原始的。

1. 默认的事件分发过程

简单的准备好实验条件后,首先来看一下 View 默认的事件分发流程。默认是指我们不对 View 的任何事件做拦截处理,遵循系统的默认规则。在这种情况下,重写 OuterViewGroupInnerViewGroupMyViewTestActivitydispatchTouchEventonTouchEvent 方法,日志打印出他们的调用顺序,即可得到 View 事件分发的过程。伪代码如下,省略了日志打印的代码。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

接着用手指在 MyView 上轻微滑动一下然后抬起,产生一次完整的 ACTION_DOWN,ACTIOIN_MOVE,ACTION_UP 事件,下文所述的事件分发流程均基于这个动作。需要注意的是,View 或 ViewGroup 的 dispatchTouchEventonTouchEvent 方法默认返回值都是 false,即不拦截任何事件。下面是手指在 MyView 上滑动抬起过程中,事件的传递结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN
InnerViewGroup: onTouchEvent: action = ACTION_DOWN
OuterViewGroup: onTouchEvent: action = ACTION_DOWN
TestActivity: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
TestActivity: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
TestActivity: onTouchEvent: action = ACTION_UP
事件分发流程图

为了让具体抽象化,读者可以将本文的 TestActivity 对应流程图中的 ActivityOuterViewGroup 对应 ViewGroup1InnerViewGroup 对应 ViewGroup2MyView 对应 View
根据日志的结果,可以画出默认的事件分发流程图如下。

默认的事件分发.png
ACTION_DOWN 事件的传递

根据上面日志的结果,ACTION_DOWN 由 TestActivity 的 dispatchTouchEvent 一直传递到 MyView 的 dispatchTouchEvent,然后由 MyView 的 onTouchEvent 向外传递到 TestActivity 的 onTouchEvent。

ACTION_MOVE 事件的传递

ACTION_MOVE 是在 ACTION_DOWN 事件后产生的,因为没有任何的 Activity 或 View 对 ACTION_DOWN 事件拦截,所以 ACTION_MOVE 传递到了 TestActivity 后就停止了。可以发现,ACTION_MOVE 事件的传递是受 ACTION_DOWN 事件影响的。

ACTION_UP 事件的传递

和 ACTION_MOVE 事件一样,受 ACTION_DOWN 事件拦截影响,ACTION_UP 传递到 TestActivity 后也停止向下传递了。

总结

根据上面的分析,在默认情况下,有如下结论。

  1. 对于 ACTION_DOWN 事件,会从最外层的 dispatchTouchEvent 传递到最内层的 dispatchTouchEvent,然后由最内层的 onTouchEvent 传递到最外层的 onTouchEvent

  2. 对于 ACTION_MOVE 和 ACTION_UP 来说,只会传递到 Activity 的 dispatchTouchEventonTouchEvent 中。

2. 在 dispatchTouchEvent 中拦截事件

2.1 在 Activity 的 dispatchTouchEvent 中拦截事件

修改 TestActivity 的代码,在 dispatchTouchEvent 中返回 true 来拦截一个完整的事件。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
TestActivity: dispatchTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
事件分发流程图

在 Activity 的 dispatchTouchEvent 中拦截事件的流程图如下。

在 Activity 的 dispatchTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件传递到 TestActivity 就停止向下传递了。

ACTION_MOVE 事件的传递

与 ACTION_DOWN 事件一样传递到 TestActivity 就停止向下传递了。

ACTION_UP 事件的传递

与 ACTION_DOWN 一样传递到 TestActivity 就停止向下传递了。

2.2 在 ViewGroup 的 dispatchTouchEvent 中拦截事件

修改 InnerViewGroup 的代码,在 dispatchTouchEvent 中返回 true 来拦截一个完整的事件。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
事件分发流程图

在 ViewGroup2 的 dispatchTouchEvent 中拦截事件的流程图如下。

在 ViewGroup2 的 dispatchTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

因为在 InnerViewGroup 的 dispatchTouchEvent 拦截了所有事件,所以 ACTION_DOWN 事件只能从 TestActivity 传递到 InnerViewGroup,而 MyView 接收不到任何事件。

ACTION_MOVE 事件的传递

ACTION_MOVE 从 TestActivity 传递到 InnerViewGroupMyView 接收不到任何事件。

ACTION_UP 事件的传递

ACTION_UP 从 TestActivity 传递到 InnerViewGroupMyView 接收不到任何事件。

这里读者思考一个问题,如果在 ViewGroup1 的 dispatchTouchEvent 中拦截事件,流程会是怎么样的呢?

2.3 在 View 的 dispatchTouchEvent 中拦截事件

修改 MyView 的代码,在 dispatchTouchEvent 中返回 true 来拦截一个完整的事件。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
MyView: dispatchTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
MyView: dispatchTouchEvent: action = ACTION_UP
事件分发流程图

在 View 的 dispatchTouchEvent 中拦截事件的流程图如下。

在 View 的 dispatchTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 从最外层(TestActivity)传递到了最内层(MyView)。

ACTION_MOVE 事件的传递

ACTION_MOVE 从最外层(TestActivity)传递到了最内层(MyView)。

ACTION_UP 事件的传递

ACTION_UP 从最外层(TestActivity)传递到了最内层(MyView)。

总结

根据上面的分析,在 dispatchTouchEvent 中拦截事件,可以得出如下结论。

  1. ActivityViewGroupView 的 dispatchTouchEvent 中拦截事件,所有的事件只能传递到拦截的地方,而处于其内部的控件将无法收到任何事件。

3. 在 onTouchEvent 中拦截事件

上面我们分析了在 dispatchTouchEvent 中拦截事件的事件传递规则,发现所有的事件会从最外层的 dispatchTouchEvent 传递到拦截处的 dispatchTouchEvent 就停止了。那么现在来看看在 onTouchEvent 中拦截事件的事件传递规则是怎么样的。

3.1 在 Activity 的 onTouchEvent 中拦截事件

修改 TestActivity 的代码,在 onTouchEvent 中返回 true 来拦截一个完整的事件。

override fun onTouchEvent(event: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN
InnerViewGroup: onTouchEvent: action = ACTION_DOWN
OuterViewGroup: onTouchEvent: action = ACTION_DOWN
TestActivity: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
TestActivity: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
TestActivity: onTouchEvent: action = ACTION_UP
事件分发流程图

在 Activity 的 onTouchEvent 中拦截事件的流程图如下,和默认的事件分发过程是一样的。

在 Activity 的 onTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件和默认的传递规则一样。由 TestActivity 的 dispatchTouchEvent 一直传递到 MyView 的 dispatchTouchEvent,然后由 MyView 的 onTouchEvent 向外传递到 TestActivity 的 onTouchEvent。

ACTION_MOVE 事件的传递

ACTION_MOVE 事件和默认的传递规则一样,传递到 TestActivity 后就停止了。

ACTION_UP 事件的传递

ACTION_UP 事件和默认的传递规则一样,传递到 TestActivity 后就停止了。

3.2 在 ViewGroup 的 onTouchEvent 中拦截事件

修改 InnerViewGroup 的代码,在 onTouchEvent 中返回 true 来拦截一个完整的事件。

override fun onTouchEvent(event: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN
InnerViewGroup: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: onTouchEvent: action = ACTION_UP
事件分发流程图

在 ViewGroup2 的 onTouchEvent 中拦截事件的流程图如下。

在 ViewGroup2 的 onTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件由 TestActivity 的 dispatchTouchEvent 一直传递到 MyView 的 dispatchTouchEvent,然后由 MyView 的 onTouchEvent 向外传递到 InnerViewGroup 的 onTouchEvent。

ACTION_MOVE 事件的传递

ACTION_MOVE 事件由 TestActivity 的 dispatchTouchEvent 一直传递到 InnerViewGroup 的 dispatchTouchEvent,然后到达 InnerViewGroup 的 onTouchEvent 后停止。

ACTION_UP 事件的传递

和 ACTION_MOVE 事件一样。

3.3 在 View 的 onTouchEvent 中拦截事件

修改 MyView 的代码,在 onTouchEvent 中返回 true 来拦截一个完整的事件。

override fun onTouchEvent(event: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
MyView: dispatchTouchEvent: action = ACTION_MOVE
MyView: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
MyView: dispatchTouchEvent: action = ACTION_UP
MyView: onTouchEvent: action = ACTION_UP
事件分发流程图

在 View 的 onTouchEvent 中拦截事件的流程图如下。

在 View 的 onTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件由外向内传递到 MyView,因为在 MyView 的 onTouchEvent 中拦截了事件,所以 ACTION_DOWN 在 MyView 停止向外传递。

ACTION_MOVE 事件的传递

与 ACTION_DOWN 事件传递流程一样。

ACTION_UP 事件的传递

与 ACTION_DOWN 事件传递流程一样。

总结

我们已经分析完了在 dispatchTouchEvent 和 onTouchEvent 中拦截事件的事件传递规则,现在结合两种拦截方式做一个总结,看看这两种拦截方式的内在联系。

  1. 可以发现,事件传递遵循由外向内(外层的 dispatchTouchEvent 到内层的 dispatchTouchEvent),然后由内向外(内层的 onTouchEvent 到外层的 onTouchEvent)的顺序

  2. 如果事件在由外向内传递过程中被拦截(消费)了,那么由内向外的过程就不会发生,这也很容易理解。

  3. 在 onTouchEvent 中拦截(消费)事件,不影响事件由外向内传递到拦截处的过程。

  4. 在 onTouchEvent 中拦截(消费)事件,只影响事件由内向外传递的过程,对于 ACTION_MOVE,其内外层的 onTouchEvent 均不会被调用。

  5. 从打印出的日志和流程图可以发现,ACTION_MOVE 和 ACTION_UP 事件在使用任何拦截策略下,他们的传递流程都是一样的,包括下文将要讨论的拦截策略。

4. 在 onInterceptTouchEvent 中拦截事件

因为 onInterceptTouchEvent 方法在遇到滑动冲突的场景很有使用价值。所以我们有必要研究一下在 ViewGroup 的 onInterceptTouchEvent 方法中拦截事件,事件的传递顺序是怎样的。

4.1 在 onInterceptTouchEvent 中拦截所有事件

InnerViewGroup 中重写 onInterceptTouchEvent 返回 true。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: onTouchEvent: action = ACTION_DOWN
OuterViewGroup: onTouchEvent: action = ACTION_DOWN
TestActivity: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
TestActivity: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
TestActivity: onTouchEvent: action = ACTION_UP
事件分发流程图

在 ViewGroup2 的onInterceptTouchEvent 中拦截所有事件的流程图如下。

在 ViewGroup2 的 onInterceptTouchEvent 返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件由外向内传递到 InnerViewGroup,然后由内向外传递到 TestActivity。

ACTION_MOVE 事件的传递

ACTION_MOVE 事件只传递到了 TestActivity,并没有传递到 InnerViewGroup。

ACTION_UP 事件的传递

和 ACTION_MOVE 事件一样。

4.2 在 onInterceptTouchEvent 和 onTouchEvent 中拦截事件

InnerViewGroup 中重写 onInterceptTouchEvent 和 onTouchEvent,返回 true,代码如下。

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return true
}

override fun onTouchEvent(ev: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: onTouchEvent: action = ACTION_UP
事件分发流程图

在 onInterceptTouchEvent 和 onTouchEvent 中拦截事件的流程图如下。

在 ViewGroup2 的 onInterceptTouchEvent 和 onTouchEvent返回 true.png
ACTION_DOWN 事件的传递

ACTION_DOWN 事件传递到 InnerViewGroup 后停止。

ACTION_MOVE 事件的传递

ACTION_MOVE 事件传递到 InnerViewGroup 后停止。

ACTION_UP 事件的传递

ACTION_UP 事件传递到 InnerViewGroup 后停止。

总结

  1. 单纯在 onInterceptTouchEvent 中拦截,ACTION_DOWN 会在拦截处停止传递,而后续的 ACTION_MOVE 只会传递到 Activity。

  2. 而如果同时在 onInterceptTouchEvent 和 onTouchEvent 中拦截事件,后续的 ACTION_MOVE 可以传递到拦截处。

5. 拦截 ACTION_MOVE 事件(必会)

以上分析基本覆盖了事件所有的拦截策略,此时读者应该对事件传递的流程有了全局的了解。接下来我们要研究的是拦截 ACTION_MOVE 事件,这部分是全文的重点,希望读者能够牢牢掌握。

研究 View 事件分发机制的目的是为了能够写出一个标准的自定义 View,在面对一些有手势交互的自定义 View 或者滑动冲突时,我们能解决这些问题。而处理 ACTION_MOVE 事件就是重点。

5.1 在 onInterceptTouchEvent 中拦截 ACTION_MOVE 事件

需要注意的是,如果想要拦截 ACTION_MOVE 事件,那么 ACTION_MOVE 就必须能够传递到拦截点。如果连 ACTION_MOVE 都不能到达拦截处,那么讨论拦截 ACTION_MOVE 就很荒谬。根据前面的分析,在 MyView 的 onTouchEvent 中返回 true,这样 ACTION_MOVE 就能够由最外层传递到最内层,再来回顾一下前面 3.3 小节的这张图。

在 View 的 onTouchEvent 返回 true.png

因此重写 MyView 的 onTouchEvent 返回 true。

override fun onTouchEvent(ev: MotionEvent): Boolean {
    return true
}

然后在 InnerViewGroup 中重写 onInterceptTouchEvent 来拦截 ACTION_MOVE 事件,其他事件遵循默认的规则,即不拦截,代码如下。

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when(ev.action) {
        MotionEvent.ACTION_MOVE -> {
            return true
        }
    }
    return super.onInterceptTouchEvent(ev) 
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
MyView: dispatchTouchEvent: action = ACTION_CANCEL
MyView: onTouchEvent: action = ACTION_CANCEL

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: onTouchEvent: action = ACTION_MOVE
TestActivity: onTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: onTouchEvent: action = ACTION_UP
TestActivity: onTouchEvent: action = ACTION_UP
事件分发流程图

在 onInterceptTouchEvent 中拦截 ACTION_MOVE 事件的流程图如下。

在 onInterceptTouchEvent 中拦截 ACTION_MOVE 事件.png
ACTION_DOWN 事件的传递

因为我们没有对 ACTION_DOWN 事件做任何的拦截处理,只在 MyView 的 onTouchEvent 返回了 true,所以 ACTION_DOWN 事件的传递,和在 3.3 小节中 ACTION_DOWN 的流程是一致的。

ACTION_MOVE 事件的传递

为了让结果更加清晰,这里我触发了两次 ACTION_MOVE 事件。

第一次 ACTION_MOVE 由 TestActivity 传递到了 InnerViewGroup。因为我们在 InnerViewGroup 中拦截了 ACTION_MOVE,所以肯定传递不到 MyView。取而代之的是 ACTION_MOVE 变成了 ACTION_CANCEL 传递到了 MyView 中。需要注意,当我们收到ACTION_CANCEL消息时,就表示该控件后续不会再获得消息

可以看见,第二次及其之后的 ACTION_MOVE 永远传递不到 MyView

ACTION_UP 事件的传递

与第二次及其之后的 ACTION_MOVE 传递流程一致。

5.2 在 dispatchTouchEvent 中拦截 ACTION_MOVE 事件

除了在 onInterceptTouchEvent 中拦截 ACTION_MOVE 事件之外,还可以在 dispatchTouchEvent 中进行拦截。修改 InnerViewGroup 的代码如下,在 dispatchTouchEvent 中拦截 ACTION_MOVE,其他事件保持默认处理,即不拦截。

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    when(ev.action) {
        MotionEvent.ACTION_MOVE -> {
            return true
        }
    }
    return super.dispatchTouchEvent(ev)
}

MyView 的 onTouchEvent 仍然返回 true。

override fun onTouchEvent(ev: MotionEvent): Boolean {
    return true
}

下面是事件传递的结果。

TestActivity: dispatchTouchEvent: action = ACTION_DOWN
OuterViewGroup: dispatchTouchEvent: action = ACTION_DOWN
InnerViewGroup: dispatchTouchEvent: action = ACTION_DOWN
MyView: dispatchTouchEvent: action = ACTION_DOWN
MyView: onTouchEvent: action = ACTION_DOWN

TestActivity: dispatchTouchEvent: action = ACTION_MOVE
OuterViewGroup: dispatchTouchEvent: action = ACTION_MOVE
InnerViewGroup: dispatchTouchEvent: action = ACTION_MOVE

TestActivity: dispatchTouchEvent: action = ACTION_UP
OuterViewGroup: dispatchTouchEvent: action = ACTION_UP
InnerViewGroup: dispatchTouchEvent: action = ACTION_UP
MyView: dispatchTouchEvent: action = ACTION_UP
MyView: onTouchEvent: action = ACTION_UP
事件分发流程图

在 dispatchTouchEvent 中拦截 ACTION_MOVE 事件的流程图如下。

在 dispatchTouchEvent 中拦截 ACTION_MOVE.png
ACTION_DOWN 事件的传递

因为没有对 ACTION_DOWN 事件做任何的拦截,所以 ACTION_DOWN 消息传递到了 MyView

ACTION_MOVE 事件的传递

因为在 ViewGroup2 的 dispatchTouchEvent 中拦截了 ACTION_MOVE,所以 ACTION_MOVE 传递到 ViewGroup2 就停止了。和在 ViewGroup2 的 onInterceptTouchEvent 拦截 ACTION_MOVE 不一样,MyView 后续并没有收到 ACTION_CANCEL 消息。

ACTION_UP 事件的传递

因为没有对 ACTION_UP 事件做任何的拦截,所以 ACTION_DOWN 消息传递到了 MyView

总结

  1. 一般可以在 onInterceptTouchEvent 和 dispatchTouchEvent 中拦截 ACTION_MOVE 消息,但要确保 ACTION_MOVE 消息可以传递到拦截处。
  2. 当你想要处理 ACTION_MOVE 消息时,有经验的工程师会告诉你在控件的 onTouchEvent 中返回 true 就可以了,从第 3 节可以知道,ACTION_MOVE 是可以传递到该控件的。但是要注意该控件的外层没有拦截掉 ACTION_MOVE。

到此,本文就全部结束了,相信读者对 View 的事件分发一定有了新的理解。

写在最后

如果你对我感兴趣,请移步到 http://blogss.cn ,或关注公众号:程序员小北,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注,这是我持续创作的动力 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于掘金,未经许可禁止转载 ©️
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容