View 的点击事件的传递

这一篇不是想写关于 View 的点击事件的传递过程,类似的 blog 已经有写得非常好的了。想记录一下今天研究一个问题的一些想法。

我遇到的需求是,在一个页面里头需要响应长按,长按后会触发一些自定义的绘制。这个页面底下还有一个 ViewPager 承载了一些内容。大概是这样的:

image.png

C 是最外层的 parent,A 是负责响应长按事件的一个透明 View,B 是 ViewPager。

原本的想法是 C 作为最外层的 parent,在 action_down 的时候会通过 dispatchTouchEvent 把事件分发下去给子 view。然后先让 A 进行判断,如果是长按的话就消费掉,并且让 C disallowInterceptTouchEvent 就好了,保证事件序列都在 A 上。如果是滚动事件的话,就不让 A 去消费,自然而然让 B 去消费滚动事件就可以了。

但事实上并不能实现这样理想的效果。表现在于,如果在 A 里头的 action_down 事件时就 return true (这一步是为了能 A 能进一步通过 gestureDetector 判断长按事件),就会造成后面全部的事件都跑到了 A 里头来了,C 没办法继续事件分发,B 也就不可能会接收到 action_move 的事件。而如果让 A 不 return true,让 action_move 自然下发到 B,会很诡异地触发 A 的 longPress 事件。每一次都会触发,这让原本的设计无法实现预想的效果。

为什么会这样呢?白天的时候看到诡异的现象心情烦躁没有能梳理明白,晚上冷静下来了,梳理了一下上面的过程,感觉其中的原因是这样的:

根本原因是:Android 中的 MotionEvent 只能有两个传递方向: parent --> child 或者 child --> parent。 事件传递的过程就是如此,parent 会判断是否需要拦截(onInterceptTouchEvent),如果不的话会向子 view 传递。如果子 view 不消耗,就会再从子 view 回到 parent 的 onTouchEvent 进行消费。这个路径是双向的,但是起点和终点分别是 parent 和 child。

上面我的设计里是这么想的:如果 child A 不需要,那么就把事件交给 child B。这样就变成了子 View 之间事件可以进行传递,不符合上面所说的,传递只能在 parent 和 child 之间发生。换句话说,一个事件在同一个层级的子 view 之间只能由其中一个来消费,而无法互相流通。

至于为什么上面提到的,如果事件给了 B 消耗后,会触发 A 的 longPress 事件的话,粗略翻看了一下源码,发现 GestureDetector 判断是否是长按事件是这样做的:action_down 的时候会延迟 500ms 发出一个 message。如果 500ms 后没有收到来自 parent 的 cancel 事件来打断这个 message,那么就判断此时是长按事件。我们的场景里的话,估计就是事件序列都跑到 B 里头去了,没有人再来管这个 message,就造成 A 误会自己是被长按了。