Android 中的事件分发拦截机制
立泉Android 的 UI 交互是由事件驱动,App 启动后其MainThread
会被Looper.loop()
阻塞,它会不断检查MessageQueue
中是否存在新的由Handler
从不同Thread
抛出的事件。这些事件会被Looper
在MainThread
中调用对应Handler
的handleMessage()
处理,处理它们的耗时过程则可通过线程管理切换到工作线程执行,然后再切回MainThread
更新 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()
onInterceptTouchEvent()
onTouchEvent()
它们的执行逻辑可用下面一段伪代码描述:
// 事件到达 ViewGroup,首先调用其 dispatchTouchEvent() 方法
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
var result = false
// 判断是否在本层拦截事件
if (onInterceptTouchEvent(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
。尝试阅读源码、实机打印一些日志可以理清这些逻辑,下面的内容是我对它的描述:
首先深吸一口气😯。
事件产生后,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
不希望父级拦截,则不拦截。 - 如果标志位没有被设置,则调用本
View
的onInterceptTouchEvent()
判断是否拦截。
- 如果标志位被设置为不允许拦截,即子
- 如果此事件序列之前的事件没有被处理,则拦截事件,禁止向下分发。这会导致一个现象,如果一个事件序列的某个事件没有被子
View
处理,那么此事件序列的所有后续事件都会被其上层拦截,不会再分发给该View
。
- 如果此事件序列之前的事件已经被处理,则根据
如果拦截事件,调用本View
的onTouchEvent()
处理事件,并将其返回值作为事件是否被处理的依据。
如果不拦截事件,则根据事件坐标判断它属于哪个子View
,调用该View
的dispatchTouchEvent()
把事件向下传递,并将其返回值作为判断事件是否被处理的依据:
- 如果事件被处理,将结果上报,作为本
View
的dispatchTouchEvent()
的返回。 - 如果事件没有被处理,则调用本
View
的onTouchEvent()
处理,并将结果上报。 - 如果没有合适的下层
View
处理事件,则调用本View
的onTouchEvent()
处理,并将结果上报。
OnTouchEventListener
在View
决定自己处理事件时,如果从外界设置了OnTouchEventListener
监听器,则首先调用OnTouchEventListener
的onTouch()
处理事件,返回true
表明事件已被处理,不再调用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
的运行机制清晰表达出来,然后再去接触更多新玩具。