事件分发机制在开发或面试中常常被提及,而其又是自定义view点击事件的处理、滑动冲突等问题的理论基础。如果想写出酷炫的自定义View,理解该机制是必不可少的功课。
但是发现往往在开发过程中,一动手写事件逻辑,常常出现一些无法理解的错误,如果还停留在“onTouchEvent 返回true拦截事件,返回false不拦截事件”表层理论,远远无法满足开发需求的。不得不翻出曾经收藏的博文或笔记,对于一个开发人员来说,太浪费时间了。 这个问题原因是,对分发机制不甚了解和对其中细节不予关注所导致的, 那么我们来梳理一下事件分发机制的结构和一些不能被忽视的细节。
本文不会涉及到源码分析,但会从源码中剥离出重要的逻辑。
事件分发机制优秀博文非常多,推荐如下几篇:
一文读懂Android View事件分发机制:https://www.jianshu.com/p/238d1b753e64
Android事件分发机制完全解析,带你从源码的角度彻底理解(郭霖大神):https://blog.csdn.net/guolin_blog/article/details/9097463/
首先从整体结构上去了解整个事件分发机制,然后深究事件分发机制中一些细节。
讲述该机制时,常常提及流程走向的“U形图”:
上图ACTION_DOWN事件流转过程,注意标红的地方,因为View默认情况下,onTouchEvent() 返回false,但部分系统View会返回false,比如ImageView,具体原因请关注下面的终极伪代码逻辑。
一个完整的事件流程包括按下(ACTION_DOWN),移动(ACTION_MOVE), 抬起(ACTION_UP)三个事件。理解事件分发机制,必须要将ACTION_DOWN事件区别于其他事件来分析,其他事件(ACTION_MOVE 和 ACTION_UP)能否继续流转往往取决处理ACTION_DOWN事件的返回结果,我们直接来总结一下:
ACTION_DOWN:
ACTION_MOVE/ACTION_UP:
以上的结论依旧很难记忆和理解,结合源码我们可以把上面的逻辑写成伪代码:
ViewGroup:
// mFirstTouchTarget 可以理解为存储可以处理Touch事件的子View(不包括自身)的数据结构
private TouchTarget mFirstTouchTarget;
public boolean dispatchTouchEvent(MotionEvent ev) {
// 是否中断
final boolean intercepted;
// 仅在ACTION_DOWN 和 已确定处理的子View时 调用,一旦onInterceptTouchEvent返回true,
//则后续将不会在被调用和接收事件。后面会讲返回true后,mFirstTouchTarget会被为null;
if (action == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev);
}
// 如果不被拦截,在ACTION_DOWN事件处理中遍历所有的子View,找寻可以处理Touch事件的目标子View
// 然后封装到mFirstTouchTarget,如果子View的dispatchTouchEvent返回true,则认为是目标子View;
if(!intercepted){
if (action == MotionEvent.ACTION_DOWN) {
if(child.dispatchTouchEvent(MotionEvent ev)){
mFirstTouchTarget = addTouchTarget(child);
break;
}
}
}
boolean handled;
// 如果mFirstTouchTarget == null,调用自身onTouchEvent()
if(mFirstTouchTarget == null){
handled=onTouchEvent(ev);
}else{
// 应上面的逻辑,如果ACTION_MOVE传递过程中被拦截,则将mFirstTouchTarget置为null,并传递一个cancel事件,
// 告诉目标子View当前动作被取消了,后续事件将不会再次被传递;
if (intercepted){
ev.action=MotionEvent.ACTION_CANCEL;
handled=mFirstTouchTarget.child.dispatchTouchEvent(ev);
mFirstTouchTarget=null;
}else {
// 调用目标子view的dispatchTouchEvent,这也是为什么,上面结论所述的,dispatchTouchEvent/onTouchEvent
// 在ACTION_DOWN事件返回true,不管子View返回什么值,都能收到后续事件,会出现所谓控制“失效”的现象。
handled=mFirstTouchTarget.child.dispatchTouchEvent(ev);
}
}
retrun handled;
}
以上伪代码囊括了整个事件机制的逻辑(包括所有事件的处理,都可以套用上面的逻辑进行分析)。再来归纳几个关键点:
View:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result;
if(mOnTouchListener !=null
&& ENABLE
&& mOnTouchListener.onTouch(this, event)){
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if(DISABLED){
return clickable;
}
if(clickable){
switch (action) {
case MotionEvent.ACTION_UP:
onClick(this);//onLongClick(this)
break;
}
}
}
通过View 的 dispatchTouchEvent和onTouchEvent 伪代码可以发现非常多有意思的事情,在下一节重点聊一聊。
请注意:伪代码是基于源码抽离出来的逻辑骨架而写成,为了方便阅读往往忽略部分细节,源码中并非如此,详细逻辑可以参考上面郭霖大神的源码解读或自行查看源码。
一般情况下,三者的调用顺序为:onTouch()>onTouchEvent()>onClick()。通过上面的伪代码发现,有几种特殊情况:
mBt.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
mBt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
mBt.setEnabled(false);
mBt.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
mBt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick");
}
});
mBt.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.i(TAG, "onLongClick");
return false;
}
});
mBt.setClickable(false);
跟着源码,以及结合众多优秀的博文(郭霖大神),尝试把逻辑架构抽取出来写成上述的伪代码,对理解事件分发机制有着莫大的好处。以及其中细节也加多加留意。如有错误请多多指正,不胜感激。