Log中分析View的事件分发

在实际项目的开发中,View的事件分发机制起到了很重要的作用,对于经常打交道的滑动冲突,了解了View事件分发的原理,也就可以很容易的解决。实践出真知,我们从一个Demo出发,通过Log的形式分析View是如何将事件分发的,又是如何拦截并作出相应处理的。

一、创建Demo

首先,创建一个简单的Demo,自定义最外层的ViewGroup,命名为CustomViewGroup1,重写onInterceptTouchEvent,onTouchEvent,dispatchTouchEvent三个方法,并将背景设为红色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CustomViewGroup1 extends FrameLayout {
private static final String TAG = "CustomeViewGroup1";

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

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

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent:" );
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
return super.onTouchEvent(event);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent:");
return super.dispatchTouchEvent(ev);
}
}

和上述代码一样,自定义中间层的CustomViewGroup2,重写拦截和处理的方法,将背景设为蓝色。
接着自定义View,重写onTouchEvent和dispatchTouchEvent方法,将背景设置为黄色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomView extends Button {
private static final String TAG = "CustomView";

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

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

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
return super.onTouchEvent(event);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent:");
return super.dispatchTouchEvent(event);
}
}

最后运行的效果为

红色为最外层CustomViewGroup1,蓝色为中间层CustomViewGroup2,黄色为CustomView。

二、分析

2.1

从上面自定义ViewGroup的代码可以看出,View的事件分发涉及到了几个重要的方法,磨刀不误砍柴工,下面就先来分析一下这几个方法。

  • boolean dispatchTouchEvent(MotionEvent event):事件分发
  • boolean onInterceptTouchEvent(MotionEvent ev):事件拦截
  • boolean onTouchEvent(MotionEvent event):事件处理

dispatchTouchEvent为事件分发的第一步,将事件向下分发;onInterceptTouchEvent表示是否拦截当前事件,当返回true时,表示拦截,也就是下面的事件就不会触发;onTouchEvent表示处理事件,返回结果表示是否消耗当前事件。这里不清楚没关系,接着看下面的分析。

上述三种方法默认返回false,不拦截也不消耗事件

2.2

接下来就通过Log一步一步的看事件分发的过程。当点击黄色界面,也就是最底层的View时,Log如下:

从上面的Log可以看出,

在默认的情况下,最先触发CustomeViewGroup1(红色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,接着触发CustomViewGroup2(蓝色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,最后才触发CustomView(黄色界面)的dispatchTouchEvent和onTouchEvent,一级一级向下传递。

上述为默认情况,不拦截也不消耗任何事件,事件就会一层一层的向下传递,那么当我们在第一层CustomeViewGroup1的onInterceptTouchEvent中返回true,将点击事件拦截下来,会出现什么情况呢?

1
2
3
4
5
6
7
8
public class CustomViewGroup1 extends FrameLayout {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent:" );
return true;
}
}

同样点击黄色区域,再看Log:

这时我们发现,Log上只走了CustomeViewGroup1的方法,并且走了onTouchEvent,这就说明CustomeViewGroup1将事件拦截了下来,并没有分发下去,自己就将事件进行了处理。那么我们再试着将CustomeViewGroup2的onInterceptTouchEvent返回true,依据上面的分析我们猜测到CustomeViewGroup2就停止传递,接着会触发onTouchEvent。同样我们修改代码并点击VIew黄色区域。

1
2
3
4
5
6
7
8
public class CustomViewGroup2 extends FrameLayout {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return true;
}
}

从上述Log可以看出我们的猜测是正确的,在CustomeViewGroup2进行拦截,事件在分发到CustomeViewGroup2就没有继续向下分发了。不过我们又发现Log的最后两行,先触发CustomViewGroup2的onTouchEvent接着触发CustomViewGroup1的 onTouchEvent,这是属于onTouchEvent返回false,CustomViewGroup2没有消耗掉当前事件,导致将事件返回给了他的上一层处理。

2.3、小结

从上面的Log分析我们可以得出三种不同情况下的事件分发规则:

1、在正常情况下,三个方法的返回值都为false,不拦截也不消耗事件,那么事件就会从ViewGroup1传递到ViewGroup2再传递到最后的View。

2、当ViewGroup1的onInterceptTouchEvent方法返回true,那么ViewGroup1就拦截下了这个事件,后面的ViewGroup2和View就接受不到任何事件的信号,并调用ViewGroup1的onTouchEvent方法对事件进行处理。

3、当ViewGroup2的onInterceptTouchEvent方法返回true,那么ViewGroup2拦截下当前事件,并调用自己的onTouchEvent方法,若ViewGroup2的onTouchEvent返回false,表示不消耗这个事件,那么就会返回他的上一层,将这个事件交给上一层处理,也就是ViewGroup1的onTouchEvent方法会被调用。相反,ViewGroup2的onTouchEvent返回true,就不会返回给上一层。

简单的打个比方,ViewGroup1代表校长,ViewGroup2代表老师,View则代表学生

1、正常情况下是校长将一个任务首先下发给老师,接着老师下发给学生,学生收到任务,并执行该任务。

2、第二种情况,校长发现该任务自己可以处理就不用下发给老师,及校长的onInterceptTouchEvent返回true,将这个任务拦截下来了。后面的老师和学生也就不知道该任务是什么。

3、第三种情况,校长将任务下发给老师,老师却将任务拦截了下来,并没有告诉学生,即ViewGroup2的onInterceptTouchEvent方法返回true。当老师准备自己处理的时候发现这个任务太难,自己解决不了,就onTouchEvent方法返回false,将事情又返回给更高一级的校长处理。

三、onTouchEvent

接下来分析onTouchEvent中是如何处理动作的,我们在onTouchEvent的方法中加上三种最常见的动作的Case,ACTION_DOWN手指点击,ACTION_MOVE手指滑动,ACTION_UP手指抬起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP");
break;
}
return super.onTouchEvent(event);
}

点击画面并滑动,

我们可以看到日志Log上只有ACTION_DOWN事件发生,并没有触发滑动MOVE和手指抬起的UP事件,这是因为ACTION_DOWN事件默认是返回false,表示不消耗DOWN事件,也就是说DOWN动作发生后是不会向下传递,也不会触发MOVE和UP的动作。所以我们需要触发后面的动作时,需要将返回值设为 true,表示消耗当前事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent:");
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent: ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

这时我们同样去点击和滑动画面,

可以从上图看到,MOVE和UP动作都相应触发。

四、最后

至此,View的事件分发基本上从头捋了一遍,更多的源码的细节准备好好研究一下,在后面的文章中展示出来。其实将事件分发的文章网络上比比皆是,本没有想法去记录这篇文章,只是因为前几天在做Android测试题的时候,有涉及到事件分发,当时将onInterceptTouchEvent和onTouchEvent的返回值弄反。原本以为自己记得很清楚,到了用的时候却弄混,才发现有些东西需要自己去好好的写上一遍,才会真正明白,也记得更牢。古人云:好记性不如烂笔头,不是没有道理的。