以下内容参考:【透镜系列】看穿 > 触摸事件分发。 这篇文章对整个事件分发的流程写得很清楚,非常值得一读。
讲讲View 的事件分发吧:
事件递归传递
首先,以一个基本的需求为例子:Activity 中有一堆层层嵌套的 View,有且只有最里边那个 View 会消费事件。也就是说,ViewGroup 、 ViewGroup 里面包着一个 View 。
当用户进行操作,会产生一个事件 MotionEvent,里面携带着此次操作的类型和坐标(类型包括 ACTION_DOWN \ ACTION_MOVE \ ACTION_UP, 分别对应着在屏幕上点击、移动、抬起).这个事件操作系统会传递到对应的 Activity,Activity 传到根 View(DecorView),整体上就是经过一层一层 ViewGroup ,传到最里边的 View。
而上面提到的事件传递过程,其实就是从上到下执行一个递归函数,在源码中叫 dispatchTouchEvent。
事件消费/执行
事件是传递了,那么接下来就考虑执行的过程:
首先要确定一个原则:用户的一次操作,只有一个 View 能去执行。不然就会出现一些很混乱的情况,基于这个原则,事件的消费应该是从下往上的,也就是说本层的 View 判断这个事件自己消费不消费,如果消费就执行自己的 onTouchEvent 方法并返回 true,如果不消费就返回 false,告诉上一层自己不消费此次事件。上一层发现自己的下一层不消费该事件,就调用自己的onTouchEvent去处理。
以一个 ViewGroup 里面有一个 View 为例:
open class MView {
open fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return onTouchEvent(ev)
}
open fun onTouchEvent(ev: MotionEvent): Boolean {
return false
}
}
class MViewGroup(private val child: MView) : MView() {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var handled = child.dispatchTouchEvent(ev)
if (!handled) handled = onTouchEvent(ev)
return handled
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
return false
}
}
事件流
然而,一次用户操作中事件分发的过程肯定要考虑更多,还要对事件操作的类型进行判断。假设是 ACTION_DOWN 事件(这肯定是用户执行操作发过来的第一个事件),当接收到这个事件,就要把对事件流中关于保存状态的变量进行重置。就比如 ViewGroup 中的isChildNeedEvent变量,该变量判断子 View 是否会消费事件。接下来如果再传来比如 ACTION_UP事件,就可以根据这个变量去决定是否还把事件传给子 View 。
open class MView {
open fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return onTouchEvent(ev)
}
open fun onTouchEvent(ev: MotionEvent): Boolean {
return false
}
}
class MViewGroup(private val child: MView) : MView() {
private var isChildNeedEvent = false
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var handled = false
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
//重置状态
clearStatus()
handled = child.dispatchTouchEvent(ev)
//如果得知用户已经处理了事件,将 isChildNeedEvent 置为 true
if (handled) isChildNeedEvent = true
if (!handled) handled = onTouchEvent(ev)
} else {
if (isChildNeedEvent) handled = child.dispatchTouchEvent(ev)
if (!handled) handled = onTouchEvent(ev)
}
if (ev.actionMasked == MotionEvent.ACTION_UP) {
//抬起手的操作也要重置状态
clearStatus()
}
return handled
}
private fun clearStatus() {
isChildNeedEvent = false
}
override fun onTouch(ev: MotionEvent): Boolean {
return false
}
}
滑动冲突
但是这样的处理还是不够的,考虑一个场景:假设一个有按钮,而它的父View是一个滑动类型的 View,那么在用户刚点击这个按钮,产生一个 ACTION_DOWN事件,根据之前的流程,这个按钮会把这个事件给消费掉,而父View 没机会去消费这个事件流了。但是,在用户刚按下的时候,我们并不知道他究竟是要点击这个按钮,还是要滑动,这就产生了父与子 View 都有能力去消费这个事件的冲突。
外部事件拦截
那么解决方案就是:(外部事件拦截)
- 首先 dispatchTouchEvent方法不只是由上往下传递事件,还可以在传递事件之前将事件拦截。
- 当事件传递到父View,即使事件父View 能消费,但是也不拦截,先交给子 View
- 子 View 不处理事件,那什么事情也没有,父View 处理就行了。
- 子 View 处理事件,可滑动父View就会绷紧神经暗中观察伺机而动,观察事件是不是符合自己的消费模式,一旦发现符合,它就把事件流拦截下来,即使子View也在处理事件,它也不往里
dispatchTouchEvent事件了,而是直接给自己的onTouchEvent()
open class MView {
open fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return onTouchEvent(ev)
}
open fun onTouchEvent(ev: MotionEvent): Boolean {
return false
}
}
class MViewGroup(private val child: MView) : MView() {
private var isChildNeedEvent = false
private var isSelfNeedEvent = false
override fun dispatch(ev: MotionEvent): Boolean {
var handled = false
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
clearStatus()
//判断是否要拦截
if (onIntercept(ev)) {
isSelfNeedEvent = true
handled = onTouchEvent(ev)
} else {
handled = child.dispatchTouchEvent(ev)
if (handled) isChildNeedEvent = true
if (!handled) {
handled = onTouchEvent(ev)
if (handled) isSelfNeedEvent = true
}
}
} else {
if (isSelfNeedEvent) {
handled = onTouchEvent(ev)
} else if (isChildNeedEvent) {
if (onIntercept(ev)) {
isSelfNeedEvent = true
handled = onTouchEvent(ev)
} else {
handled = child.dispatchTouchEvent(ev)
}
}
}
if (ev.actionMasked == MotionEvent.ACTION_UP) {
clearStatus()
}
return handled
}
private fun clearStatus() {
isChildNeedEvent = false
isSelfNeedEvent = false
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
return false
}
open fun onIntercept(ev: MotionEvent): Boolean {
return false
}
}
可见,如果事情拦截掉,增加一个isSelfNeedEvent记录自己是否拦截过事件,如果拦截过,后续事件直接就交给自己处理。
而且对于框架的使用者来说,关注点还是非常少
- 重写
onIntercept()方法,判断什么时候需要拦截事件,需要拦截时返回true - 重写
onTouch()方法,如果处理了事件,返回true
事情还没完
内部事件拦截
上面的处理思路虽然实现了需求,但可能会导致一个问题:里边的子 View 接收了一半的事件,可能都已经开始处理并做了一些事情,父 View 忽然就不把后续事件给它了,会不会违背用户操作的直觉?甚至出现更奇怪的现象?
分为两种情况讨论:
-
子 View 接收了一半事件,但还没有真正开始反馈交互,或者在进行可以被取消的反馈。
对于这种可以被取消的情况,可以通过父View 在拦截的时候给子 View 传一个 CANCEL 事件,比如子 VIEW由于接收了一半事件使得某个地方高亮了,CANCEL 事件就将这个高亮取消 -
View接收了一半事件,已经开始反馈交互了,这种反馈最好不要去取消它,或者说取消了会显得很怪
例子:
在`ViewPager`里有三个page,page里是`ScrollView`,`ViewPager`可以横向滑动,page里的`ScrollView`可以竖向滑动。
如果按前面逻辑,当`ViewPager`把事件给里边`ScrollView`之后,它也会偷偷观察,如果你一直是竖向滑动,那没话说,`ViewPager`不会触发拦截事件
但如果你竖着滑着滑着,手抖了,开始横滑(或者只是斜滑),`ViewPager`就会开始紧张,想「组织终于决定是我了吗?真的假的,那我可就不客气了」,于是在你斜滑一定距离之后,忽然发现,你划不动`ScrollView`了,而`ViewPager`开始动
原因就是`ScrollView`的竖滑被取消了,`ViewPager`把事件拦下来,开始横滑
这个体验还是比较怪的,会有种过于灵敏的感觉,会让用户只能小心翼翼地滑动
解决方法:
简单来说就是子 View通过通知父View,你不要拦截。Android也使用了这个方式,父View给子View提供了一个方法requestDisallowInterceptTouchEvent(),子View调用它改变父View的一个状态,同时父View每次在准备拦截前都会判断这个状态(当然这个状态只对当前事件流有效)
实现这种逻辑就是让父View 继承 ViewParent,实现方法requestDisallowInterceptTouchEvent,这个方法会使得父 View 在想拦截的时候再去判断子 View 是不是不让自己拦截。(RecyclerView也有这种方法)。
interface ViewParent {
fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean)
}
open class MView {
var parent: ViewParent? = null
open fun dispatch(ev: MotionEvent): Boolean {
return onTouch(ev)
}
open fun onTouch(ev: MotionEvent): Boolean {
return false
}
}
open class MViewGroup(private val child: MView) : MView(), ViewParent {
private var isChildNeedEvent = false
private var isSelfNeedEvent = false
private var isDisallowIntercept = false
init {
child.parent = this
}
override fun dispatch(ev: MotionEvent): Boolean {
var handled = false
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
clearStatus()
// add isDisallowIntercept
if (!isDisallowIntercept && onIntercept(ev)) {
isSelfNeedEvent = true
handled = onTouch(ev)
} else {
handled = child.dispatch(ev)
if (handled) isChildNeedEvent = true
if (!handled) {
handled = onTouch(ev)
if (handled) isSelfNeedEvent = true
}
}
} else {
if (isSelfNeedEvent) {
handled = onTouch(ev)
} else if (isChildNeedEvent) {
// add isDisallowIntercept
if (!isDisallowIntercept && onIntercept(ev)) {
isSelfNeedEvent = true
// add cancel
val cancel = MotionEvent.obtain(ev)
cancel.action = MotionEvent.ACTION_CANCEL
handled = child.dispatch(cancel)
cancel.recycle()
} else {
handled = child.dispatch(ev)
}
}
}
if (ev.actionMasked == MotionEvent.ACTION_UP
|| ev.actionMasked == MotionEvent.ACTION_CANCEL) {
clearStatus()
}
return handled
}
private fun clearStatus() {
isChildNeedEvent = false
isSelfNeedEvent = false
isDisallowIntercept = false
}
override fun onTouch(ev: MotionEvent): Boolean {
return false
}
open fun onIntercept(ev: MotionEvent): Boolean {
return false
}
override fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean) {
this.isDisallowIntercept = isDisallowIntercept
parent?.requestDisallowInterceptTouchEvent(isDisallowIntercept)
}
}
~到这里,整个 View 的分发机制已经总结完了~