安卓自定义View进阶-事件分发机制详解
Android 事件分发机制详解,在上一篇文章 事件分发机制原理 中简要分析了一下事件分发机制的原理,原理是十分简单的,一句话就能总结:责任链模式,事件层层传递,直到被消费。 虽然原理简单,但是随着 Android 不断的发展,实际运用场景也越来越复杂,所以想要彻底玩转事件分发机制还需要一定技巧,本篇事件分发机制详解将带大家了解 …
你以为我接下来要讲源码?
我就不按套路,所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。所以本篇的核心问题是:正确理解在实际场景中事件分发机制的作用。 会涉及到源码,但不是主角。
注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。
常见事件
既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。
根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:
事件 | 简介 |
---|---|
ACTION_DOWN | 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会会多次触发。 |
ACTION_UP | 手指 离开屏幕 时触发。 |
ACTION_CANCEL | 事件 被上层拦截 时触发。 |
对于单指触控来说,一次简单的交互流程是这样的:
手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)
本次事例中 ACTION_MOVE 有多次触发。
如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
事件分发、拦截与消费
关于这一部分内容,上一篇文章 事件分发机制原理 已经将流程整理的比较清楚了,本文会深入细节来研究这些内容。之所以分开讲,是为了防止大家被细节所迷惑而忽略了整体逻辑。
√
表示有该方法。
X
表示没有该方法。
类型 | 相关方法 | ViewGroup | View |
---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ |
事件拦截 | onInterceptTouchEvent | √ | X |
事件消费 | onTouchEvent | √ | √ |
View 相关
dispatchTouchEvent
是事件分发机制中的核心,所有的事件调度都归它管。不过我细看表格, ViewGroup 有 dispatchTouchEvent 也就算了,毕竟人家有一堆 ChildView 需要管理,但为啥 View 也有?这就引出了我们的第一个疑问。
Q: 为什么 View 会有 dispatchTouchEvent ?
A: 我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent
,所以 View 也会有事件分发。
相信看到这里很多小伙伴会产生第二个疑问,View 有这么多事件监听器,到底哪个先执行?
Q: 与 View 事件相关的各个方法调用顺序是怎样的?
A: 如果不去看源码,想一下让自己设计会怎样?
-
单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
-
长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
-
触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
-
View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)
所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
。
下面我们来看一下实际测试结果:
手指按下,不移动,稍等片刻再抬起。
可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。
接下来就看一下源码是怎么设计的(省略了大量无关代码):
如果觉得源码还是太长,那么用伪代码实现应当是这样的(省略若干安全判断),简单粗暴:
正当你沉迷在源码的”精妙”逻辑的时候,你可能没发现有两个东西失踪了,等回过神来,定睛一看,哎呦妈呀,OnClick 和 OnLongClick 去哪里了?
不要担心,OnClick 和 OnLongClick 的具体调用位置在 onTouchEvent 中,看源码(同样省略大量无关代码):
注意了,第一个重点要出现了(敲黑板)!
注意上面代码中存在一个
return true;
并且是只要 View 可点击就返回 true,就表示事件被消费了。举个栗子: I have a RelativeLayout,I have a View,Ugh,RelativeLayout - View
<RelativeLayout android:background="#CCC" android:id="@+id/layout" android:onClick="myClick" android:layout_width="200dp" android:layout_height="200dp"> <View android:clickable="true" android:layout_width="200dp" android:layout_height="200dp" />
</RelativeLayout>现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件
myClick
,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性android:clickable="true"
正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
关于 View 的事件分发先说这么多,下面我们来看一下 ViewGroup 的事件分发。
ViewGroup 相关
ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。
VIewGroup 的事件分发流程又是如何的呢?
上一篇文章 事件分发机制原理 中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。
-
1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
-
2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
-
3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
用伪代码应该是这样的:
有人看到这里可能会有疑问,我看过源码,ViewGroup 的 dispatchTouchEvent
可有二百多行呢,你弄这几行就想忽悠我,别以为我读书少。
当然了,上述源码是不完善的,还有很多问题是没有解决的,例如:
1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
2. 当该点的 ChildView 有重叠时应该如何分配?
当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。
如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.gcssloop.viewtest.MainActivity">
<View
android:id="@+id/view1"
android:background="#E4A07B"
android:layout_width="200dp"
android:layout_height="200dp"/>
<View
android:id="@+id/view2"
android:layout_margin="100dp"
android:background="#BDDA66"
android:layout_width="200dp"
android:layout_height="200dp"/>
</RelativeLayout>
当手指点击有重叠区域时,分如下几种情况:
-
只有 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 就不会消费事件。
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,表示当前事件已经结束,后续事件不会再传递过来。
源码:
其实如果能够理解上面的内容,不看源码也能非常顺利的使用事件分发,但源码中能挖掘出更多的内容。
核心要点
-
事件分发原理: 责任链模式,事件层层传递,直到被消费。
-
View 的
dispatchTouchEvent
主要用于调度自身的监听器和 onTouchEvent。 -
View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
-
不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
-
事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
-
ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
-
ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
-
一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
-
只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
-
如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。
posted on 2017-05-25 17:33 yuezhusust 阅读(397) 评论(0) 编辑 收藏 举报