android事件分发
责任链模式
有一公司A,一客户给公司的boss发派了一个任务,然后boss比较忙,旨在锻炼下属,就把任务抛给了下属,即二级boss,然后这个二级boss也与大boss有同样的想法,就再把这个任务抛给了下一级,一层一层,直到传递到最后一层了,一个刚入职的萌新,试用期还没过,一看到这个任务就头皮发麻,告诉自己的上级说自己做不了,没办法,这个上级决定自己做,无奈自己也不行,只能再告诉自己的上级,一层一层,最后任务原原本本的回到了大boss的手上,一边抱怨着现在的年轻人不行了,一边把这个任务自己完成。
之后,当这个客户再有其他什么任务要给boss的时候,如果上一个任务交给了boss底下的某个人C完成,那么大boss就对二级boss说,把这个任务还给C做吧,然后把这个任务又抛给了二级boss,二级boss再这样一层一层,最后到了C的手里,他二话不说,拿起就做,也懒得给下面的人了。
之后,这客户又有新任务了,然后把任务给了大boss,大boss依旧如上一个任务,要把任务一层一层传给C去做,不过当任务到了二级boss手里的时候,二级boss一看这个任务的内容,就生出了一种感觉,一种这个任务必须要由自己完成使命,于是他不顾大boss的吩咐,自己做了这个任务,然后他又告诉C说,以后这个客户的任务不需要C做了,C说哦,说完把所有关于这个客户之前任务的资料都删掉了。
之后,客户又有新任务了,大boss什么话都没说,就把资料扔给了二级boss,二级boss一看客户的名字,按理说这个任务应该是要给C的,但是上次自己把他的任务拦下来了,这次C还会老老实实的做吗?然后二级boss当机立断,自己很快地做完,交给了大boss,也是什么也没说。
故事本应该就这样结束了,可是二级boss把自己的工作拦截下来的事情被C发现了,C就很不爽了,他就立马告诉自己的上级说,“你跟上面说说,下次再有这个客户的任务,还是给我做吧,毕竟我一直在做”,然后一层一层,大boss就知道了。直到下一次此客户再发派任务的时候,大boss拿起笔记本一看,C要做这个,就告诉二级boss,把这个给C做去吧,然后潇洒走开。这下二级boss可不能再任由自己了,全公司上上下下都看着呢,就是自己再怎么隐蔽,也不能瞒得了所有人,还是老老实实按大boss说的做吧,于是C这下终于可以如愿以偿了。
使用了这种工作模式的公司A的生意越做越大了,很显然这样的工作模式能够很大程度上发挥每个人的工作效率,同时大boss也在之后的工作中不断完善自己公司的工作模式,渐臻完美。这就是责任链模式的逐层传递。
在android中
在 Android 的日常开发中,有一个比较大的模块叫做自定义 view ,自定义 view 又包括大致两个方面,自定义 ViewGroup 和自定义的 View,其中 ViewGroup 的自定义主要涉及的就是父 view 与子 view 之间的协调工作,包括对子 view 的位置、大小的控制,以及功能上的分配。而功能的分配,大多数情况下就是对用户输入事件的处理,因为现在的 android 以及其他平台的手机,大都是只有触摸屏而没有其他供用户与手机交互的方式,所以应用就应该能够根据用户的触摸情况,以及自身的逻辑,给予用户好的使用体验,什么时候用户是想与自己交互,什么时候用户是在与自己的子 view 交互,一个好的 viewGroup 都应该清楚这些。就像是上面的例子中,在某一层的人员接到一个任务时,他要知道怎么处理这个任务,给下一级,给自己,还是返回给上一级。
在 android 的 view 自定义中一般使用的关于事件分发的函数有这三个:
| 函数名 | 说明 | 返回结果 |
|---|---|---|
| dispatchTouchEvent | 用来分发一个事件,当有事件传递进来的时候调用,判断是否要继续往下传递或者是自己消费 | true:消费此次事件,并且后续事件到这个函数就会终止传递 false:不消费此次事件,但事件终止传递,后续事件仍继续传递 super:调用后面两个函数的返回值 |
| onInterceptTouchEvent | 在 dispatchTouchEvent 内部调用,判断是否拦截此事件,只在 ViewGroup 中有这个方法 | true:表示拦截事件,调用 onTouchEvent 方法 false:不拦截,事件继续向下传递 |
| onTouchEvent | 在 dispatchTouchEvent 内部调用,处理点击事件 | true:消费事件,后续事件会直接传递到这里,不再判断是否拦截 false:将事件向上传递给 onTouchEvent 进行判读 |
先列出一张图:

