简述
顶级View对点击事件的分发:
点击事件达到顶级View(一般是一个 ViewGroup )以后,会调用 ViewGroup的 dispatchTouchEvent 方法,然后的逻辑是这样的:如果顶级 ViewGroup 拦截事件的 onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说,如果都提供的话,onTouch 会屏蔽掉 onTouchEvent 。在 onTouchEvent 中,如果设置了 mOnClickListener,则 onClick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级 View 是一致的,如此循环,完成整个事件的分发。
事件分发、拦截与消费
View 相关
dispatchTouchEvent
是事件分发机制中的核心,所有的事件调度都归它管
我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent
,所以 View 也会有事件分发。
消费:
1 | <RelativeLayout |
现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件myClick
,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 android:clickable="true"
正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
ViewGroup相关
ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突
前面事件分发机制中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。
- 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
- 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
- 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
用伪代码应该是这样的:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
几个点:
1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
2. 当该点的 ChildView 有重叠时应该如何分配?
当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。
3. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。
4. 所有事件都应该被同一 View 消费
在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。
View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发。
安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。
如下:
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
当手指点击有重叠区域时,分如下几种情况:
- 只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。
- 只有 View2 可点击时,事件将会分配给 View2。
- View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。
注意:
- 上面说的是可点击,可点击包括很多种情况,只要你给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。
另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。 - 给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件。