Android自定义View

View 事件分发规律总结(超详细)

2019-09-15  本文已影响0人  Kip_Salens

上一篇 事件分发—初体验 文章中实现了一个能够滑动关闭的 Demo,主要来体验一下事件分发,这篇来对 View 的事件分发做一下规律总结,包括【单一 View】,【单一 ViewGroup(不含子 View)】,【ViewGroup + View】,【ViewGroup + ViewGroup】。

1. 事件分发总览

当用户点击屏幕产生一个动作,这个动作通过底层硬件来捕获,然后交给 ViewRootImpl,接着将事件传递给 DecorView,DecorView 再交给 PhoneWindow,PhoneWindow 再交给Activity,然后接下来就是我们常见的 View 事件分发。

硬件 -> ViewRootImpl -> DecorView -> PhoneWindow -> Activity

先来看一张图,看一下事件分发的规则

view_event_content.png

我们知道常见的控制事件传递的 3 个回调函数,dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,其中 onInterceptTouchEvent 函数是 ViewGroup 所有。

(1) dispatchTouchEvent(针对 ACTION_DOWN)

事件从 Activity 开始,当 Activity 中的 dispatchTouchEvent 返回 true 和 false,均会消耗事件,事件不再向下传递,只有 super 时才会向下传递。super 这个返回操作实际上对于 ViewGroup 和 View 也是类似,ViewGroup 中,dispatchTouchEvent 返回使用 super 时,会交给 onInterceptTouchEvent,也相当于向下传递。View 中 dispatchTouchEvent 返回使用 super 时,会传递给 onTouchEvent,同样类似于向下传递,所有可以总结出 dispatchTouchEvent 中的返回 super,相当于向下分发,自己不消耗,一路 super 下去,就会形成一个事件分发的 U 型图。对于返回 true 时,很好记忆,直接消耗掉事件,不再分发也不向上回溯,后续的事件也会传到该位置。对于返回值 为 false 时,从图中可以看出,Activity 中直接消耗事件,事件不再向下传递,对于 ViewGroup 和 View,事件会回溯到父 View 或者 Activity 中,后续的事件不再传到这里。

(2) onInterceptTouchEvent,是 ViewGroup 中的方法,具有导流的作用,事件从自己的 dispatchTouchEvent (通过 super 返回)分发过来,它的返回值为 super 或者 false 时,表示不拦截事件,事件会传递到子 View 中。当返回值为 true 时,表示拦截事件,会将事件交给自己的 onTouchEvent 处理,一旦拦截,后续的事件不会再经过 onInterceptTouchEvent。拦截后分两种情况,一种是 onTouchEvent 返回 true,则后续事件直接交给 onTouchEvent 处理,不经过 onInterceptTouchEvent;另一种情况是 onTouchEvent 返回 false 或者 super,则后续不在传递到该 ViewGroup,自然也就不在经过 onInterceptTouchEvent。

(3) onTouchEvent,一般具体的执行动作会在这个方法中处理,在这个方法中,对 ACTION_DOWN 事件的处理尤为关键,当动作为 ACTION_DOWN 时,返回 true,表示事件分发到此结束,事件不再向下或者向上传递,同时后续事件 ACTION_MOVE 和 ACTION_UP 也会传到这里。当动作为 ACTION_DOWN 时,返回 false 时,事件会回溯到父 View 或者 Activity 中,后续事件不再交给此 View 处理。ACTION_MOVE 和 ACTION_UP 中的返回值仅仅影响父 View 或者 Activity 中能否收到 对应的事件,如在 ACTION_MOVE 返回 super 或者 false 时,父 View 或者 Activity 也会同样收到 move 事件,不影响后续事件分发,返回值 为 true 时,父 View 或者 Activity 不会收到 move 事件,对于 ACTION_UP 同样。

对于事件分发,一般更多的操作都是在 onInterceptTouchEvent、onTouchEvent,很少在 dispatchTouchEvent 做处理,dispatchTouchEvent 中处理也相对比较简单,因为它是一个事件的入口,消耗就自己处理,不消耗就交给父 View/ Activity,或者交给 onInterceptTouchEvent。所以我们主要对 onInterceptTouchEvent、onTouchEvent 来分析总结,对这几种组合总结 【单一 View】,【单一 ViewGroup(不含子 View)】,【ViewGroup + View】,【ViewGroup + View】。

