一、点击事件介绍
在Android 的点击事件中我们涉及到ViewGroup类的三个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
其中view与activity只有dispatchTouchEvent(),onTouchEvent()这两个方法,而onInterceptTouchEvent()则是viewGroup独有的方法,我们平常说的点击事件传递也主要复写这三个方法,先来了解一下这几个方法:
(1)dispatchTouchEvent
(2)onInterceptTouchEvent
(3)onTouchEvent
另外,如果我们的view实现了View.OnTouchListener接口,并重写了onTouch方法,那么这个方法会比onTouchEvent方法优先调用,并且如果onTouch方法返回true的话,onTouchEvent将不会被调用
二、案例讲解
public class CustomRel extends RelativeLayout{
private static final String TAG = "CustomRel";
public CustomRel(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
}
其布局如下,CustomRel嵌套着CustomRel2
我们就一个一个的来验证,注意:以下均用控制变量法来验证,即只修改一处,其他均为默认值
(1)dispatchTouchEvent
09-19 16:20:14.915 2255-2255/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:20:14.915 2255-2255/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 16:20:14.916 2255-2255/com.ujs.tounchtest I/CustomRel2: onTouchEvent:
09-19 16:20:14.917 2255-2255/com.ujs.tounchtest I/CustomRel: onTouchEvent:
我们可以看到,事件传递到CustomRel2 的onTouchEvent方法,由于我们在这个方法里面什么也不做,事件又重新交给一层的ViewGroup的onTouchEvent方法处理,这就是传说中的U型图
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 16:27:23.613 15488-15488/com.ujs.tounchtest I/CustomRel: onTouchEvent:
可以看到,事件传递到CustomRel2 的dispatchTouchEvent方法时候,由于返回了false,CustomRel2 的其他两个方法均不会调用,而是直接将事件返回给上一层ViewGroup
onTouchEvent方法进行处理
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:32:38.974 24932-24932/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 16:32:38.995 24932-24932/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:32:38.996 24932-24932/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:32:38.996 24932-24932/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
发现事件到了CustomRel2 的dispatchTouchEvent方法后就被消耗掉了,既不调用之后的两个方法也不会返回给上一层ViewGroup进行处理
(2)onInterceptTouchEvent
通过上面的讲解,我们知道onInterceptTouchEvent是用来对事件的拦截的,那么我们就来修改CustomRel的onInterceptTouchEvent方法,反回默认值与最刚开始的打印结果一致,直接修改返回false来看看:09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel2: onTouchEvent:
09-19 16:43:01.119 13004-13004/com.ujs.tounchtest I/CustomRel: onTouchEvent:
结果与直接返回默认值一致,呈现U型传递
09-19 16:44:47.341 15717-15717/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:44:47.342 15717-15717/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:44:47.342 15717-15717/com.ujs.tounchtest I/CustomRel: onTouchEvent:
我们发现,事件直接交给了CustomRel的onTouchEvent方法处理,没有向下传递,即事件被拦截掉了
(3)onTouchEvent
先重写两个类的onTouchEvent方法,获取点击事件的属性: @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent: down ");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouchEvent: up ");
break;
}
return super.onTouchEvent(event);
}
这里也有一个返回值,先返回默认的
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down
09-19 16:59:22.903 27001-27001/com.ujs.tounchtest I/CustomRel: onTouchEvent: down
我们将CustomRel2 的onTouchEvent方法返回true看看
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 17:04:19.887 5766-5766/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 17:04:19.981 5766-5766/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 17:04:19.982 5766-5766/com.ujs.tounchtest I/CustomRel2: onTouchEvent: up
我们可以看到CustomRel的onTouchEvent方法并没有获得事件,CustomRel2直接将事件消耗掉了
而返回false与返回默认值一样,均会将事件回传给上一层viewGroup的onTouchEvent方法
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 17:08:46.785 10070-10070/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 17:08:46.786 10070-10070/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down
09-19 17:08:46.786 10070-10070/com.ujs.tounchtest I/CustomRel: onTouchEvent: down
以上是针对子ViewGroup来说的,那如果父ViewGroup的onTouchEvent返回true呢?会把事件消耗掉吗?将CustomRel的onTouchEvent方法返回值修改为true,我们发现第一次事件传给子层后,CustomRel就直接把事件给消耗掉了,也就是说,当onTouchEvent反回true的时候,之后的事件默认就交给该ViewGroup的来处理,不再将事件向下传递
09-19 17:19:04.110 20055-20055/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel: onInterceptTouchEvent:
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: dispatchTouchEvent:
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: onInterceptTouchEvent:
09-19 17:19:04.111 20055-20055/com.ujs.tounchtest I/CustomRel2: onTouchEvent: down
09-19 17:19:04.112 20055-20055/com.ujs.tounchtest I/CustomRel: onTouchEvent: down
09-19 17:19:04.160 20055-20055/com.ujs.tounchtest I/CustomRel: dispatchTouchEvent:
09-19 17:19:04.160 20055-20055/com.ujs.tounchtest I/CustomRel: onTouchEvent: move
以上建议大家自行跑一遍就能体会了
三、源码分析
1、按键的点击事件2、屏幕的触摸事件
按键的事件主要由Wms来完成分发的 ,我们在实际中一般只考虑屏幕的触摸事件,在处理view的事件时有以下几点需要注意的:
1、屏幕触摸事件是直接分发给应用程序2、子视图优先于父视图处理事件,即只有子视图消耗该事件了,父视图才有机会处理3、在处理触摸事件时,需要根据触摸坐标来计算应该将事件分发给哪一个view/ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
这里先获取mWindow的callbak对象,如果没有就直接调用 基类的dispatchToucnEvent()方法
在ViewGroup中采用递归的方式分发事件:
1、触摸事件首先会把消息派发给view树中的最后一个子视图。如果该View没有消耗该事件,才会递归派发给父视图
在处理Down 消息的时候,其作用是判断点击事件是落在该ViewGroup的哪一个视图中
再来看看ViewGroup的dispatchTouchEvent方法
(1)当MotionEvent的消息为Down的时候 ,先判断自身是否被禁止获取Touch消息,如果没有禁止,并且回调函数 onInterceptTouchEvent()中没有消耗该消息,就可以接着往下传递给子视图
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
然后开始根据坐标寻找子视图,主要作用是将布局坐标转化为视图坐标,获取到子view,在判断该子view 是否是ViewGroup,如果是就接着调用dispatchTouchEvent方法,否则递归结束
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...............
}
}
(2)当MotionEvent消息为UP或者cancel的时候,就清除 mGroupFlags 的FLAG_DISALLOW_INTERCEPT标志,即允许该ViewGroup截获消息。比如当用户释放手指,再次点击的时候,该ViewGroup可以再次截获消息,而当按下还没有释放期间,是不允许截获消息的
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
2、在上面viewGroup 把消息传递到了view,先看view的dispatchTouchEvent这个方法,里面有下面几行代码
public boolean dispatchTouchEvent(MotionEvent event) {
...............
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
.........................
}
可以看到,当onFilterTouchEventForSecurity方法返回true的时候才会调用OnTouch 跟onTouchAEvent 方法,onFilterTouchEventForSecurity代码如下
/**
* Filter the touch event to apply security policies.
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
通过注释我们可以很清楚的看到,如果onFilterTouchEventForSecurity方法返回true,说明该消息应该分发到当前的view,否则不分发。我们注意到这一行注释
// Window is obscured, drop this touch.
如果当前Window 为模糊状态,那么他内部的所有view都应该为模糊效果,此时消息就不能分发到当前的view
回到dispatchTouchEvent方法中,判断当前mOnTouchListener是否为空,不为空就调用onTouch方法(注:OnTouch方法是View.OnTouchListener的抽象方法),如果onTouch返回true,那么就直接跳过onTouchEvent方法,否则调用onTouchEvent方法进行处理。这也就是为什么onTouch方法会优先于onTouchEvent方法的原因