一 概述
触摸事件的分发机制是安卓开发中的基础知识,但这块知识又有点绕,总是让人觉得似懂非懂。其实安卓事件传递就是把用户触摸屏幕时的touch事件封装成MotionEvent对象在Activity、ViewGroup和View中传递并处理该touch事件的过程。
二 触摸事件分发的方法
现在我们知道触摸事件是在Activity、ViewGroup和View中进行传递的,对应的方法如下:
- Activity
Activity不对触摸事件进行拦截,收到触摸事件后直接分发给ViewGroup,如果所有的view最后都没有处理该触摸事件,会调用Activity的onTouchEvent方法进行处理,因此Activity处理触摸事件的方法为:
dispatchTouchEvent
onTouchEvent
- ViewGroup
当ViewGroup收到触摸事件后,它可以分发给自己的子View但在分发之前可以判断是否需要拦截该触摸事件,也可以调用自己的onTouchEvent方法处理触摸事件,因此ViewGroup处理触摸事件的方法有三个:
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
- View
View和Activity一样可以接收和处理触摸事件但不能拦截触摸事件,毕竟View下面也没有子View存在了,拦截没有意义。故View中的方法为:
dispatchTouchEvent
onTouchEvent
三 触摸事件的传递机制
- 在Activity中
当用户点击屏幕时Activity最先收到触摸事件此时会调用Activity的dispatchTouchEvent方法,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到Activity调用getWindow().superDispatchTouchEvent(ev)方法继续把触摸事件传递给包含的View,如果有View处理了该事件则返回true,事件传递结束;如果没有View处理该事件则调用Activity的onTouchEvent(ev)方法处理该事件,无论在Activity的onTouchEvent(ev)方法中是否消费该事件,该事件的传递都结束了。
- 在ViewGroup中
当在Activity中调用getWindow().superDispatchTouchEvent(ev)方法时,Touch事件会被传递给Activity包含的最外层ViewGroup,然后层层向下传递。我们分析ViewGroup是如何处理Touch事件的。
当Touch事件传递到ViewGroup会先调用ViewGroup的dispatchTouchEvent方法:
/**
* 源码分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 仅贴出关键代码
// ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
// 若是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即把ViewGroup的点击事件拦截掉
}
}
}
}
}
}
}
/**
* 作用:是否拦截事件
* 说明:
* a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
* b. 返回false = 不拦截(默认)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
从代码中可以看出在ViewGroup的dispatchTouchEvent方法中先判断该ViewGroup是否拦截该Touch事件,如果拦截了,Touch事件就不会往下传递而是直接调用ViewGroup的onTouchEvent方法处理事件;如果没有拦截则遍历所有子View找到正在点击的那个View并把Touch事件传递给它。
3.在View中
View收到Touch事件后会先调用View的dispatchTouchEvent方法,在dispatchTouchEvent方法中调用该View的onTouchEvent方法去处理该Touch事件,如果该View的onTouchEvent方法返回true则表示该View消费了该事件,否则会继续调用其父View的onTouchEvent方法去处理该事件,直到某个View的onTouchEvent方法消费了该事件,或者传递到Activity的onTouchEvent方法,则事件传递结束。
在View的onTouchEvent方法中如果接收并消费了ACTION_DOWN事件,则该View会接收到后续的ACTION_MOVE、ACTION_UP等事件;反之,如果该View没有消费ACTION_DOWN事件则后续的事件不会再传递给该View。
四 注意事项
- ViewGroup的onInterceptTouchEvent方法默认返回false,ViewGroup进行事件分发都会调用该方法,但是一旦onInterceptTouchEvent方法返回true则表示该ViewGroup拦截了触摸事件,后续进行事件分发不再调用onInterceptTouchEvent方法。
举个栗子:我们在onInterceptTouchEvent方法中判断是ACTION_MOVE事件就返回true,在该ViewGroup的子View可以收到ACTION_DOWN事件,如该子View消费了ACTION_DOWN事件,则在第一个ACTION_MOVE事件到来时,ViewGroup会拦截该事件,但是并不会调用ViewGroup的onTouchEvent方法,同时把ACTION_CANCEL事件传递给子View。后续的事件都不会传递给子View了,而是直接调用ViewGroup的onTouchEvent方法去处理。 - View的onTouch方法会先于onTouchEvent方法执行,如下所示(onClick在onTouchEvent方法中执行):
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("onTouch","touch:button1");
//返回true不执行onClick方法
//返回false接着执行onClick方法
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("button1","点击了:button1");
}
});
- View的onClick方法是在ACTION_UP事件之后执行的,并不是在ACTION_DOWN事件到来时就执行。因此如果在父View中拦截了ACTION_MOVE或者ACTION_UP事件,是不会执行该方法的。
- 可以调用
getParent().requestDisallowInterceptTouchEvent(true)
方法请求父View不要拦截Touch事件。注意这个方法不能在子View初始化时调用(无效),最好在子View接收到Touch事件也就是在子View的dispatchTouchEvent方法中调用。调用完该方法后,父View以及父View的父View就不会再调用onInterceptTouchEvent方法去判断是否拦截了。
好了,触摸事件的传递机制就讲到这里啦,有不对的地方欢迎留言指正。