2.单一 View

自定义一个 View,主要用来打印 log。

class MyView1 @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr) {

    companion object {
        const val TAG = "MyView1"
    }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        Log.e(TAG, "--- > dispatchTouchEvent --- > ${convertActivon2Name(ev.action)}")
        // (1)自己消耗事件
//        return true
        // (2)自己不消耗事件,回溯到父 View 中的 onTouchEvent
//        return false
        // (3)自己不消耗,传给自己的 onTouchEvent
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var result = false
        when (event.action) {
            // 事件为 false,后续事件都不会传递给 MyView1,ACTION_MOVE 和 ACTION_UP 中 true 或 false 不影响事件传过来
            // 所以 ACTION_DOWN 事件才是决定的关键,起到导流的作用
            MotionEvent.ACTION_DOWN -> {
                Log.e(TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                result = true
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e(TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                result = false
            }
            MotionEvent.ACTION_UP -> {
                Log.e(TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                result = true
            }
        }
        return result
        /**
         * (1)自己消耗事件
         * return true
         *
         * (2)自己不消耗事件,回溯到父 View 中的 onTouchEvent
         * return false
         *
         * (3)自己不消耗,回溯到父 View 中的 onTouchEvent
         * return super.onTouchEvent(event)
         */

    }
}

然后在布局文件中直使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/gray"
        tools:context=".SingleViewActivity">

    <com.ralf.vieweventtest1.MyView1
            android:id="@+id/single_view"
            android:layout_margin="40dp"
            android:background="@color/colorPrimary"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

</LinearLayout>

在MyView上按下触屏并滑动

(1) MyView1 - onTouchEvent - ACTION_DOWN 中返回 false

1970-01-02 14:22:43.123 5023-5023/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-02 14:22:43.124 5023-5023/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:22:43.126 5023-5023/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:22:43.152 5023-5023/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:22:43.157 5023-5023/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:22:43.157 5023-5023/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_UP

可以看到 onTouchEvent 在事件为 ACTION_DOWN 返回 false 时,只会收到第一次的 down 事件,后续的 move、up 事件都不再派发给 MyView1,事件均交给父 View 或者 Activity 处理,这里仅仅用 Activity 来显示。

(2) MyView1 - onTouchEvent - ACTION_DOWN 中返回 true

1970-01-02 14:28:27.480 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-02 14:28:27.481 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:28:27.503 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-02 14:28:27.503 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:28:27.556 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_UP
1970-01-02 14:28:27.556 5527-5527/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_UP

可以看到在第一次 down 事件过来时,返回 true,则后续事件 move 和 up 均会传到 MyView1,而 move 和 up 中的事件的返回值仅仅影响事件能否被父 view 或者 Activity 收到,不会影响事件后续事件传到 MyView1。

ACTION_MOVE 中返回 true, ACTION_UP 中返回 false 或者 super

1970-01-02 14:21:03.625 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-02 14:21:03.626 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:21:03.650 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-02 14:21:03.650 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:21:03.650 12865-12865/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:21:03.660 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_UP
1970-01-02 14:21:03.660 12865-12865/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_UP

ACTION_MOVE 中返回 true, ACTION_UP 中返回 false, SingleViewActivity 中不会收到 move 事件,会收到 up 事件

1970-01-02 14:33:10.218 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-02 14:33:10.219 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:33:10.235 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-02 14:33:10.236 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:33:10.241 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-02 14:33:10.242 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:33:10.242 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_UP
1970-01-02 14:33:10.243 5783-5783/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_UP
1970-01-02 14:33:10.243 5783-5783/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_UP

ACTION_MOVE 中返回 false, ACTION_UP 中返回 true, SingleViewActivity 中不会收到 up 事件,会收到 move 事件

1970-01-02 14:34:41.769 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-02 14:34:41.769 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-02 14:34:41.853 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-02 14:34:41.853 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:34:41.853 6053-6053/com.ralf.vieweventtest1 E/SingleViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-02 14:34:41.855 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_UP
1970-01-02 14:34:41.855 6053-6053/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_UP

好,到这里总结一下,对于单一 View 事件分发处理比较简单,主要看在 ACTION_DOWN 中事件拦截情况,返回true 时,后续事件也会传到这里,如果返回 false 或者 super,那么后续事件不会交给该 View 处理,会交给父 view 或者 Activity 处理。

single_view_event.png

3.单一 ViewGroup(不含子 View)

这里定义一个 MyLayout1,用来看下打印的日志,代码中有各种情况的注释,下面也会对各种情况做详细的说明。

class MyLayout1 @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr) {

