Android 事件分发机制详解

TouchEvent 事件分发机制算作是 Android 开发中很重要的知识点了,以前一直对这个传递过程有点模糊,现在来仔细研究下这整个过程

一、概念解释

触摸事件对应的是 MotionEvent 类,触摸事件的类型分为如下三种:

  • Action_Down :用户手指的按下操作,标志着一次触摸事件的开始
  • Action_Move:用户手指按压屏幕后,在松开手指之前如果移动距离超出一定的阈值,则发生了Action _ Move 事件
  • Action_Up:用户手指离开屏幕时触发的操作,标志着当前触摸事件的结束

在一次屏幕触摸操作中,Action_Down 和 Action_Up 这两个事件是必需的,Action_Move 事件则视情况而定

通过 MotionEvent 对象可以得到点击事件发生的 x 和 y 坐标。系统提供了两组方法: getX / getYgetRawX / getRawY 。两组方法之间的区别在于:getX / getY 返回的是相对于当前 View 左上角的 x 和 y 坐标,而 getRawX / getRawY 返回的是相对于手机屏幕左上角的 x 和 y 坐标

二、事件传递的三个阶段

一次完整的事件传递包括三个阶段,分别是事件的发布、拦截和消费。发生事件传递的视图可以分为三类:Activity、View 和 ViewGroup

2.1、发布(Dispatch):

事件的发布对应着如下方法:

public boolean dispatchTouchEvent(MotionEvent ev)

在 Android 系统中,所有的触摸事件都是通过这个方法来发布的,如果事件能够传递给当前 View,则此方法一定会被调用。在这个方法中,根据当前视图的具体实现逻辑,来决定是直接消费这个事件还是将事件继续发布给子视图处理。
返回 true 表示事件被当前视图消费掉,不再继续发布事件。返回 false 则依据视图类型会有所不同。返回 super.dispatchTouchEvent(ev) 表示继续发布该事件。如果当前视图是 ViewGroup 及其子类,则会调用 onInterceptTouchEvent(MotionEvent ev) 方法判定是否拦截该事件

2.2、拦截(Intercept):

事件的拦截对应着如下方法:

public boolean onInterceptTouchEvent(MotionEvent ev)

这个方法只在 ViewGroup 及其子类中才有,在 View 和 Activity 中是不存在的。该方法通过返回值来决定是否拦截对应的事件。返回 true 表示拦截这个事件,不继续发布给子视图,同时交由自身的 onTouchEvent(MotionEvent event) 方法进行处理;返回 false 或者 super.onInterceptTouchEvent(ev) 表示不对事件进行拦截,继续传递给子视图。如果当前ViewGroup 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用

2.3、消费(Consume):

事件的消费对应着如下方法:

public boolean onTouchEvent(MotionEvent event)

该方法返回 true 表示当前视图可以处理对应的事件,事件将不会传递给父视图;返回 false 表示当前视图不处理这个事件,事件会被传递给父视图的相同方法进行处理

2.4、联系:

三个方法之间的联系可以以如下伪代码来表示:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if (onInterceptTouchEvent(ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }

对于一个根 ViewGroup 来说,点击事件发生后,首先会传递给它,这时它的 dispatchTouchEvent 方法就会被调用,如果它的 onInterceptTouchEvent 方法返回 true 就表示要拦截当前事件,接着事件就会交由 ViewGroup 的 onTouchEvent 方法进行处理。如果 onInterceptTouchEvent 方法返回 fasle,就表示它不拦截当前事件,事件会继续传递给 ViewGroup 的子元素,再次重复以上步骤

此外,View 的 onTouchEvent 方法默认都会返回 true,即消耗事件,除非 View 是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都为 false,clickable 属性则不一定,例如 Button 的clickable 属性默认为 true,TextView 的clickable 属性默认为 false。View 的 enable 属性不影响 onTouchEvent 方法的默认返回值。即时View是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,则 onTouchEvent 方法就返回 true

我们可以为 View 设置 setOnTouchListener 方法和 setOnClickListener 方法,与 View 内部的 onTouchEvent 方法的优先级进行比较,依次是 setOnTouchListener > onTouchEvent > setOnClickListener
此外,如果为 View 设置了 setOnClickListener 方法和 setOnLongClickListener 方法,则会分别将 View 的 clickable 和 longClickable 设置为 true

三、View 的事件传递流程

首先继承 AppCompatTextView 类并重写其与 TouchEvent 事件发布相关的两个方法,输出相应的触摸事件类型

/**
 * Created by CZY on 2017/6/7.
 */
public class MyTextView extends AppCompatTextView {

    private final String TAG = "MyTextView";

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

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent  ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent  ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent  ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

}

在布局文件中声明使用 MyTextView




    



重写 MainActivity 中与触摸事件相关的两个方法,输出相应的触摸事件类型,并为 MyTextView 设置 TouchEvent 事件监听

public class MainActivity extends AppCompatActivity implements View.OnTouchListener{

    private final String TAG = "Activity";

    private final String TAG_VIEW = "MyTextView";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.myTextView).setOnTouchListener(this);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent  ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent  ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent  ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.myTextView:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG_VIEW, "onTouch  ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG_VIEW, "onTouch  ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG_VIEW, "onTouch  ACTION_UP");
                        break;
                }
                break;
        }
        return false;
    }

}

此时,运行程序后点击 MyTextView 控件,输出的 Log 如下所示:

