Android事件体系
一切的起源
https://blog.csdn.net/a992036795/article/details/51690303 这篇文章中对Android事件的起源进行了详细的分析,虽说代码版本应该是Android5.0以前的,但也具有一定的参考价值。
总结起来大致可以概括为:Activity启动过程中会创建PhoneWindow和DecorView,然后通过ViewRootImpl向InputManagerService注册一个InputChannel以监听屏幕触摸事件,后续的工作原理涉及硬件层的知识就不再追溯。从这个流程里面我们可以提取出我们所关心的问题:Android事件的源头是在Activity启动过程中注册的,事件流向应用层可以简单认为是Activity->View。
事件传递机制
![](https://img.haomeiwen.com/i2362099/03cfdd01d65a061f.png)
Android整个事件流转过程如上图所示,是一个从Activity->ViewGroup->View自上而下的过程,当某一层拦截了事件传递便就此结束,如果下面的层级都不需要拦截事件,则再次一层层向上回传,最终回到Acitivity的onTouchEvent方法。整个过程和现实中工作安排是一模一样的,老板把任务安排给部门领导,部门领导再安排给手下,一层层向下委派,当底层手下无法解决这个问题时,再一层层向上反馈,最终回到老板处。其中ViewGroup的事件分发伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
//是否处理事件标志
boolean handled = false;
if(!onInterceptTouchEvent()){
//遍历子view分发
handled = childView.dispatchTouchEvent(event);
}
if(!handled) handled = onTouchEvent(event);
return handled;
}
事件序列
Android的touch事件从手指触摸屏幕开始,到手指离开屏幕,可以视为一个完整的事件序列,从MotionEvent的类型来看,一个事件序列包含一个ACTION_DOWN事件、n个ACTION_MOVE事件、一个ACTION_UP事件:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上滑动
ACTION_UP:手指离开屏幕
一个完整的事件序列只能被同一个view消耗,简而言之,一旦某个view决定拦截ACTION_DOWN事件之后,这个事件序列之后的事件都将由该view消耗,其他的view即使想要单独拦截ACTION_MOVE、ACTION_DOWN事件也是做不到的,我们可以写个简单的demo来论证这个理论。
public class CustomViewGroup extends ViewGroup {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("TAG","ViewGroup dispatchTouchEvent:"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("TAG","ViewGroup onInterceptTouchEvent:"+ev.getAction());
//第二次测试
// if(event.getAction() == MotionEvent.ACTION_DOWN) {
// return true;
// }else{
//第三次测试
// return false;
// }
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG","ViewGroup onTouchEvent:"+event.getAction());
// if(event.getAction() == MotionEvent.ACTION_DOWN) {
// return true;
// }else{
//第三次测试
// return false;
// }
return super.onTouchEvent(event);
}
}
public class CustomView extends TextView {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("TAG","View dispatchTouchEvent:"+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG","View onTouchEvent:"+event.getAction());
// if(event.getAction() == MotionEvent.ACTION_DOWN) {
// return true;
// }
return super.onTouchEvent(event);
}
}
初次测试:CustomViewGroup,CustomView均设置为不可点击,不拦截任何事件,日志如下:
//0-ACTION_DOWN,1-ACTION_UP,2-ACTION_MOVE
11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup dispatchTouchEvent:0
11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup onInterceptTouchEvent:0
11-13 23:05:05.751 17354-17354/ E/TAG: View dispatchTouchEvent:0
11-13 23:05:05.751 17354-17354/ E/TAG: View onTouchEvent:0
11-13 23:05:05.751 17354-17354/ E/TAG: ViewGroup onTouchEvent:0
正如事件分发机制所述,事件经过CustomViewGroup#dispatchTouchEvent->CustomViewGroup#onInterceptTouchEvent->子View#dispatchTouchEvent->子View#onTouchEvent,子View无法处理后再次回到CustomViewGroup#onTouchEvent。(这边发现日志只有ACTION_DOWN的事件分发,暂且记下后续会有说明)
第二次测试:CustomViewGroup,CustomView均设置拦截ACTION_DOWN事件,日志如下:
//
11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup dispatchTouchEvent:0
11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup onInterceptTouchEvent:0
11-13 23:17:00.691 22798-22798/ E/TAG: ViewGroup onTouchEvent:0
11-13 23:17:00.751 22798-22798/ E/TAG: ViewGroup dispatchTouchEvent:1
11-13 23:17:00.751 22798-22798/ E/TAG: ViewGroup onTouchEvent:1
由日志可看出,ViewGroup决定拦截ACTION_DOWN事件之后,都没有调用子view的dispatch,而且在分发ACTION_UP事件时都没有调用onInterceptTouchEvent。
第三次测试:CustomViewGroup,CustomView均设置拦截ACTION_DOWN事件,并且CustomViewGroup设置只拦截ACTION_DOWN事件,日志如下:
//
11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:0
11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup onInterceptTouchEvent:0
11-13 23:22:39.181 24428-24428/ E/TAG: ViewGroup onTouchEvent:0
11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:2
11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup onTouchEvent:2
11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup dispatchTouchEvent:1
11-13 23:22:39.261 24428-24428/ E/TAG: ViewGroup onTouchEvent:1
由日志可看出,ViewGroup决定拦截ACTION_DOWN事件之后,即使设置不拦截其他类型事件,整个事件序列的日志还是到ViewGroup就结束了。
综上所述,Android对于事件序列的分发有某种特殊的机制:一个事件序列只会由拦截ACTION_DOWN的那个View消耗,同时在事件分发时有做过优化,能够在分发ACTION_UP、ACTION_MOVE事件时找到拦截ACTION_DOWN的那个View并跳过onIntercept方法。得出该结论之后,再回到源码也能找到相应的依据,具体代码片段见ViewGroup的dispatchTouchEvent方法:
//处理ACTION_DOWN事件时会记录touchTarget
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
}
//后续会对touchTarget进行判断
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
由此可见,在一次事件序列分发过程中,ViewGroup会记录最初的touchTarget,有利于提高后续的事件分发效率。
滑动冲突
对Android View的事件分发体系有了完整的了解之后,比较经典的应用就是滑动冲突的解决,核心思想就是ACTION_UP事件拦截的特殊处理,方法有内部拦截法和外部拦截法两种,有兴趣的童鞋可以查查资料了解下。