    companion object {
        const val TAG = "MyLayout - 1"
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        // (1)自己消耗事件
//        return true
        // (2)自己不消耗事件,回溯到父 View 或者 Activity 中的 onTouchEvent
//        return false
        // (3)自己不消耗,传给自己的 onTouchEvent
        return super.dispatchTouchEvent(ev)
    }

    /**
     * return super.onInterceptTouchEvent(ev) 和 return false 的效果是一样的,不拦截事件,事件会传递到子 View
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        Log.e(MyLayout1.TAG, "--- > onInterceptTouchEvent -- ${convertActivon2Name(ev.action)}")
        var result = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // (1)false,不拦截事件,事件会传递到子 View
                // a. 如果子 View 拦截 down 事件(可点击或者在子 view 的 onTouchEvent 中返回 true)
                // 则后续的 move、up 等事件都将先传递到 ViewGroup 的 onInterceptTouchEvent 的方法
                // b. 如果没有子 View 拦截 down 事件
                // 则 down 事件将交由该 ViewGroup 的 onTouchEvent 来处理,后续事件都不再走 onInterceptTouchEvent
                // (2)true,事件拦截,事件会交给自己的 onTouchEvent 处理,
                // 无论 onTouchEvent 是否返回 true 都不再走 onInterceptTouchEvent,并且事件不会再向下传递给子 View
                result = false
            }
            MotionEvent.ACTION_MOVE -> {
                // (3)只有上面的 down 事件中为 false 时,才有可能走这里。有可能的原因:看子 view 的处理,有子 View 并且子 View 拦截 down 事件
                // a. false 不拦截,move 事件会流向子 View,子 View 中 onTouchEvent 中为 true,
                // 则后续的 move、up 等事件都将先传递到 ViewGroup 的 onInterceptTouchEvent 的方法
                // 子 View 中 onTouchEvent 中为 false,事件会回溯到 MyLayout1 父 View 或者 Activity 中的 onTouchEvent,MyLayout1 不接收,MyLayout1 只接收来自 onInterceptTouchEvent 拦截的事件
                // b. true 拦截,move 事件交给自己的 onTouchEvent 来处理, 后续 move 和 up 事件不经过 onInterceptTouchEvent,直接传给 MyLayout1 的 onTouchEvent,
                // move 和 up 的返回值影响父 View 或者 Activity 能否收到响应的事件
                result = true
            }
            MotionEvent.ACTION_UP -> {
                // (4) 只有 onInterceptTouchEvent 中 move 不拦截,
                // 并且子 View 中 onTouchEvent 中 down 事件为 true,up 事件才会走这里
                // a. true,事件【不会】传递到 MyLayout1 的 onTouchEvent,子 View 的 dispatchTouchEvent 中收到 ACTION_CANCEL,
                // MyLayout1 的 父 View 或者 Activity 会收到 up 事件
                // b. false 交给子 view 处理
                result = false
            }
        }
        return result
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {

        var result = false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                Log.e(MyLayout1.TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                // onInterceptTouchEvent 拦截 down 事件或者事件从子 view 回溯回来:
                // (1) true,后续事件 move 和 up 直接传到这里,不再通过 onInterceptTouchEvent
                // (2) false,事件会回溯到父 View 或者 Activity,后续事件不再传给到 MyLayout1
                result = false
            }
            /**
             * down 事件返回值直接影响 move 和 up,down 为 false,
             * 后续事件都会收不到,为 true,会收到 move 和 up,move 事件中的返回值不会影响 up 事件的接收
             */
            MotionEvent.ACTION_MOVE -> {
                Log.e(MyLayout1.TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                // 能到这里有 2 种情况
                // a. 由自己的 onTouchEvent 中 down 事件为 true,后续事会直接传到这里
                // b. onInterceptTouchEvent 不拦截 down 事件,子 View 拦截 down 事件,onInterceptTouchEvent 中拦截 move 事件
                // true 和 false 只影响 move 能否到达父 View 或者 Activity,不影响 up 事件的接收
                result = false
            }
            MotionEvent.ACTION_UP -> {
                Log.e(MyLayout1.TAG, "--- > onTouchEvent -- ${convertActivon2Name(event.action)}")
                // 能到这里只有一种情况
                // a.由自己的 onTouchEvent 中 down 事件为 true
                result = false
            }
        }
        return result
    }
}

