一、事件分发机制详解:

大佬名言:所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。

核心问题是:正确理解在实际场景中事件分发机制的作用。

常见事件

事件 简介
ACTION_DOWN 手指 初次接触到屏幕 时触发。
ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
ACTION_UP 手指 离开屏幕 时触发。
ACTION_CANCEL 事件 被上层拦截 时触发。

单次触发:

手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)

事件分发、拦截与消费

类型 相关方法 ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent X
事件消费 onTouchEvent

Q: 为什么 View 会有 dispatchTouchEvent ?

A: 我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent,所以 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

源码是怎么设计的:

public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;    // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;/**
* 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
* 如果 onTouch 方法返回值为 true,就设置 result 为 true。
*/
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/**
* 如果 result 为 false,则调用自身的 onTouchEvent。
* 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
*/
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}

OnClick、OnLongClick在onTouchEvent中:

public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();// 检查各种 clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
removeLongPressCallback(); // 移除长按
...
performClick(); // 检查单击
...
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0); // 检测长按
...
break;
...
}
return true; // ◀︎表示事件被消费
}
return false;
}

view只要默认接受click事件就会消费掉,不会传递给他的父控件。

ViewGroup 相关

ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。

默认分发流程:

  • 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。

  • 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。

  • 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
result = child.dispatchTouchEvent(ev);
}if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
}return result;
}

ChildView重叠的解决策略:

1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?

这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。

2. 当该点的 ChildView 有重叠时应该如何分配?

当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。

如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。

当手指点击有重叠区域时,分如下几种情况:

  1. 只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。

  2. 只有 View2 可点击时,事件将会分配给 View2。

  3. View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。

注意:

  • 上面说的是可点击,可点击包括很多种情况,只要你给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。

    另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。

  • 给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件。

所有事件都应该被同一 View 消费

在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。

View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发。

安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。

如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。

源码:

public boolean dispatchTouchEvent(MotionEvent ev) {// 调试用
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}// 判断事件是否是针对可访问的焦点视图(很晚才添加的内容,个人猜测和屏幕辅助相关,方便盲人等使用设备)
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;// 处理第一次ACTION_DOWN.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 清除之前所有的状态
cancelAndClearTouchTargets(ev);
resetTouchState();
}// 检查是否需要拦截.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);    // 询问是否拦截
ev.setAction(action);                         // 恢复操作,防止被更改
} else {
intercepted = false;
}
} else {// 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。
intercepted = true;
}// 判断事件是否是针对可访问的焦点视图
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}// 检查事件是否被取消(ACTION_CANCEL).
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;// 如果没有取消也没有被拦截    (进入事件分发)
if (!canceled && !intercepted) {// 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。// 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。
// 我们检测并避免保持这种状态,因为这些事非常罕见。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;// 清除此指针ID的早期触摸目标,防止不同步。
removePointersFromTouchTargets(idBitsToAssign);final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);    // 获取触摸位置坐标
final float y = ev.getY(actionIndex);
// 查找可以接受事件的 ChildView
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;// ▼注意,从最后向前扫描
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);// 如果有一个视图具有可访问性焦点,我们希望它首先获取事件,// 如果不处理,我们将执行正常的分派。// 尽管这可能会分发两次,但它能保证在给定的时间内更安全的执行。
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}// 检查View是否允许接受事件(即处于显示状态(VISIBLE)或者正在播放动画)// 检查触摸位置是否在View区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}// getTouchTarget 中判断了 child 是否包含在 mFirstTouchTarget 中// 如果有返回 target,如果没有返回 null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// ChildView 已经准备好接受在其区域内的事件。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;    // ◀︎已经找到目标View,跳出循环
}resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}if (newTouchTarget == null && mFirstTouchTarget != null) {
// 没有找到 ChildView 接收事件
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}// 分发 TouchTarget
if (mFirstTouchTarget == null) {
// 没有 TouchTarget,将当前 ViewGroup 当作普通的 View 处理。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 分发TouchTarget,如果我们已经分发过,则避免分配给新的目标。// 如有必要,取消分发。
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}// 如果需要,更新指针的触摸目标列表或取消。
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

核心要点

  1. 事件分发原理: 责任链模式,事件层层传递,直到被消费。

  2. View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。

  3. View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。

  4. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。

  5. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。

  6. ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。

  7. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。

  8. 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。

  9. 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。

  10. 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