看上面这张图,当用户产生一个触摸事件之后,最先会交给当前 activity 的 dispatchTouchEvent 处理,然后在 activity 的 dispatchTouchEvent 中会调用当前布局 window 的 dispatchTouchEvent 方法,即 phoneWindow,
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
而在phoneWindow 中的 dispatchTouchEvent 方法中又继续调用了 DecorView 的 superDispatchTouchEvent 方法。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
又继续调用了父类的 dispatchTouchEvent 方法,DecorView 的父类是一个 FrameLayout,FrameLayout 并没有关于dispatchTouchEvent 的实现,接着找 FrameLayout 的父类 ViewGroup,在 ViewGroup 中有对 dispatchTouchEvent 方法的具体实现,包括各种对于是否拦截此次事件的判断。
另外,在 activity 的 dispatchTouchEvent 方法中,有一个判断,if ( getWindow().superDispatchTouchEvent(ev) ),这里面的返回值就是各级 view 的 dispatchTouchEvent 的返回值,表示是否消费事件,所以这里的逻辑就是,判断是否有 view 消费了这次事件,如果有,直接返回 true,如果没有,就把这个事件交给自己的 onTouchEvent 处理,并返回处理的结果。
dispatchTouchEvent
当有一个新的事件产生的时候,会首先调用一个 view 的 dispatchTouchEvent 对这个事件进行分发,dispatchTouchEvent 的返回值表示是否消费这个事件,在函数内部用 handled 这个值表示。在一个简单的系列事件中,一般包含一个 MotionEvent.ACTION_DOWN,若干个(可为零) MotionEvent.ACTION_MOVE,一个 MotionEvent.ACTION_UP 或 MotionEvent.ACTION_CANCEL。其中 MotionEvent.ACTION_DOWN 被认为是一个系列事件的开始的标志,也决定着后续事件要在何处处理。一般情况下,如果 MotionEvent.ACTION_DOWN 被某个 view A 消费了,那么这个系列事件的后续事件,在传送到 A 的时候就会直接调用 onTouchEvent,而不会再考虑是否要拦截,以及是否分发给子 view 了。所以在遇到一个 MotionEvent.ACTION_DOWN 事件的时候,就意味着一个新的系列事件开始了,所以 view 要首先清除之前的事件分发的时候的残留数据,重新开始新的旅途,大有从头来过的意思。然后就是调用自己的 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;
}
这里只有在 disallowIntercept 为 false 的时候才会调用 onInterceptTouchEvent 进行是否拦截判断,那么这个 disallowIntercept 的意思就是是否允许拦截,那么这个允许拦截是谁允许的?是当前 ViewGroup 的子 view,默认这个值是 false,只有在子 view 里面调用了 parent.requestDisallowInterceptTouchEvent(disallowIntercept)的时候就会把这个值赋为 true,表示子 view 需要这个事件,而自己不能拦截。这里的 parent 可以由 getParent()获得,getParent 返回的是一个 ViewParent 实例,ViewGroup 实现了 ViewParent,可以看一下 ViewGroup 中的 requestDisallowInterceptTouchEvent 方法:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
可以看到当自己的子 view 调用 parent.requestDisallowInterceptTouchEvent() 方法的时候,ViewGroup 就会将自己的 FLAG_DISALLOW_INTERCEPT 置为一个值,使得final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;为 true,同时当前 ViewGroup 还会继续调用自己的父类的此方法,保证这一系列事件能够到达需要这个事件的 view 那里。同时在判断是否拦截的时候除了判断是否是 MotionEvent.ACTION_DOWN 之外,还判断了 mFirstTouchTarget 是否为空。
之后会在继续判断是否被取消了此次事件,即是否本应该传递到这里的一个事件,被上层 view 给拦截了,那么就会在这里响应一个 MotionEvent.ACTION_CANCEL,如:‘
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
如果这个值为 true,表示此次事件被拦截了,那么就不能再继续往下传递了。如果当前事件没有被自己的上层 view 拦截,同时也没有被自己拦截,那么 view 就会把这个事件传递给自己的子 view:
if (!canceled && !intercepted) {
...
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
...
}
...
...
}
在这一个代码块里主要做的事情就是传递事件给子 view,首先对 view 的子 view 遍历,然后对每个子 view 调用了 dispatchTransformTouchEvent 方法,在这个方法里真正调用了每一个子 view 的 dispatchTouchEvent 方法,并获得子 view 的 dispatchTouchEvent 的返回值,如果有子 view 返回 true 即消费了这个事件,当前 view 就不会再继续传递给其他子 view 了,同时记录下这个信息。另外,如果这个事件被拦截了,就会通过 dispatchTransformedTouchEvent 调用到自己的父类的 dispatchTouchEvent,即在 View 中定义的 dispatchTouchEvent,在里面会调用到 view 的 onTouchEvent 方法,对事件进行处理,并返回是否消费。
上面所说的都是 ViewGroup 定义的 dispatchTouchEvent 方法,里面涉及到很大一部分的关于事件分发的逻辑,不过在 View 的 dispatchTouchEvent 里面,就没有这些了。
在一个 View 的 dispatchTouchEvent 方法里面,除了判断这个事件是否需要处理(ViewGroup 里也有这个判断),即产生当前事件的 activity、window 等是否还可用之类的,还使用 handleScrollBarDragging 方法判断这个事件是不是 scrollBar 的拖拽事件,如果是,消费掉,但是并不调用 onTouchEvent 方法,否则继续判断当前 view 是否设置了 onTouchListener,如果有,那么调用设置的 listener 的 onTouch 方法,查看返回值,如果为 true 则消费掉,并且不会再调用 自己的 onTouchEvent 方法,所以由此可以得到一个结论,view的 setOnTouchListener 方法会优先于 view 的 onTouchEvent 方法调用,只有当上述两个都表示自己不消费这个事件之后,才会调用 view 的 onTouchEvent 方法判断是否消费。最后,返回是否消费这个事件。
onInterceptTouchEvent
这个方法只在 ViewGroup 里面有,因为一个纯粹的 view 并没有子 view,所以不需要考虑是否要分发给子 view,只需要直接调用自己的 onTouch 或者什么来判断是否消费即可。这个方法在 ViewGroup 里面默认返回的是 false,表示不拦截此次事件,将事件继续向下传递。这个方法主要是用来自定义一个 ViewGroup 的时候,重写这个方法来判断什么时候需要用户与当前 view 互动,什么时候用户是要与自己的子 view 互动,返回 true 即表示自己要处理这个事件,随之调用自己的 onTouchEvent,否则就会将这个事件传递给子 view,由子 view 进行分发。
OnTouchEvent
在 ViewGroup 里面没有对 onTouchEvent 的重写,那么就可以直接看 View 里面的 onTouchEvent。在 View 的 onTouchEvent 中,首先判断了当前 view 是否是可点击的,如果是可点击的,就可以直接判断这个 view 会消费掉此次事件,相应的,可以直接在 xml 布局中使用 clickable 来指定一个 view 是否可点击,或者对于继承 view 的一些控件,如 Button,ImageButton,内置的就设置为可点击。另外当我们对一个 view 使用 setOnClickListener 设置了一个单机监听的时候,也会将这个 view 置为可点击,如:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
如果一个 view 是可点击的,那么就可以直接在这个方法里面对点击事件进行判断了,比如监听单击、双击、长按什么的。
如果一个 view 有 TOOLTIP 这个 flag 也是会消费掉事件的,对 TOOLTIP 的描述为:
Indicates this view can display a tooltip on hover or long press.
表示 view 会对长按什么的作出反应,也表明了这个 view 是需要能点击的·。
如果这个 view 是不可点击的且不会对长按什么的作出响应,那就表示这个 view 不消费事件,返回 false。如果 view 消费了这个事件,那么这个事件就会在这里终止,否则的话,就会由 dispatchTouchEvent 方法回溯到上一层 view 的 dispatchTouchEvent 方法里面,然后再由上一层的 view 调用他的 onTouchEvent 方法判断是否消费这个事件,如果每一层的 onTouchEvent 里面都没有消费这个事件,那么这个事件就会一层一层的回到 activity 里面,调用 activity 的 onTouchEvent 方法。至此,一个 MotionEvent.ACTION_DOWN 的事件传送完毕了,它最初由 activity 中的 getWindow().superDispatchTouchEvent()方法一步一步送给各个 view 进行判断,最后还是会由自己将这个结果返回出去,而在这个过程中,各个 view 已经根据这个事件作出了自己该有的响应,用户与应用进行了一次友好的交流。
后续事件
不过事情并没有结束,这只是这一系列事件的第一个事件,而在这个事件中,还有不计可数的事件等待着处理,当然,确定了第一个事件的处理流程之后,后续事件都是根据这个事件的处理过程来处理的。因为在处理 MotionEvent.ACTION_DOWN 的时候,是谁消费了这个事件,这个事件都经过了哪些 view,这都是有记录的。比如第一次事件被 view A 消费了,那么当再有事件传输的时候,调用到 A 的 dispatchTouchEvent 方法,就不会再进行判断了,因为它消费了第一次事件,所以后面的事件都要由它来消费,所以就会直接调用这个 view 的 onTouchEvent 方法。不过如果下一个事件传递到 view A 的父 view B 的时候,在 B 的 onIntercrptTouchEvent 方法中返回的值是 true,那么这个事件就会被传递到 B 的 onTouchEvent 方法中,处理完之后就会从 B 向上传,不会再传到 A 了,那么这样的话就是说 A 的事件被 B 拦截了,在 A 里面就会传递一个 MotionEvent.ACTION_CANCEL,供 A 来处理后事,之后这一系列的后续事件的传递就会到 B 之后,就会往回走或者直接终止了。
关于MotionEvent.ACTION_CANCEL
ACTION_CANCEL 表示 当前手势操作被取消。
关于何时会响应 MotionEvent.ACTION_CANCEL 事件,一般的解释为 当用户的手指从当前控件移动到其他控件。比如说有一 ViewGroup a,它有一个子 View b, 手指最先点击在了 b 上面,然后慢慢移动手指,直到移除 b 的范围,并且触点还在 a 的范围上时,b 就会响应这个事件。又或者是,一个事件流从最开始都是由某个 View 消费的,但是在某个时刻,它的某个父 View 因为某种原因需要拦截这个事件,此时就需要给子 View 传递一个 MotionEvent.ACTION_CANCEL 事件告诉子 View 取消对这个事件的监听,做一些资源回收工作等。