本文为事件分发的学习总结。
《Android开发艺术探索》一书中对事件分发做了很详细的介绍。
大神博客:http://blog.csdn.net/singwhatiwanna
View的事件分发机制
- 事件的传递机制,指的就是事件的分发,也就是对MotionEvent事件的分发过程。
MotionEvent类
- MotionEvent:手指接触屏幕后产生的事件,封装成了MotionEvent类
- 典型的事件类型(MotionEvent类中的int型常量):
- ACTION_DOWN:手指刚接触屏幕 (按下) 值为:0
- ACTION_UP:手指在屏幕上松开的一瞬间 (抬起) 值为:1
- ACTION_MOVE:手指在屏幕上移动 值为:2
- 事件序列:手指接触屏幕后产生的一系列事件,就称为事件序列
- 点击屏幕后离开松手,事件序列:DOWN–>UP
- 点击屏幕滑动一会再松手,事件序列:DOWN–>MOVE–>MOVE–>…–>MOVE–>UP
- 事件分发:当MotionEvent产生之后,系统要把这个事件传递给一个具体的view,这个传递的过程就是分发的过程
事件分发的主要方法
- 事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
dispatchTouchEvent
- 用来进行事件的分发。
- 如果事件能够被传递给当前View,那么此方法一定会被调用,返回结果受到当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件。
onInterceptTouchEvent
- 会在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件(返回true),那么在同一个事件序列当中,此方法不会再次被调用,返回结果表示是否拦截当前事件。
onTouchEvent
- 在dispatchTouchEvent方法中调用,用来处理事件,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中,当前View无法再次接收到后续的事件队列
- onTouchEvent默认的返回值由clickable和longClickable共同决定,只要有一个为true,onTouchEvent的返回值就是true,longClickable的默认值都为false,clickable的默认值分情况,Button为true,TextView为false。
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的关系
//有一个LinearLayout对象:demoLayout
//当demoLayout被点击时,先执行demoLayout的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume(是否消耗) = false;(不消耗)
boolean intercept(是否拦截此事件) = onInterceptTouchEvent(event)
//(该方法也是demoLayout的方法,并且该方法会返回一个boolean的值来判断是否要拦截此次事件)
if(intercept){
//如果拦截此事件,那么执行demoLayout自己的onTouchEvent方法
consume(是否消耗) = onTouchEvent(event);
//(调用的也是demoLayout的onTouchEvent方法,并且该方法会返回一个boolean的值来判断是否消耗当前事件)
}else{
//如果不拦截此事件,那么事件会传递到demoLayout的子视图的dispatchTouchEvent方法中
consume(是否消耗) = childView.dispatchTouchEvent(event);
//(这时候的dispatchTouchEvent方法就是childView的方法了,不再是demoLayout自己的了)
//而childView调用了dispatchTouchEvent后,又会走一遍childView自己的流程
//当所有都走完(一层一层下去,一层一层返回来后),下面的return consume才会执行
//如果所有的子View都没有消耗该事件,那么会执行demoLayout的onTouchEvent方法
//若onTouchEvent返回false,则后续的事件(比如抬起)都不会被响应
}
return consume;
事件的传递过程
- 对应外层的根ViewGroup,举例为demoLayout。
- 事件产生后,因为demoLayout在外层,所以demoLayout先接受到这个事件对象event,并且将这个event对象传递给它(demoLayout)本身的dispatchTouchEvent方法。在dispatchTouchEvent中会调用onInterceptTouchEvent方法,
- 如果onInterceptTouchEvent返回true,那么说明这次触摸事件,demoLayout本身要响应,也就是拦截该事件,不再向下(子视图)传递。
- 如果onInterceptTouchEvent返回false,那么说明这次触摸事件,demoLayout不响应,不拦截,会将该事件event传递到子视图中(向下传递),接着子视图的dispatchTouchEvent就会被调用,直到事件被最终处理,整个分发过程才结束。
案例演示:自定义RelativeLayout
- 建立类EventRelativeLayout类,继承RelativeLayout
- 复写前两个构造方法
- 建立一个String类型的TAG,用来标识log日志
- 复写方法dispatchTouchEvent(),在该方法中输出log
- 复写方法onInterceptTouchEvent(),在该方法中输出log
- 复写方法onTouchEvent(),在该方法中输出log
- ev.getAction输出的是事件的类型
public class EventRelativeLayout extends RelativeLayout {
private static final String TAG = "EventRelativeLayout";
public EventRelativeLayout(Context context) {
(context);
}
public EventRelativeLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent:
---父容器的分发事件"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "onInterceptTouchEvent:
---父容器的拦截事件"+ev.getAction() );
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent:
---父容器的触摸事件发生"+event.getAction());
return super.onTouchEvent(event);
}
}
案例演示:自定义Button
- 建立类EventButton类,继承Button
- 复写前两个构造方法
- 建立一个String类型的TAG,用来标识log日志
- 复写方法dispatchTouchEvent(),在该方法中输出log
- 复写方法onTouchEvent(),在该方法中输出log
- ev.getAction输出的是事件的类型
public class EventButton extends Button {
private static final String TAG = "EventRelativeLayout";
public EventRelativeLayout(Context context) {
(context);
}
public EventRelativeLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent: ---"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: ---"+event.getAction());
return super.onTouchEvent(event);
}
}
案例演示:在xml文件中使用
- 在layout布局文件中建立一个EventRelativeLayout
- 在该EventRelativeLayout中建立一个EventButton
- 运行程序,点击EventButton,观察日志(注意:没做任何绑定组件监听事件的东西,只是单纯的在xml中使用)
案例演示:log日志
- 按log出现顺序分析
- EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件0
- 先执行了父容器的分发事件方法,获得的事件为0,也就是按下操作
- EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件0
- 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
- EventButton: dispatchTouchEvent: —分发事件0
- 然后执行button的分发方法
EventButton: onTouchEvent: —触摸事件发生0
- 然后执行button的onTouchEvent方法
EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件1
- 先执行了父容器的分发事件方法,获得的事件为1,也就是抬起操作
- EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件1
- 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
- EventButton: dispatchTouchEvent: —分发事件1
- 然后执行button的分发方法
- EventButton: onTouchEvent: —触摸事件发生1
- 然后执行button的onTouchEvent方法
案例演示:分析log日志
- 点击button后,出现了两次流程,并且一个事件为0,一个为1
- 可以看出,点击操作会被拆分成两个事件,一个是按下,一个是抬起
案例演示:将EventRelativeLayout的拦截事件修改为返回true
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "onInterceptTouchEvent:
---父容器的拦截事件"+ev.getAction());
return true;
}
- 运行程序,点击Button,观察log日志
- 发现这回的log只有Layout的ACTION_DOWN的log,抬起的log没有出现
- Button的log也没有出现
- 并且多了一个onTouchEvent的log
- 说明EventRelativeLayout将这次事件拦截了,事件并没有向下传递
案例演示:为什么只有按下的log出现了呢?
- 在xml文件中,设置EventRelativeLayout的属性clickable为true
- 运行程序,点击button后观察log
案例演示:更改clickable属性为true后的log日志
- EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件0
- 先执行了父容器的分发事件方法,获得的事件为0,也就是按下操作
- EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件0
- 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
- EventRelativeLayout: onTouchEvent: —父容器的触摸事件发生0
- 执行了父容器的onTouchEvent方法
- EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件1
- 先执行了父容器的分发事件方法,获得的事件为1,也就是抬起操作
- EventRelativeLayout: onTouchEvent: —父容器的触摸事件发生1
- 执行了父容器的onTouchEvent方法
案例演示:分析log日志
- 更改clickable属性为true后,发现抬起事件的日志出现了
- clickable属性:是否能响应点击事件
- 这个属性,一般的View的默认值都为false,Button的默认值为true
- 因为EventRelativeLayout将这次事件拦截了,所以该事件不会向下传递,会执行onTouchEvent
- 但是因为EventRelativeLayout的属性clickable默认为false,EventRelativeLayout不可响应点击事件,所以接收不到后续的事件队列(抬起的动作)
- 而将clickable属性改为true后,EventRelativeLayout可以响应点击事件了,所以可以接收到抬起动作的事件,又由于onInterceptTouchEvent方法返回true,说明该事件队列已经被拦截了,会执行onTouchEvent(按下事件的),只要拦截了某事件,后续的事件队列就不需要再做一次判断了,所以能看到分发事件的日志,但是看不到onInterceptTouchEvent的log日志,就直接onTouchEvent(抬起事件的)了
案例演示总结
当父容器EventRelativeLayout不拦截事件时
* 点击Button,事件会传递到子视图Button中,因为Button的clickable的默认值为true,调用Button的onTouchEvent方法。
* 该方法的默认返回值为true(因为Button的clickable默认为true),那么会消耗掉该事件,并且可以接收后续的事件序列,并且父容器的
onTouchEvent方法不会被执行
* 如果将该方法的返回值改为false,表示不会消耗该事件,会将该事件传递给父容器的onTouchEvent,并且不会再接受其后续事件队列。
* 因为其父容器clickable为true,说明可以接收到(抬起)后续的事件队列。
* 但是不会调用父容器的拦截事件方法(因为子View已经拒绝了一次,后续事件都不需要再传递给子View,自然不需要多做一次是否拦截的判断)
* 如果将Button的clickable设置为false,那么调用Button的onTouchEvent方法时该方法会返回false(则不可以接收后续的事件序列)。
* 会一层层回调父容器的onTouchEvent方法
当父容器EventRelativeLayout拦截事件时
* 点击Button,事件会被父容器EventRelativeLayout拦截,执行父容器的onTouchEvent方法
* 若父容器EventRelativeLayout的clickable属性为false,执行onTouchEvent方法
* 因clickable属性为false,则onTouchEvent也会返回false,无法接收到后续的抬起事件
* 若父容器EventRelativeLayout的clickable属性为true,执行onTouchEvent方法
* 因clickable属性为true,则onTouchEvent也会返回true,可以接收后续的抬起事件
当EventButton的onTouchEvent的返回值被修改时
- 如果对EventButton绑定组件设置了onClickListener
- 那么该onClickListener的onClick方法不会被回调
- 因为回调onClick方法是在父类的onTouchEvent中执行的
- 如果将返回值super.onTouchEvent(event)修改,那么不会调用父类的onTouchEvent
- 也就不会去回调onClick方法
事件分发总结
- 当一个点击事件产生后,它的传递过程遵循如下规则:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,Window在传递给顶级View-DecorView,DecorView接收到事件后,就会按照事件分发机制去分发事件。
- 如果一个View-A的onTouchEvent返回false(能传递到A的onTouchEvent,说明A的onInterceptTouchEvent方法返回true,并且说明A的父View的onInterceptTouchEvent返回flase),那么说明A的dispatchTouchEvent会返回false,那么最后事件就被父view处理,如果所有View都不处理这个事件,那么事件最后会由Activity处理。
事件分发模拟举例
- 技术总监接到了一个任务,(Activity接收到了一个手指点击的事件)
- 总监将任务分配给了副总监,等待副总监回复(Activity将事件传递到了Window)
副总监将任务分配给了Android主管,等待主管回复(Window将事件传递给了DecorView)
- Android主管将任务分配给了项目组长,等待项目组长回复(DecorView将事件传递给了ContentView,也就是Activity绑定的xml文件中的根视图)(R.layout.activity_main中最外层的那个视图,假设叫:eventView)
- 项目组长扫了一圈项目里的员工(遍历eventView中的子视图),找到了带你的那个前辈relativeLayout,因为前辈擅长任务的这个方向,等待前辈回复(点击事件的坐标落在该子元素的区域中)
- 前辈瞅了你一眼,把任务交给了你,等待你回复(事件传递到了relativeLayout中的子view)
- 你一看任务好简单,写了三天搞定了,上级觉得很满意,以后有任务还交给你(子视图响应了事件,消耗了事件,onTouchEvent返回true,可接收后续事件)
- 你一听,让老子做这玩意?不干了,离职了(onTouchEvent返回false,则接收不到后续事件)
- 你以为任务好简单,写了三天啥也没写出来(子视图不相应该事件,不消耗该事件)
- 前辈说你好菜,新手就是新手,看我教你吧(事件传递到了relativeLayout)
- 前辈就是前辈,一天就搞定了(relativeLayout消耗了事件)
- 半天后,前辈说:这个任务看似简单,实则复杂,再拖就交不了工了。(relativeLayout不消耗该事件,onTouchEvent返回false,向上传递)找项目组长去了
- 前辈瞅了你一眼,把任务交给了你,等待你回复(事件传递到了relativeLayout中的子view)
- 项目组长寻思你(前辈)是这个方向最擅长的了,我还是去找主管吧(eventView也不响应)
- 项目组长扫了一圈项目里的员工(遍历eventView中的子视图),找到了带你的那个前辈relativeLayout,因为前辈擅长任务的这个方向,等待前辈回复(点击事件的坐标落在该子元素的区域中)
- 主管都多长时间不敲代码了,直接把任务还给了副总监
- 副总监要管的事情太多,就和技术总监说这个任务开发人员完不成
总监说那拉倒吧
如果技术总监想了想,觉得下面的人应该写不出来(判断是否拦截任务,若拦截),那么后续的任务会再判断是否要拦截,直接执行总监的onTouchEvent
- Android主管将任务分配给了项目组长,等待项目组长回复(DecorView将事件传递给了ContentView,也就是Activity绑定的xml文件中的根视图)(R.layout.activity_main中最外层的那个视图,假设叫:eventView)