单一 ViewGroup 布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="match_parent"
        android:background="@color/gray"
        tools:context=".SingleViewGroupActivity">

    <com.ralf.vieweventtest1.MyLayout1
            android:id="@+id/single_view_group"
            android:layout_width="match_parent"
            android:layout_marginStart="20dp"
            android:layout_marginEnd="20dp"
            android:layout_height="400dp"
            android:background="@color/colorPrimary">

    </com.ralf.vieweventtest1.MyLayout1>

</LinearLayout>

由于是 ViewGroup,所以就多了 onInterceptTouchEvent 方法,对于单一 ViewGroup, onInterceptTouchEvent 在 down 事件到来时:

(1) 如果拦截(返回 true),那么 down 事件传到 Mylayout1 的 onTouchEvent 中

(2) 如果不拦截(返回 false 或者 super),此时因为没有子 View,事件也会交给 onTouchEvent 处理

所以此时的 Mylayout1 相当于一个单一 View,自然事件的分发情况就和上面单一 View 的情况是一致的

single_viewgroup.png

4.ViewGroup + View

对于 ViewGroup 嵌套 View 的情况, 用我们前面自定义的两个 View,MyView1 和 Mylayout1

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/gray"
        tools:context=".ViewGroupAndViewActivity">

    <com.ralf.vieweventtest1.MyLayout1
            android:id="@+id/mylayout1"
            android:layout_width="match_parent"
            android:background="@color/colorPrimary"
            android:layout_margin="40dp"
            android:layout_height="match_parent">

        <com.ralf.vieweventtest1.MyView1
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="40dp"
                android:layout_gravity="center"
                android:background="@color/colorAccent"/>

    </com.ralf.vieweventtest1.MyLayout1>
</LinearLayout>

ViewGroup 中嵌套 View 的情况比较多,这里先给出一张看似凌乱的图,我们揪着这张图来分析。

viewgroup_view1.png

(1) MyLayout1 中 onInterceptTouchEvent 拦截 down 事件

此时 MyView1 不会收到事件,事件会传递到 Mylayout1 中的 onTouchEvent,onTouchEvent 中对事件的处理情况和单一 View 的处理保持一致。此时的就是图中左边的一根蓝线,事件从 onInterceptTouchEvent 转到 Mylayout1 中的 onTouchEvent

下面给出其中一种情况的 log,onInterceptTouchEvent 拦截 down 事件,Mylayout1 中的 onTouchEvent 拦截 down 事件,不拦截 move 和 up 事件,此时 VGAndViewActivity 中可以收到 move 和 up 事件。

1970-01-04 06:26:31.626 9967-9967/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onInterceptTouchEvent -- ACTION_DOWN
1970-01-04 06:26:31.628 9967-9967/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onTouchEvent -- ACTION_DOWN
1970-01-04 06:26:31.651 9967-9967/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 06:26:31.651 9967-9967/com.ralf.vieweventtest1 E/VGAndViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 06:26:31.724 9967-9967/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 06:26:31.724 9967-9967/com.ralf.vieweventtest1 E/VGAndViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 06:26:31.725 9967-9967/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onTouchEvent -- ACTION_UP
1970-01-04 06:26:31.725 9967-9967/com.ralf.vieweventtest1 E/VGAndViewActivity: --- > onTouchEvent -- ACTION_UP

(2) MyLayout1 中 onInterceptTouchEvent 不拦截 down 事件,MyView1 不拦截 down 事件

