背景知识
一般我们所说的事件分发是指点击事件的分发处理。其次,点击事件一般都是通过应用界面点击产生的,而界面又是 xml 布局的直观展示。xml 布局文件里,摆放着我们精心设计的布局控件,最终加载到内存里的就是一棵视图树,而 DecorView 就是树的根。
点击事件的产生会经过 Activity,ViewGroup, View 进行分发处理,这个是规定好的事件分发走向,可以强行记住。
另外,我们说的事件分发机制也可以认为就是对 3 个关键方法的使用,
- dispatchTouchEvent
分发点击事件 - onInterceptTouchEvent
拦截点击事件,这会在 dispatchTouchEvent 方法中调用,格外注意,只有类似于 LinearLayout 这种容器类控件才有这个方法,也就是说只有继承自 ViewGroup 的才有,对于特定的 View,只有处理或不处理事件的份。 - onTouchEvent
处理点击事件,这也会在 dispatchTouchEvent 方法中调用
这 3 个方法的入参都有一个 MotionEvent 类型的参数,这个就是点击事件。并且方法都带有 boolean 型的返回值,true 表示分发或处理成功,此时事件分发流程应该终止,false 表示仍可继续。
所以事件分发的大致逻辑可以这样来理解,接收到事件后对其进行分发操作,在分发的过程中就确定了是否拦截,不拦截就继续分发,拦截了就看看是否处理。
有了以上概念的了解,再来结合源码看看具体怎么走,我将整个流程拆为两个部分,一部分是事件分发,另一部分是事件处理。
事件分发过程
//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
//判断事件是否分发成功
if (getWindow().superDispatchTouchEvent(ev)) {
//成功的话就结束了
return true;
}
//分发了一遍都没有分发掉就自己处理,并返回处理结果
return onTouchEvent(ev);
}
//getWindow() 方法获取的是窗口对象,具体实现类为 PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
//PhoneWindow 里又调用了 mDecor 的分发方法
//mDecor 就是 DecorView 类型
//可见接下去就开始从视图树的根节点开始分发事件了
return mDecor.superDispatchTouchEvent(event);
}
//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
//调用了父类的方法
//DecorView 继承自 FrameLayout, FrameLayout 又继承自 ViewGroup
//FrameLayout 没有该方法,所以最终调用的是 ViewGroup 的
return super.dispatchTouchEvent(event);
}
//ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
//标记事件分发是否处理完成
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//标记是否拦截事件,注意这个只有 ViewGroup 才有的逻辑
final boolean intercepted;
//这里的 mFirstTouchTarget 可能需要重点关注下
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//disallowIntercept 用来表示该 ViewGroup 不具备拦截功能是否成立
//也就是 disallowIntercept 为 true 就是不拦截,默认是 false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//如果可以拦截,那就要通过 onInterceptTouchEvent 方法决定是否拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//如果不是 DOWN 事件,且 mFirstTouchTarget 为空的话就直接拦截
intercepted = true;
}
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//如果事件没有取消,也没有被拦截,就要继续分发
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//按 z 轴进行子 View 排序
final ArrayList preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
//遍历子 View 找到可以处理事件的 view
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
newTouchTarget = getTouchTarget(child);
if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
}
}
//这块我只能看懂个大概,如果有什么不对,可以帮忙指正
//经过上面的遍历,如果能找到合适的处理事件的 view 就会走这里的 else 部分,
//不然就会自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
}
//View
public boolean dispatchTouchEvent(MotionEvent event) {
//同 ViewGroup 一样,在认为点击事件有效的情况下继续执行
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//这里就触发了 onTouch 方法回调,
//这个 onTouch 方法就是通过 setOnTouchListener 方法设置的点击事件监听器的回调,如果事件在这里被处理掉了,那么事件处理就结束了。
result = true;
}
if (!result && onTouchEvent(event)) {
//要是上面没处理掉,就会走到这里,看到了吧,onTouchEvent 就是在这里调的
//所以这里能调的前提是前面没有把事件拦掉,那怎么不会拦掉呢?
//要么不设置监听器,要么 onTouch 方法不要返回 true
result = true;
}
}
return result;
}
经过上面的一路分析,事件的分发到这就算是到的末端了。总结起来就是从点击事件的生成到分发经过了 Activity(PhoneWindow -> DecorView) -> ViewGroup -> View。在事件分发给合适的 View 之后,就要开始处理了。
事件处理过程
//前面我们已经将事件分发到了 ViewGroup,在 ViewGroup 会决定分发给哪个具体 View
//View,代码分析同前面一样,可以不看
public boolean dispatchTouchEvent(MotionEvent event) {
//同 ViewGroup 一样,在认为点击事件有效的情况下继续执行
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//这里就触发了 onTouch 方法回调,
//这个 onTouch 方法就是通过 setOnTouchListener 方法设置的点击事件监听器的回调,如果事件在这里被处理掉了,那么事件处理就结束了。
result = true;
}
if (!result && onTouchEvent(event)) {
//要是上面没处理掉,就会走到这里,看到了吧,onTouchEvent 就是在这里调的
//所以这里能调的前提是前面没有把事件拦掉,那怎么不会拦掉呢?
//要么不设置监听器,要么 onTouch 方法不要返回 true
result = true;
}
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
//onTouchEvent 里逻辑也蛮多的,来看主要的
//简单来说会根据事件类型来做对应处理,
//事件会被封装成 MotionEvent 对象,分为 DOWN, MOVE, UP, CANCEL 四种
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
//UP 事件里会调用下面这个方法,而这个就是我们平时对控件做点击事件响应用的最多的方法
//对应的就是 onClick 方法回调
//所以有时候 set 了 onTouchListener 又 set 了 onClickListener 就会发现 onClick 方法不走了。原因就在于前后顺序的问题。
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
}
其实对于具体 View 的事件分发就是看事件要不要处理。按前面在 ViewGroup 里说的,如果没有合适的 View 会处理事件的话,ViewGroup 会自己处理,但其实 ViewGroup 也继承自 View,所以它的事件处理也和具体 View 的一样。
再回退到 Activity 里,如果事件没有分发成功,那 Activity 就会自己处理
//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
//判断事件是否分发成功
if (getWindow().superDispatchTouchEvent(ev)) {
//成功的话就结束了
return true;
}
//分发了一遍都没有分发掉就自己处理,并返回处理结果
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
//既然事件都没分发成功,这个事件有可能是个关闭事件或者无效的
if(mWindow.shouldCloseOnTouch(this, event)) {
//这里应该就是表示关闭事件,这时会销毁 Activity
finish();
return true;
}
return false;
}
到这事件处理部分也就差不多了。虽说整个过程还是涉及比较多的东西,但平时实际用的话基本就做点击响应,除非一些自定义控件需要特殊处理,那么就要考虑拦不拦截啊,各种类型的事件做什么处理啊。又或者多个控件组合使用,例如列表控件和刷新控件或者嵌套滚动之类的。
总之,事件分发机制还是必须要掌握的,毕竟是 Android 开发里天天都要打交道的东西。
参考内容
Android事件分发机制 详解攻略,您值得拥有 文章讲的很好,很是推荐