Android中的事件分发拦截机制
立泉Android
的UI交互是由事件驱动的,每一个App启动后,其Main Thread
都会被Looper.loop()
阻塞,它会不断检查MessageQueue
中是否存在新的由Handler
在不同Thread
中抛出的事件。这些事件会被Looper
在Main Thread
中调用对应Handler
的handleMessage()
来处理,而处理这些事件的耗时过程则可以通过线程管理切换到工作线程中执行,然后再切回Main Thread
更新UI。
正确使用这种机制可以保证事件被及时分发处理,并能在UI交互上随之作出响应。这篇文章谈论的事件指的是触控事件
即MotionEvent
。
事件序列
一次触控行为是由一系列MotionEvent
组成的,以手指接触屏幕的ACTION_DOWN
起始,手指离开屏幕的ACTION_UP
结束,中间则是连续的ACTION_MOVE
事件,它们被称为一次触控的事件序列
。
事件的分发轨迹
Activity -> PhoneWindow -> DecorView -> ViewGroup -> View
MotionEvent
事件产生后,会首先由Activity
传递到Window
,然后通过DecorView
进入View树
,至此便会从整个View树
中的根View
开始,层层向下传递,并监听下层View
对事件的处理结果。可以简单的理解为,在View
的树形结构中,事件层层向下分发,而事件的处理结果层层向上报告。如果上层View
发现事件已经下发,但子View
没有处理这个事件,则它会自己去处理这个事件,并把处理结果上报,这种逻辑也是符合常理的。
需要定义一下事件处理
这个词的含义,View
从上级接收事件,该上级会通过这个View
的某些方法返回的特定值来判断其对所接收事件的“态度”。比如onTouchEvent()
返回true
,即认为这个事件已经被处理了,如果返回false
,则说明View
收到了这个事件,也可能确实执行了某些操作,但它的返回值却向上报告称它没有处理这个事件,那么上级也会认为事件没有被处理。
分发、拦截、处理
ViewGroup
有3个重要方法:
// ViewGroup
// 分发事件
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val result = super.dispatchTouchEvent(ev)
// 返回true表示事件下发后被子View处理,false表示事件下发后没有被子View处理
return result
}
// 拦截事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
val result = super.onInterceptTouchEvent(ev)
// 返回true表示拦截事件,即事件不会再向下分发,调用本层的onTouchEvent()处理
return result
}
// 处理事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
val result = super.onTouchEvent(event)
// 返回true表示事件被处理,false表示事件未被处理
return result
}
对于View
,因为已经没有可以向下传递的子View
,也就没必要判断是否要拦截,所以没有onInterceptTouchEvent()
方法:
// View
// 分发事件
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val result = super.dispatchTouchEvent(ev)
// 返回true表示事件被本View处理,false表示事件没有被本View处理
return result
}
// 处理事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
val result = super.onTouchEvent(event)
// 返回true表示事件被处理,false表示事件未被处理
return result
}
事件在ViewGroup
中的处理分为3步:
- 判断是否拦截
- 处理事件(下发或本层处理)
- 向上报告处理结果
事件分发、拦截、处理的3个关键方法:
dispatchTouchEvent()
onInteceptTouchEvent()
onTouchEvent()
它们的执行逻辑可以用下面一段伪代码描述:
// 事件到达ViewGroup,首先会调用其dispatchTouchEvent()方法
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
var result = false
// 判断是否在本层拦截事件
if (onInteceptTouchEvent(ev)) {
// 拦截事件,调用本层onTouchEvent()方法处理事件,并将其返回值作为事件处理结果
result = onTouchEvent(ev)
} else {
// 不拦截事件,查找可以处理事件的子View
val item = findItem()
if(item == null) {
// 没有找到可以处理事件的子View,调用本层的onTouchEvent()方法处理事件
result = onTouchEvent(ev)
} else {
// 找到了可以处理事件的子View,调用其dispatchTouchEvent()方法传递事件,并将其返回值作为事件执行结果
result = item.dispatchTouchEvent(ev)
}
}
return result
}
真实的事件分发拦截逻辑十分复杂,涉及到对ACTION_DOWN
这一特殊事件的判断和一些与拦截有关的标志位处理,比如FLAG_DISALLOW_INTERCEPT
。但如果尝试阅读源码,参考一些技术文档,再实机打印一些Log
,还是可以理清这些逻辑的,下面的内容就是我对它的描述:
首先深吸一口气。。
事件产生后,Activity
的dispatchTouchEvent()
首先被调用,它会调用PhoneWindow
的superDispatchTouchEvent()
,如果返回false
,说明PhoneWindow
及该Window
上的View
都没有处理这个事件,将调用Activity
自己的onTouchEvent()
处理。
PhoneWindow
的superDispatchTouchEvent()
被调用时,会调用View树
的根View
即DecorView
的dispatchTouchEvent()
。
DecorView
收到事件即说明事件已经正式进入View树
。
DecorView
的dispatchTouchEvent()
被调用时,会首先判断要不要拦截这个事件:
- 如果是
ACTION_DOWN
,即它是一个事件序列的开始,则调用本View
的onInterceptTouchEvent()
判断是否拦截。 - 如果不是
ACTION_DOWN
,即它不是一个事件序列的开始,有两种情况:- 如果此事件序列之前的事件已经被处理了,则根据
FLAG_DISALLOW_INTERCEPT
标志位:- 如果标志位被设置为不允许拦截,则不拦截。
- 如果标志位没有被设置,则调用本
View
的onInterceptTouchEvent()
判断是否拦截。
- 如果此事件序列之前的事件没有被处理,则拦截事件,禁止向下分发。所以这会导致一个现象,如果一个事件序列的某个事件没有被
View
处理,那么此事件序列的所有后续事件都会被上层拦截,不会再分发给该View
。
- 如果此事件序列之前的事件已经被处理了,则根据
如果拦截事件,则调用本View
的onTouchEvent()
来处理事件,并将其返回值作为事件是否被处理的依据。
如果不拦截事件,则根据事件坐标判断它属于哪个子View
,调用该View
的dispatchTouchEvent()
把事件向下传递,并将其返回值作为判断这个事件是否被处理的依据:
- 如果事件被处理,则将结果上报,作为本
View
的dispatchTouchEvent()
的返回。 - 如果事件没有被处理,则调用本
View
的onTouchEvent()
来处理,并将结果上报。 - 如果没有合适的下层
View
处理事件,则调用本View
的onTouchEvent()
处理,并将结果上报。
OnTouchEventListener
在View
使用onTouchEvent()
方法来处理事件,但如果从外界设置了OnTouchEventListener
监听器,则要处理事件时首先调用OnTouchEventListener
的onTouch()
来处理事件,如果返回true
则事件已被处理,不再调用View
的onTouchEvent()
。如果返回false
则调用View
的onTouchEvent()
来处理事件,并将其返回值作为事件是否被处理的结果上报。
即从外部设置的OnTouchEventListener
的优先级是高于View
的onTouchEvent()
的。
一些结论
理解Android
中事件的分发、拦截、处理机制非常重要,这也是自定义View
和处理滑动冲突
的知识基础,不过日常开发中只需要记住一些关键结论就足以应对大部分场景的需求了。
- 事件的分发是层层向下的,事件的处理是层层向上的,当子
View
没有处理事件时,父View
的onTouchEvent()
就会被调用,并把其返回值作为是否处理了事件的结果向更上层汇报。 - 从一个层级分发下去的事件,如果没有被处理,则该事件序列的后续事件在这个层级就会被拦截,不会再向下分发了。
ACTION_DOWN
是一个事件序列的开始,如果一个View
接收了ACTION_DOWN
事件,或一个ViewGroup
拦截了ACTION_DOWN
事件,而没有处理,则此事件序列的后续事件就会在上层被直接拦截,不会再下发到这里。所以,如果要监听整个事件序列,ACTION_DOWN
必须被处理。ViewGroup
一旦决定拦截事件,无论是否处理,此事件序列的后续事件都会跳过此ViewGroup
的onInterceptTouchEvent()
而直接执行onTouchEvent()
,即后续事件会在本层被直接拦截,不会再向下分发。- 事件分发是层层向下传递的,下层的某个
View
或ViewGroup
如果处理了一个事件,该事件序列的后续事件依然是在上面层层传递下来的,每一层都可以拦截,而一旦拦截,后续事件就不会向下再传递过来了。如果不想让父View
拦截,可以设置它的FLAG_DISALLOW_INTERCEPT
标志位,让父View
在这种情况下不拦截事件。 - 如果
View
处理了ACTION_DOWN
事件,则此事件序列的后续事件都可以被发送到该View
,除非在上层被拦截。对于此事件序列的后续事件,如果本View
没有处理,则上层ViewGroup
的onTouchEvent()
也不会被调用,当前View
依然可以正常的收到后续事件,这些事件最终会被传递给Activity
处理。 - 如果事件到达一个
ViewGroup
,则onDispatchTouchEvent()
是一定会被执行的,而onInterceptTouchEvent()
则不一定会被执行,某些情况下,无需调用此方法就可以判断是否要拦截这个事件 。ACTION_DOWN
事件,一定调用onInterceptTouchEvent()
。- 非
ACTION_DOWN
事件,此序列前事件未被处理过,则直接拦截,不调用onInterceptTouchEvent()
。 - 非
ACTION_DOWN
事件,此序列前事件被处理过,但ViewGroup
的FLAG_DISALLOW_INTERCEPT
标志位被设置,即不允许拦截事件,则不拦截,也不调用onInterceptTouchEvent()
。
ViewGroup
的onTouchEvent()
默认返回false
,即不处理事件。View
的onTouchEvent()
的返回值与具体的View
有关,只要clickable
和longClickable
属性有一个是true
,则onTouchEvent()
返回true
,即处理事件。
其实工作一年来,我的多数精力都被集中在业务逻辑的实现上,已经很久没有认真复习过View
了,所以最近一直在整理过去的学习笔记,把这些原生View
的运行机制清晰的表达出来,然后才可以放心的去接触更多新玩具。