根据文章开头的事件分发概览图,可以知道,此时事件从 MyView1 的 onTouchEvent 回溯到 Mylayout1 中的 onTouchEvent,这种情况的处理和 (1) 的情况一致。此时也是由图中的右边的两条蓝色线体现,子 View 不拦截事件,事件会交给父 View 的 onTouchEvent 处理。

(3) MyLayout1 中 onInterceptTouchEvent 不拦截 down 事件,MyView1 拦截 down 事件

此时,down 事件到达 MyView1 的 onTouchEvent 中,后续的 move 和 up 事件还要看 onInterceptTouchEvent 中是否做拦截,因为后续事件会先经过 onInterceptTouchEvent。这里也分三种情况来讨论。

a. onInterceptTouchEvent 不拦截 move 和 up 事件

此时 move 事件会传递到 MyView1 的 onTouchEvent 中,MyView1 可以收到 move 事件,对应图中的粉色线,onInterceptTouchEvent 中 ACTION_MOVE 不拦截。那么在 MyView1 的 onTouchEvent 处理 move 和 up 事件时,返回值仅仅影响除 MyLayout1 以外的父 View 或者 Activity

  • 如果返回 true 事件不再传递,事件到此截止。
  • 如果返回 false,事件会再传递给 MyLayout1 的父 View 或者 Activity,但 MyLayout1 尽管是父 View,却收不到事件
1970-01-04 07:19:32.297 11314-11314/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onInterceptTouchEvent -- ACTION_DOWN
1970-01-04 07:19:32.298 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_DOWN
1970-01-04 07:19:32.298 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_DOWN
1970-01-04 07:19:32.318 11314-11314/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onInterceptTouchEvent -- ACTION_MOVE
1970-01-04 07:19:32.318 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_MOVE
1970-01-04 07:19:32.318 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 07:19:32.318 11314-11314/com.ralf.vieweventtest1 E/VGAndViewActivity: --- > onTouchEvent -- ACTION_MOVE
1970-01-04 07:19:32.331 11314-11314/com.ralf.vieweventtest1 E/MyLayout - 1: --- > onInterceptTouchEvent -- ACTION_UP
1970-01-04 07:19:32.331 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > dispatchTouchEvent --- > ACTION_UP
1970-01-04 07:19:32.332 11314-11314/com.ralf.vieweventtest1 E/MyView1: --- > onTouchEvent -- ACTION_UP
1970-01-04 07:19:32.332 11314-11314/com.ralf.vieweventtest1 E/VGAndViewActivity: --- > onTouchEvent -- ACTION_UP

从 Log 中可以看出,onInterceptTouchEvent 不拦截 move 和 up 事件,MyView1 中是可以收到后续的 move 和 up 事件,返回值仅仅影响除 MyLayout1 以外的父 View 或者 Activity

b. onInterceptTouchEvent 拦截 move 事件

如果 Mylayout1 在 onInterceptTouchEvent 中拦截 move 事件,此时事件就不再传递给 MyView1 了,而是由自己来处理,也就是说 MyLayout1 的 onTouchEvent 会收到后续的 move 和 up 事件,这也符合 onInterceptTouchEvent 的拦截特点,事件一旦拦截,后续事件都交给它来处理,同时,后续事件到来时,就不再经过 onInterceptTouchEvent,直接交给 MyLayout1 的 onTouchEvent 处理,对应图中左边黄色线。然后在 onTouchEvent 中事件的返回值影响 MyLayout1 的父 View 或者 Activity 能否收到 move 和 up 事件,true 表示截断事件,事件不再传递,所以收不到;false 不截断事件,可以收到事件

c. onInterceptTouchEvent 拦截 up 事件

事件能到这里,意味着上面 move 事件没有拦截。此时拦截 up 事件,猜想 up 事件会交给 MyLayout1 的 onTouchEvent 处理,但实际上 MyLayout1 的 onTouchEvent 中并没有收到事件,这里猜测可能不符合逻辑吧,只有手指在屏幕上有 move 事件时,然后才有手指抬起的 up 动作。具体的原因在源码分析时再去找答案,这里先记住总结。