06-13 12:13:26.384 9522-9522/com.czy.touchevent E/Activity: dispatchTouchEvent    ACTION_DOWN
06-13 12:13:26.384 9522-9522/com.czy.touchevent E/MyTextView: dispatchTouchEvent  ACTION_DOWN
06-13 12:13:26.385 9522-9522/com.czy.touchevent E/MyTextView: onTouch             ACTION_DOWN
06-13 12:13:26.385 9522-9522/com.czy.touchevent E/MyTextView: onTouchEvent        ACTION_DOWN
06-13 12:13:26.385 9522-9522/com.czy.touchevent E/Activity: onTouchEvent          ACTION_DOWN
06-13 12:13:26.480 9522-9522/com.czy.touchevent E/Activity: dispatchTouchEvent    ACTION_UP
06-13 12:13:26.480 9522-9522/com.czy.touchevent E/Activity: onTouchEvent          ACTION_UP

在默认情况下,Activity 与 MyTextView 的各个触摸事件相关方法的调用顺序如上所示。可以看到,MyTextView 的 onTouch 方法比 onTouchEvent 方法更早被调用,说明 onTouch 方法的优先级更高。

dispatchTouchEvent 方法和 onTouchEvent 方法的返回值存在三种情况:

  • 返回 true
  • 返回 false
  • 返回 父类的同名方法

通过不断改变 Activity 与 MyTextView 中各个方法的返回值,可以得到如下所示的TouchEvent事件发布机制流程图:


Android 事件分发机制详解_第1张图片
这里写图片描述

从上面的流程图可以得出以下结论:

  • 触摸事件的传递流程是从 dispatchTouchEvent 方法开始的,如果默认返回父类的同名函数,则事件将会依照嵌套层次从外层向内层传递,到达最内层的 View 时,就由它的 onTouchEvent 方法进行处理。该方法返回 true 则表示消费了该事件;如果处理不了则返回 false,这时事件会重新向外层传递,并交由外层的 View、ViewGroup 或者 Activity 的 onTouchEvent 方法进行处理,依次类推
  • 如果事件传递在向内层传递过程中由于人为干预,事件处理函数返回 true ,则会导致事件被提前消费掉,内层 View 或 ViewGroup 将不会收到这个事件
  • View 控件的事件触发顺序是先执行 onTouch 方法,再执行 onTouchEvent 方法,onClick 方法排在最后。如果优先级高的方法返回了 true,则事件将不会继续传递

四、ViewGroup 的事件传递流程

ViewGroup 相比 View和Activity多出了一个 onInterceptTouchEvent(MotionEvent ev) 方法
首先继承 LinearLayout 类并重写其与 TouchEvent 事件发布相关的三个方法,输出相应的触摸事件类型

/**
 * Created by CZY on 2017/6/7.
 */
public class MyLinearLayout extends LinearLayout {

    private final String TAG = "外层ViewGroup";

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

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent  ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent  ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent  ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

}

Activity 的布局文件代码如下所示:




    


运行程序后点击 MyTextView 控件,输出的Log如下所示:

06-18 04:22:54.669 12309-12309/com.czy.touchevent E/Activity: dispatchTouchEvent  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/外层ViewGroup: dispatchTouchEvent  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/外层ViewGroup: onInterceptTouchEvent   ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/MyTextView: dispatchTouchEvent  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/MyTextView: onTouch  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/MyTextView: onTouchEvent  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/外层ViewGroup: onTouchEvent  ACTION_DOWN
06-18 04:22:54.669 12309-12309/com.czy.touchevent E/Activity: onTouchEvent  ACTION_DOWN
06-18 04:22:54.764 12309-12309/com.czy.touchevent E/Activity: dispatchTouchEvent  ACTION_UP
06-18 04:22:54.764 12309-12309/com.czy.touchevent E/Activity: onTouchEvent  ACTION_UP

在默认情况下,Activity 、ViewGroup 和 View 的各个触摸事件相关方法的调用顺序如上所示
通过不断改变 Activity、ViewGroup 和 View 中各个方法的返回值,可以得到如下所示的 TouchEvent 事件发布机制流程图:


Android 事件分发机制详解_第2张图片
这里写图片描述

从上面的流程图可以得出以下结论:

  • ViewGroup 通过 onInterceptTouchEvent 方法对事件进行拦截。如果该方法返回 true,则事件不会继续传递给子View;如果返回 false 或 super.onInterceptTouchEvent ,则事件会继续传递给子 View
  • 在子 View 中对事件进行消费后,ViewGroup 将接收不到任何事件

五、事件传递的“记忆”功能

从上边展示的事件传递默认的方法调用顺序可以看出来,Action_Up 事件都是直接交由 Activity 进行处理,而没有传递给内部的 ViewGroup 或 View
其实,dispatchTouchEvent 方法具有“记忆”功能。如果 Action_Down 事件传递给了某 ViewGroup(或者Activity),ViewGroup 默认继续向下传递交由子View进行处理,ViewGroup 会记录该事件是否被子View给消费了。那 ViewGroup 如何知道子 View 是否消费了该事件呢?如果该事件会再次向上传递给 ViewGroup 的 onTouchEvent 方法进行处理,那就说明子 View 没能消费掉该事件。当第二次事件(Action_Move或者Action_Up)向下传递到该 ViewGroup, ViewGroup 的 dispatchTouchEvent 方法会进行判断,若子 View 消费了上次的 Action_Down 事件,那么本次事件就继续向下传递交由子 View 进行处理,若上次的事件没有被子 View 所消费,那么本次的事件就不会继续向下传递了,ViewGroup 直接调用自己的 onTouchEvent 方法来处理该事件
“记忆”的有效期只在单次的触摸事件中,即从Action_Down 事件开始,在 Action_Up 事件结束

更多的学习笔记看这里:Java_Android_Learn

你可能感兴趣的:(Android 事件分发机制详解)