怕什么真理无穷 进一寸有进一寸的欢喜
在实际项目的开发中,View的事件分发机制起到了很重要的作用,尤其是遇到不按常理出牌的产品经理的时候,没事给你一个嵌套,没事再给你加个嵌套,那么事件分发就显得尤为重要。对于经常打交道的滑动冲突,了解了View事件分发的原理,也就可以很容易的解决。实践出真知,我们从一个Demo出发,通过Log的形式分析View是如何将事件分发的,又是如何拦截并作出相应处理的。
一、创建Demo
首先,创建一个简单的Demo,自定义最外层的ViewGroup,命名为CustomViewGroup1,重写onInterceptTouchEvent,onTouchEvent,dispatchTouchEvent三个方法,并将背景设为红色。
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方法,将背景设置为黄色。
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,将点击事件拦截下来,会出现什么情况呢?
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黄色区域。
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手指抬起。
@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,表示消耗当前事件。
@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的返回值弄反。原本以为自己记得很清楚,到了用的时候却弄混,才发现有些东西需要自己去好好的写上一遍,才会真正明白,也记得更牢。古人云:好记性不如烂笔头,不是没有道理的。
文章同步个人博客:https://fuusy.github.io
公众号:小猿说,欢迎关注,分享Android知识。