事件分发机制
事件分发机制
1、触摸事件分发流程
在《Activity、Window、DecorView以及ViewRoot层级关系全解析》一节有下图:
我们知道View的结构是树形结构,View可以放在ViewGroup中,ViewGroup也可以放在另一个ViewGroup中,如此就形成了层层嵌套的关系。当我们触摸到屏幕后,就会生成一个Touch事件,常见的Touch事件有:
- MotionEvent.ACTION_DOWN:按下
- MotionEvent.ACTION_MOVE:滑动
- MotionEvent.ACTION_CANCEL:非人为原因结束本次事件
- MotionEvent.ACTION_UP:抬起 一般来说,一个事件会经过:按下 –》 滑动 –》抬起,这三个阶段,并在这个过程中会有非人为原因结束本次触摸流程。这些事件会在代码里会封装成一个MotionEvent。那么,当MotionEvent产生后,系统就会将其传递给View树,MotionEvent在View的层级传递,并最终得到处理的过程,就是触摸事件分发流程。一个流程的传递顺序是:
Activity/Window –> ViewGroup –> View
其中,View就是各种控件,如Button、TextView等,而ViewGroup是View的子类,因此本质上也是一个View,只不过ViewGroup可以包含多个子View和定义布局参数。
2、触摸事件分发的3个重要方法
有以下3个重要方法是必须掌握的:
- dispatchTouchEvent(MotionEvent ev):进行事件的分发,在View和ViewGroup类都有该方法,下文会对该方法的源码进行分析,需要区分清楚;
- onInterceptTouchEvent(MotionEvent ev):进行事件拦截,在dispatchTouchEvent()中调用,在分发的过程中判断是否需要进行拦截,需要注意的是只有ViewGroup有该方法,View是没有提供该方法的。如果返回true代表拦截,返回false代表不拦截;
- onTouchEvent(MotionEvent ev):触摸事件处理,同样在dispatchTouchEvent()方法中进行调用,如果返回true代表已处理事件,返回false代表不处理事件,事件继续传递。
为了更好的了解三者的关系,我们从源码出发,首先看看ViewGroup的dispatchTouchEvent(),源码是Android9.0.0=》/frameworks/base/core/java/android/view/ViewGroup.java:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); //1
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) { //2:遍历ViewGroup的子View
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //3
...
break;
}
...
}
}
}
}
在[注释1]调用了onInterceptTouchEvent()方法来判断是否要拦截当前的事件。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
可以看出onInterceptTouchEvent()默认返回false,代表不拦截。接着在[注释2]遍历ViewGroup的子View,如果子View可以接收到触摸事件,则会执行[注释3]dispatchTransformedTouchEvent():
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent); //4
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent); //5
}
...
return handled;
}
在[注释4],如果当前的ViewGroup没有View则调用父类的dispatchTouchEvent(),在[注释5]如果有子View,则调用子View的dispatchTouchEvent()。因为ViewGroup类也是继承View类的,因此[注释4]super.dispatchTouchEvent(transformedEvent)对应的源码在:/frameworks/base/core/java/android/view/View.java
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 ///6
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { //7
result = true;
}
if (!result && onTouchEvent(event)) { //8
result = true;
}
}
...
return result;
}
这个函数要稍微捋一捋,如果[注释6]mOnTouchListener不为空且[注释7]mOnTouchListener.onTouch()返回true,则result设置为true,因此[注释8]的onTouchEvent(event)就不执行,反之则执行。
最后,看看三大重要方法最后一个的onTouchEvent(),同样在View.java里:
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE //9
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) //10
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP: // 11
...
if (mPerformClick == null) {
mPerformClick = new PerformClick(); //12
}
if (!post(mPerformClick)) {
performClickInternal();
}
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return false;
}
从上面代码知道,[注释9]的CLICKABLE(点击)和[注释10]的LONG_CLICKABLE(长按点击)有一个为true,则会进入switch循环处理这个事件。最后在ACTION_UP[注释11]事件会调用performClick()[注释12]方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
如果View设置了点击事件OnClickListener,那么它的onClick()方法就会被执行。
3、onTouch()、onTouchEvent()、onClick()傻傻分不清?
这三个方法,相信很多读者都经常见过,到底谁的优先级更高呢?结合[注释7][注释8][注释12],可以看出,如果执行了onTouch()并返回false才会执行onTouchEvent(),在执行onTouchEvent()时如果调用了setOnClickListener()注册了点击事件回调,则还会执行OnClick(),因此三者的优先级为:
onTouch() > onTouchEvent() > OnClick()
4、事件分发总结
4.1 表格总结
上述源码分析如果觉得还有点懵,我们做个总结:
上面表格里的“事件列”是指一个事件序列有:ACTION_DOWN、ACTION_MOVE、ACTION_UP等。大部分情况由上面的表格就可以概括了。
4.2 伪代码表示
如果用伪代码可以如下表示:
// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// 父View决定是否拦截事件
if(onInterceptTouchEvent(event)){
// 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
// 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
// 该其他View。
consume = onTouchEvent(event);
}else{
// 调用子View的dispatchTouchEvent(event)方法继续分发事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}
以下总结的非常好,因此直接抄录至刘偶像的博客,最后升华下本文的理解:参考文献
4.3 事件由上到下的传递规则
当点击事件产生后会由Activity来处理再传递给Window再传递给顶层的ViewGroup,一般在事件传递中只考虑ViewGroup的onInterceptTouchEvent()方法,因为一般情况我们不会去重写dispatchTouchEvent()方法。
对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGroup的onInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理,如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则交给它的子元素的dispatchTouchEvent()来处理,如此的反复下去。如果传递给最底层的View,View是没有子View的,就会调用View的dispatchTouchEvent()方法,一般情况下最终会调用View的onTouchEvent()方法。
举个现实的例子,就是我们的应用产生了重大的bug,这个bug首先会汇报给技术总监那:
技术总监(顶层ViewGroup)→技术经理(中层ViewGroup)→工程师(底层View)
技术总监不拦截,把bug分给了技术经理,技术经理不拦截把bug分给了工程师,工程师没有下属只有自己处理了。
事件由上而下传递返回值规则为:true,拦截,不继续向下传递;false,不拦截,继续向下传递。
4.4 事件由下而上的传递规则
点击事件传给最底层的View,如果他的onTouchEvent()方法返回true,则事件由最底层的View消耗并处理了,如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()仍旧返回返回false,则继续传递给改父View的父View处理,如此的反复下去。
再返回我们现实的例子,工程师发现这个bug太难搞不定(onTouchEvent()返回false),他只能交给上级技术经理处理,如果技术经理也搞不定(onTouchEvent()返回false),那就把bug传给技术总监,技术总监一看bug很简单就解决了(onTouchEvent()返回true)。
事件由下而上传递返回值规则为:true,处理了,不继续向上传递;false,不处理,继续向上传递。