Android开发中经常会遇到多个View、ViewGroup嵌套的情况,
此时就可能遇到滑动冲突的问题。
为了这种问题,就必须对View的事件传递机制有一定的了解。

本篇博客就以一些简单的例子,
来看看Activity、View、ViewGroup三者的触摸事件传递机制。


一、基本概念
Android中的触摸事件对应于MotionEvent类,事件的类型包括ACTION_DOWN、ACTION_UP、ACTION_MOVE等。
不同事件类型的意义,大家可以参看源码对应的注释信息,此处不做赘述。

一次完整的事件传递主要包括三个阶段,分别是事件的分发、拦截和消费。
分发:
事件的分发对应着dispatchTouchEvent方法,原型如下:

public boolean dispatchTouchEvent(MotionEvent event)

Android系统中,所有的触摸事件都是通过该方法来分发的。

自定义视图时,可以复写该方法实现自己的事件分发逻辑。
这个方法一般根据当前视图的具体实现,决定是直接消费事件,
还是将事件继续分发给子视图处理。
若该方法返回true,则表示事件被当前视图消费掉,不再继续分发;
若该方法返回super.dispatchTouchEvent,则表示继续分发事件。

拦截:
事件的拦截对应着onInterceptTouchEvent方法,原型如下:

public boolean onInterceptTouchEvent(MotionEvent ev)

该方法仅在ViewGroup及其子类中存在。

自定义视图时,可以复写该方法实现自己的事件拦截逻辑。
当该方法返回true时,表示当前视图拦截事件,不再将事件分发给子视图,
同时会将事件交由当前视图消费;
当该方法返回false或super.onInterceptTouchEvent时,
表示当前视图不对事件进行拦截,将事件分发给子视图。

消费:
事件的消费对应着onTouchEvent,方法原型如下:

public boolean onTouchEvent(MotionEvent event)

自定义视图时,可以复写该方法实现自己的事件消费逻辑。
当该方法返回true时,表示当前视图可以处理对应的事件,事件不会在向上传递给父视图;
当该方法返回false时,表示当前视图不处理这个事件,事件会被递交给父视图的onTouchEvent处理。

二、事件传递机制
前文已经提到过了,在Android系统中,拥有事件传递、处理能力的类有以下三种:
Activity: 拥有dispatchTouchEvent和onTouchEvent两个方法;
ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法;
View:拥有dispatchTouchEvent和onTouchEvent两个方法。

接下来我们通过例子,主要分析下View和ViewGroup的事件传递机制。
Activity传递事件时表现得与View比较相似,就不专门分析了。

2.1 View的事件传递机制
首先我们结合例子,看看View的事件传递机制。
考虑到ViewGroup本身就是View的子类,此处的View指的是除去ViewGroup外的控件。
即本身已经是最小的单位,不能再作为其它View容器的View控件。

为了比较直观的看清出事件传递的过程,我们自定义一个继承TextView的类,
并复写分发和消费对应的函数,并增加一些打印日志:

/**
 * @author zhangjian
 */

public class MyTextView extends AppCompatTextView {
    private static final String TAG = "MyTextView";

    public MyTextView(Context context) {
        super(context);
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //主要关注事件分发的流程, 就简单以ACTION_DOWN事件为例了
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }

        return super.onTouchEvent(ev);
    }
}

同时,我们定义一个MainActivity用来呈现MyTextView,并监听MyTextView的点击和触摸事件:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity
        implements View.OnClickListener, View.OnTouchListener {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.my_text_view);
        textView.setOnClickListener(this);
        textView.setOnTouchListener(this);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.my_text_view:
                Log.v(TAG, "MyTextView click");
                break;
            default:
                break;
        }
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (view.getId()) {
            case R.id.my_text_view:
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.v(TAG, "TextView onTouch ACTION_DOWN");
                        break;
                    default:
                        break;
                }

                break;
            default:
                break;
        }
        return false;
    }
}

运行上面的代码并点击MyTextView后,将打印如下log:

12-04 14:45:01.279 29626-29626/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-04 14:45:01.409 29626-29626/work.test V/MainActivity: MyTextView click

从上面的代码和日志可以看出,dispatchTouchEvent和onTouchEvent这两个函数的返回值可能存在以下三种情况:
返回true、返回false和返回父类的同名方法。

不同的返回值将导致事件传递流程相差甚远,通过不断修改这些方法的返回值,

我们大概可以得到类似如下的事件处理流程图:

android view 点击传递 覆盖 android点击事件传递机制_ide

从上面的流程图可以得出如下结论:
1、触摸事件的传递流程是从dispatchTouchEvent开始的,如果不进行人为干预(即返回父类的同名函数),
那么事件将按照视图的嵌套层次,从外层视图逐步向内层传递,到达最内层的View时,就由它的onTouchEvent处理。
该方法如果能够消费该事件,则返回true;如果处理不了,则返回false。
这时事件会重新向外层传递,并由外层View的onTouchEvent方法处理,并依此类推。
2、如果事件在向内层传递的过程中,若某个视图的dispatchTouchEvent返回true,
则会导致事件提前被消费掉,内层View将不会收到这个事件;
若某个视图的dispatchTouchEvent返回false,事件也不会继续分发,
而被交给其父视图的onTouchEvent处理。
3、对于View控件而言,消费事件的逻辑顺序从前到后依次为:
onTouch、onTouchEvent、onClick。
若优先级靠前的接口返回true,则表示事件已经被消费掉了,
不会再调用后续接口。

2.2 ViewGroup的事件传递机制
在Android中,ViewGroup主要作为View控件的容器。
前文已经提到过,与View控件相比,ViewGroup多了onInterceptTouchEvent函数。

在这一部分,我们同样自定义一个ViewGroup并增加打印信息,看看触摸事件传递的流程:

/**
 * @author zhangjian
 */

public class MyRelativeLayout extends RelativeLayout {
    private static final String TAG = "MyRelativeLayout";

    public MyRelativeLayout(Context context) {
        super(context);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onInterceptTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
}

同时,更新对应的layout文件:

<?xml version="1.0" encoding="utf-8"?>
<work.test.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="work.test.MainActivity">

    <work.test.MyTextView
        android:id="@+id/my_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/test" />

</work.test.MyRelativeLayout>

MainActivity和MyTextView的事件传递函数均返回super方法的情况下,
点击MyTextView的打印log如下:

12-05 10:26:17.584 12021-12021/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-05 10:26:17.754 12021-12021/work.test V/MainActivity: MyTextView click

从log来看,事件分发的流程几乎没变,
依然从外层视图逐步向内层传递,然后调用内层视图的onTouchEvent处理。
不过,ViewGroup视图调用dispatchTouchEvent后,
会先调用onInterceptTouchEvent函数。

与前文类似,我们可以修改这些函数的返回值,得到下面的流程图:

android view 点击传递 覆盖 android点击事件传递机制_ide_02

从上面的流程图可以得出如下结论:
1、不考虑事件拦截时,ViewGroup传递事件的逻辑与View完全一样。
2、 ViewGroup通过onInterceptTouchEvent方法拦截事件时,
返回super方法或false时均表示拦截失败,事件将继续被传递给子视图;
返回true时,表示拦截成功,此时事件交给ViewGroup的onTouchEvent处理。
3、 ViewGroup的onTouchEvent处理事件时,
如果返回false或super方法(此处是RelativeLayout),那么表示事件未被消费掉,
需要继续将事件递交给父视图消费;
如果返回true,则表示ViewGroup消费掉了事件。

三、总结
至此,我们通过demo大致了解了View和ViewGroup的事件传递流程。
后续我们再看看源码中到底是如何实现的。