网络测速全解析之一:自定义View基础知识(八)相关推荐

  1. Linux Command iperf3网络测速工具

    Linux Command iperf3网络测速工具 文章目录 Linux Command iperf3网络测速工具 1. 简介 2. 安装 3. 功能 4. 参数 5. 示例 5.1 测试TCP吞吐 ...

  2. Python3,6行代码,搞定网络测速神器,我直接卸载某60测速器。

    6行代码搞定网络测速器 1.引言 2.代码实战 2.1 介绍 2.1.1 定义 2.1.2 常用方法 2.1.3 功能 2.2 安装 2.3 示例 2.3.1 测试上传下载速度 2.3.2 测试延迟 ...

  3. 查看网络抖动_Linux下3种常用的网络测速工具

    大家好,我是良许. 不管你用的是什么操作系统,网速都是你非常关心的一个性能指标,毕竟,谁都不想看个视频结果网速卡到你怀疑人生.本文介绍三个 Linux 命令行下的网络测速工具,让你随时随地知道你的网络 ...

  4. WiFi网络测速专业版

    WiFi网络测速专业版 应用隐私政策 尊敬的用户: WiFi网络测速专业版 应用是由 北京微言科技有限公司 (以下简称 " 微言 " )为您提供的一款 手机网络测速软件 . &qu ...

  5. [HTML] HTML简单实现网络测速

    1. 简单实现网络测速: 实现原理:通过系统服务器上的图片,来计算出当前访问服务的带宽: 注:准确性就不好说了,做此功能也只是想简单的显示给用户参考,请求的系统服务器的当前带宽! <!DOCTY ...

  6. 网络测速linux,Linux系统下的网络带宽测速

    网络测速 speedtest 下载通过直接下载SpeedTest脚本,给权限运行脚本即可 [root@localhost ~]# curl -o speedtest-cli https://raw.g ...

  7. 网络测速服务器OpenSpeedTest

    什么是 OpenSpeedTest ? OpenSpeedTest 是一个跨平台的互联网速度测试应用程序.因此,您可以在不同操作系统中的各种网络浏览器中测试您的互联网速度,而无需安装任何其他应用程序或 ...

  8. Linux网络测速工具Speedtest

    安装speedtest-cli yum install python-pip –y pip install speedtest-cli 执行网络测速 speedtest #执行结果 Retrievin ...

  9. mac brew 测速 软件_最好用的网络测速工具speedtest

    你可以通过浏览器打开网站 http://www.speedtest.net/ 在线进行测速当上网速度很慢的时候,人们想到的第一件事就进行网络测速.在window上,只要你安装了360全家桶,测速功能就 ...

最新文章

  1. android实现华为手机拍照上传_继续引领手机拍照 华为将带来液态镜头
  2. PM2 进程管理工具使用总结
  3. json 数据 生成 图表_CAPP工艺图表 / 知识重用 快速编制
  4. 解决Red hat 5.4的中文问题
  5. svn版利用什么技术实现_金葱粉涂料印花利用了什么技术?
  6. 串口工具securecrt_SecureCRT配置华为交换机部分命令
  7. 瓦片经纬度及行列号转换_ArcGIS根据最大最小坐标换算瓦片行列号
  8. python sort dict 总结
  9. 5.3 Date类型
  10. java 二分_java二分查找算法
  11. 基于matlab的数字图像处理---图像的锐化与边缘提取
  12. 前端-微信浏览器无法下载附件解决方法?
  13. GoldenDict和主流英语词典产品
  14. 无限循环 for(;;) 与 while(true) 的区别
  15. 用java做考试管理系统,考试管理系统的开发实现(Java+Web)
  16. sql server无法用sql server身份验证
  17. PTA L2-039 清点代码库
  18. Element级联菜单省市json数据
  19. 高中数学:三角函数的周期与值域
  20. 运放专题:运放输出电压

热门文章

  1. JAY和ZOOM,还有铁面人究竟是谁?
  2. LeetCode/LintCode 题解丨一周爆刷双指针: 两数之和
  3. char, unsigned char, int,unsigned int之间的相互转换
  4. c++#学生平均成绩,学号排序
  5. Linux CRDA(Central Regulatory Domain Agent)
  6. 移动端浏览器隐私模式/无痕模式使用本地存储localStorage/sessionStorage的问题
  7. 基于微信小程序房屋出租民宿预定app设计
  8. WPS无法关闭excel表格,提示:关闭窗口前请先退出编辑单元格内容或格式
  9. 微信云开发配置自有域名(短信跳转小程序)
  10. Python实现门禁管理系统(源码)