网络测速全解析之一:自定义View基础知识(八)
一、事件分发机制详解:
大佬名言:所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。
核心问题是:正确理解在实际场景中事件分发机制的作用。
常见事件
事件 | 简介 |
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。
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。
当手指点击有重叠区域时,分如下几种情况:
只有 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 就不会消费事件。
所有事件都应该被同一 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;
}
核心要点
事件分发原理: 责任链模式,事件层层传递,直到被消费。
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,后续事件不会再传递过来。
网络测速全解析之一:自定义View基础知识(八)相关推荐
- Linux Command iperf3网络测速工具
Linux Command iperf3网络测速工具 文章目录 Linux Command iperf3网络测速工具 1. 简介 2. 安装 3. 功能 4. 参数 5. 示例 5.1 测试TCP吞吐 ...
- 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 测试延迟 ...
- 查看网络抖动_Linux下3种常用的网络测速工具
大家好,我是良许. 不管你用的是什么操作系统,网速都是你非常关心的一个性能指标,毕竟,谁都不想看个视频结果网速卡到你怀疑人生.本文介绍三个 Linux 命令行下的网络测速工具,让你随时随地知道你的网络 ...
- WiFi网络测速专业版
WiFi网络测速专业版 应用隐私政策 尊敬的用户: WiFi网络测速专业版 应用是由 北京微言科技有限公司 (以下简称 " 微言 " )为您提供的一款 手机网络测速软件 . &qu ...
- [HTML] HTML简单实现网络测速
1. 简单实现网络测速: 实现原理:通过系统服务器上的图片,来计算出当前访问服务的带宽: 注:准确性就不好说了,做此功能也只是想简单的显示给用户参考,请求的系统服务器的当前带宽! <!DOCTY ...
- 网络测速linux,Linux系统下的网络带宽测速
网络测速 speedtest 下载通过直接下载SpeedTest脚本,给权限运行脚本即可 [root@localhost ~]# curl -o speedtest-cli https://raw.g ...
- 网络测速服务器OpenSpeedTest
什么是 OpenSpeedTest ? OpenSpeedTest 是一个跨平台的互联网速度测试应用程序.因此,您可以在不同操作系统中的各种网络浏览器中测试您的互联网速度,而无需安装任何其他应用程序或 ...
- Linux网络测速工具Speedtest
安装speedtest-cli yum install python-pip –y pip install speedtest-cli 执行网络测速 speedtest #执行结果 Retrievin ...
- mac brew 测速 软件_最好用的网络测速工具speedtest
你可以通过浏览器打开网站 http://www.speedtest.net/ 在线进行测速当上网速度很慢的时候,人们想到的第一件事就进行网络测速.在window上,只要你安装了360全家桶,测速功能就 ...
最新文章
- android实现华为手机拍照上传_继续引领手机拍照 华为将带来液态镜头
- PM2 进程管理工具使用总结
- json 数据 生成 图表_CAPP工艺图表 / 知识重用 快速编制
- 解决Red hat 5.4的中文问题
- svn版利用什么技术实现_金葱粉涂料印花利用了什么技术?
- 串口工具securecrt_SecureCRT配置华为交换机部分命令
- 瓦片经纬度及行列号转换_ArcGIS根据最大最小坐标换算瓦片行列号
- python sort dict 总结
- 5.3 Date类型
- java 二分_java二分查找算法
- 基于matlab的数字图像处理---图像的锐化与边缘提取
- 前端-微信浏览器无法下载附件解决方法?
- GoldenDict和主流英语词典产品
- 无限循环 for(;;) 与 while(true) 的区别
- 用java做考试管理系统,考试管理系统的开发实现(Java+Web)
- sql server无法用sql server身份验证
- PTA L2-039 清点代码库
- Element级联菜单省市json数据
- 高中数学:三角函数的周期与值域
- 运放专题:运放输出电压
热门文章
- JAY和ZOOM,还有铁面人究竟是谁?
- LeetCode/LintCode 题解丨一周爆刷双指针: 两数之和
- char, unsigned char, int,unsigned int之间的相互转换
- c++#学生平均成绩,学号排序
- Linux CRDA(Central Regulatory Domain Agent)
- 移动端浏览器隐私模式/无痕模式使用本地存储localStorage/sessionStorage的问题
- 基于微信小程序房屋出租民宿预定app设计
- WPS无法关闭excel表格,提示:关闭窗口前请先退出编辑单元格内容或格式
- 微信云开发配置自有域名(短信跳转小程序)
- Python实现门禁管理系统(源码)