那么拦截 up 事件时,会有哪些效果呢?首先在 MyView1 的 dispatchTouchEvent 中收到了一个动作,ACTION_CANCEL,源码中给出了说明,后面这个 View 不再收到事件,相当于一个 up 事件,因为 up 事件是事件的结束动作。总之,MyView1 不再收到任何动作。所以 onInterceptTouchEvent 拦截 up 事件,使得子 View 中收不到 up 事件,但是 MyLayout1 的父 View 或者 Activity 能收到 up 事件。对应图中很特殊的一根线,红色线段。

/**
 * Constant for {@link #getActionMasked}: The current gesture has been aborted.
 * You will not receive any more points in it.  You should treat this as
 * an up event, but not perform any action that you normally would.
 */
public static final int ACTION_CANCEL           = 3;

到这里就将 ViewGroup + View 组合情况的事件分发总结完了,虽然情况有点多,但总结之后也是很容易记住的。

首先 ViewGroup 中 onInterceptTouchEvent 拦截 down 事件和子 View 中不拦截 down 事件,即上面的情况 (1) 和 (2),事件就会交给 ViewGroup 的 onTouchEvent 处理,这种情况就相当于一个单一 View。

第二类情况就是 ViewGroup 中 onInterceptTouchEvent 不拦截 down 事件,子 View 拦截 down 事件,此时子 View 中才有机会收到 move 和 up 事件,因为 move 和 up 事件的接收要看 onInterceptTouchEvent 对事件的处理,onInterceptTouchEvent 中拦截 move,那么后续 move 和 up 事件就交给 ViewGroup 的 onTouchEvent 处理,子 View 不再接收事件。onInterceptTouchEvent 中不拦截 move,子 View 能收到 move 事件,其中返回值也需要注意,true 时自己处理事件,不再向上传递,false 时子 事件会向上传递,但是 onInterceptTouchEvent 中不拦截 move 的父 View(如上面的 MyLayout1) 不接收 move 事件,但是 MyLayout1 的 View 或者 Activity 中可以收到 move 事件。还有一种情况就是 onInterceptTouchEvent 中拦截 up,拦截后自己不接收 up 事件,子 View 中不接收 up 事件,父 View 或者 Activity 中可以收到 up 事件。

这里再给出一张脑图,对于各种情况的总结会更加直观。

viewgroup_view.png

这里还有一个小细节,当 onInterceptTouchEvent 中拦截 move 时,首次 move 事件过来时, MyView1 的 dispatchTouchEvent 中收到了一个动作 ACTION_CANCEL,MyLayout1 中并没有收到第一次的 move 事件,而 Activity 中收到了第一次的 move 事件,从 log 中可以看出,VGAndViewActivity 中第一次 move 动作的时间比 MyLayout1 的第一次 move 动作要早,即 MyLayout1 中 move 动作应该是第二次到来的 move。这个小细节也留到源码分析时去找答案。

view_event_log.png

5.ViewGroup + ViewGroup

对于这种情况的处理,基本上和上面 ViewGroup + View 的情况类似。

(1) MyLayout1 拦截 down 事件,事件都会交给 MyLayout1 的 onTouchEvent 处理,MyLayout2 不会收到事件

(2) Mylayout1 不拦截 down 事件,此时 MyLayout2 相当于一个单一 ViewGroup:

MyLayout2 中 onInterceptTouchEvent 中拦截 down 事件和不拦截 down 事件,事件都会交给 MyLayout2 的 onTouchEvent 处理,此时 MyLayout2 就是一个 View,此时就相当于 ViewGroup + View,按照上面 ViewGroup + View 的处理即可。

到这里,View 事件分发规律总结就完了,可以看出其实事件分发也并没有那么难,只要细心分析上述的几种情况,就已经将各种事件的处理涵盖全面了,日常开发中可能会有很多 View 的组合情况,按照合理拆分和要求,就会很容易完成事件的分发了。本篇文章是对各种事件分发情况的一种总结,并没有源码分析,有些细节问题到源码分析时再去寻求答案。

参考、练习代码

图解 Android 事件分发机制

Android Touch事件传递机制(一) -- onInterceptTouchEvent & onTouchEvent

代码地址

上一篇下一篇

猜你喜欢

热点阅读