IOS 触摸事件分发机制详解
欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~
作者:MelonTeam
前言
很多时候大家都不关心IOS触摸事件的分发机制的实现原理,当遇到以下几种情形的时候你很可能抓破头皮都找不到解决方案:
某个点击消息由父视图来处理,子视图怎么把消息传递给父视图 这个按钮不灵敏,怎么扩大点击响应区域 怎么在一个页面处理手绘、表情拖动放缩、文本编辑三种消息 阅读本文,你会明白两个问题:IOS如何找到响应者、响应者是如何做出响应,明白这两个问题你就能解决类似上述的疑难杂症。通过控制Hit-test view 、人为干预响应者能否对这一事件作出响应最终来控制触摸事件的分发机制。
原理详解
IOS把用户触发事件打包成一个UIEvent对象,作为事件传递的消息载体,放入当前活跃的APP的消息队列中,然后通过Hit-Testing来找到响应者,响应者通过响应链的传递做出响应,这就是IOS事件分发机制的实现原理。
接下来从这三个概念UIEvent,UIResponder、Hit-Testing、Responder Chain入手,为你详细讲解这句话的含义。
UIEvent
UIEvent包含最常见的三种事件:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机上得按键来控制手机), 通过 type、 subtype属性表明事件类型。IOS把屏幕监测到的点击事件用UITouch对象来表示,最终被封装成UIEvent作为事件的消息载体在响应链上传递。
Hit-Testing
屏幕上有很多UIView,你点击一下屏幕,IOS是怎么知道你点击的是哪个UIView呢?
Hit-Testing就完美的解决了这个问题,通过检测触摸点是否在相关的视图边界范围内,如果在,就继续递归检测该视图的所有子视图,离用户最近的那个视图的边界如果包含触摸点,那么它就是我们要找的Hit-Test view。 举例说明,假如用户点击下图中的 view E,那么IOS是通过如下顺序来找到view E的:
点击在view A的范围内,所以就检测它的子视图 view B和 view C。 点击不在view B内,但是在view C内,所以接下来检测view D和view E 点击不在view D内,而是在view E内,并且view E是在包含点击的视图树中离用户最近的,所以view E就是要找的Hit-Test view。

具体的检测工作是通过UIView中两个方法来完成的
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
hitTest:withEvent: 方法通过传递进来CGPoint和UIEvent返回Hit-Test view,该方法调用 pointInside:withEvent: 方法来检测point是否在view的边界范围内,如果在view的边界范围内,则返回YES,然后,在子视图中递归调用 hitTest:withEvent: 。如果不在范围内,则返回NO,那么它的所有子视图都会被忽略,hitTest:withEvent:返回 nil 。
Hit-Test view只是有权优先处理该事件,如果它不能处理那么事件消息就会沿着响应链传递给下一个响应者来处理。所以能通过控制 Hit-Test view 和 能否响应两个途径来控制消息的传递和处理。
UIResponder
UIResponder 类提供了一组接口专门用来响应用户的操作,处理各种事件,其中包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events),标准文本编辑事件(Standard Edit Actions)如:复制、选择、粘贴、剪切等。在UIKit中,UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类
第一响应者(first responder)
第一响应者能够优先处理事件,通常是一个UIView的对象,如果一个普通的对象想成为第一响应者,只需要做两件事情:
- 重写
canBecomeFirstResponder方法返回YES - 调用
becomeFirstResponder
提示:当一个对象变成第一响应者的时候,要确保APP已经建立了object graph(暂且翻译为”对象图“),举例说明,你可以在viewDidAppear: 调用becomeFirstResponder,如果你在viewWillAppear:中调用这个方法可能会返回NO。
触摸事件接口
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);
运动事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
标准编辑事件
@implementation TBExtendedHitButton
+ (instancetype)extendedHitButton
{
return (TBExtendedHitButton *)[TBExtendedHitButton buttonWithType:UIButtonTypeCustom];
} - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-, -, -, -);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
Responder Chain
Responder Chain 暂且翻译为“响应链”,它是由一些列的响应者(UIResponder)链接起来的,起始于第一响应者(first responder),结束于UIApplication,当第一响应者(first responder)不能处理该事件的时候,事件消息沿着响应链继续转发。响应链能为一下几种事件进行消息转发,但不仅限于一下几类事件类型:
触摸事件(Touch Events) 运动事件(Motion Events) 远程控制事件(Remote Control Events) 耳机等 control事件(Action messages),UIBUtton,UISwitch等 编辑菜单事件(Editing-menu messages)复制、粘贴、剪切等 文本控件编辑事件(Text editing),UITextView、UITextfiled等
传递路径
如果初始化对象(initial object 即hit-test view或者first responder)不处理事件,UIKit会将事件传递给响应链中的下一个响应者。每个响应者决定它是传递事件还是通过nextResponder方法传递给它的下一个响应者。这个操作继续直到一个响应者处理该事件或者没有响应者了。
响应链序列在iOS确定一个事件并将它传递给initial object(通常是view)时开始。所以initial view有处理事件的第一个机会。 下图描述了两个不同的事件传递路径(因为不同的app设置),一个App的事件传递路径由app特殊的构成决定,但事件传递路径会遵守相同的规则。以下图片很能说明响应链是如何传递的。

应用
扩大按钮点击区域
当视图调用 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 进行边界检测的时候,重写该方法扩大视图的检测边界值。
@implementation TBExtendedHitButton
+ (instancetype)extendedHitButton
{
return (TBExtendedHitButton *)[TBExtendedHitButton buttonWithType:UIButtonTypeCustom];
} - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-, -, -, -);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
@end
子视图消息传递给父视图
解决办法通常有两种:
父视图和子视图都重写- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;,其中子视图返回nil,让父视图成为Hit-Test view. 父视图成为first responder,子视图把事件沿着响应链转发。 更多应用解决方案,请参考http://zhoon.github.io/ios/2015/04/12/ios-event.html
参考文献
相关阅读
此文已由作者授权云+社区发布,转载请注明原文出处
IOS 触摸事件分发机制详解的更多相关文章
- Android事件分发机制详解
事件分发机制详解 一.基础知识介绍 1.经常用的事件有:MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP等 2 ...
- Android开发——事件分发机制详解
0. 前言 转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52566965 深入学习事件分发机制,是为了解决在Android开发中 ...
- Android事件分发机制详解(2)----分析ViewGruop的事件分发
首先,我们需要 知道什么是ViewGroup,它和普通的View有什么区别? ViewGroup就是一组View的集合,它包含很多子View和ViewGroup,是Android 所有布局的父类或间接 ...
- Android View 事件分发机制详解
想必很多android开发者都遇到过手势冲突的情况,我们一般都是通过内部拦截和外部拦截法解决此类问题.要想搞明白原理就必须了解View的分发机制.在此之前我们先来了解一下以下三个非常重要的方法: di ...
- 【Android面试查漏补缺】之事件分发机制详解
前言 查漏补缺,查漏补缺,你不知道哪里漏了,怎么补缺呢?本文属于[Android面试查漏补缺]系列文章第一篇,持续更新中,感兴趣的朋友可以[关注+收藏]哦~ 本系列文章是对自己的前段时间面试经历的总结 ...
- android 事件分发机制详解(OnTouchListener,OnClick)
昨天做东西做到触摸事件冲突,以前也经常碰到事件冲突,想到要研究一下Android的事件冲突机制,于是从昨天开始到今天整整一天时间都要了解这方面的知识,这才懂了安卓的触摸和点击事件的机制.探究如下: 首 ...
- Android事件分发机制详解(1)----探究View的事件分发
探究View的事件分发 在Activity中,只有一个按钮,注册一个点击事件 [java] view plaincopy button.setOnClickListener(new OnClickLi ...
- Android事件传递机制详解及最新源码分析——ViewGroup篇
版权声明:本文出自汪磊的博客,转载请务必注明出处. 在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴 ...
- Android 的事件传递机制,详解
Android 的事件传递机制,详解 前两天和一个朋友聊天的时候.然后说到事件传递机制.然后让我说的时候,忽然发现说的不是非常清楚,事实上Android 的事件传递机制也是知道一些,可是感觉自己知道的 ...
随机推荐
- LintCode-落单的数 III
给出2*n + 2个的数字.除当中两个数字之外其它每一个数字均出现两次,找到这两个数字. 您在真实的面试中是否遇到过这个题? Yes 例子 给出 [1,2,2,3,4,4,5,3].返回 1和5 挑战 ...
- Implement Queue using Stacks(用栈实现队列)
Implement the following operations of a queue using stacks. push(x) -- Push element x to the back of ...
- PMP杂谈--PMP中一些easy忽视的地方
识别干系人:这个过程是持续的,在整个项目的生命周期中都要持续识别干系人. 组织过程资产和事业环境因素:这两个东西在过程的输入中似乎常常看到,但有时候又看不到,不要纠结了 ,不要浪费脑细胞去背诵哪个有, ...
- java 线程 捕获异常
java 线程 捕获异常 来自:thinking in java 4 文件夹20.2.13 package org.rui.thread.concurrent; import java.util.c ...
- UVA 11324 The Largest Clique(强连通分量+缩点DAG的DP)
题意:给定一个有向图,求出一个最大的结点集,这个节点集中的随意两个点之间至少一个能到达还有一个点. 思路:假设一个点在这个节点集中,那么它所在的强连通分量中的点一定所有在这个节点集中,反之亦然, 求出 ...
- redis基础(一)
redis是一种流行的非关系内存型数据库,拥有非常高的读写性能,下面是本人学习的总结. redis的类型 键:redis的所有的键都是string类型: 值:五种类型 string:字符串类型:一个s ...
- 关于MySql中使用IFNULL()函数失效的问题。
今天在学习时,碰到一个问题:在联表查询取得结果后,如果取得的结果是空值,则给一个默认值,如果不是空值,则返回这个值. 下面我们来看看业务场景: 在menu表中: 存储的是前端页面的菜单配置,注意成员权 ...
- springboot 项目maven 打包错误
Execution default of goal org.springframework.boot:spring-boot-maven-plugin:1.5.6.RELEASE:repackage ...
- 第一安装oracle数据库后,需要创建一个用户,给用户解锁并赋予权限
1.第一次安装oracle数据库应该做的事情. 注: 1.安装oracle后需要创建用户,连接数据库,(注意数据库名,还有好像后面的 ":"也有影响) 2.解锁用户, 3.授予新登 ...
- Html 段落自动换行
1.段落换行 在 Html 中,关于段落换行,是我们经常遇见的问题,那么正如我下图没加换行代码所示: 在网页上的显示的样式,是这样的: 你可以看见,原本的样式,不会自动换行.在 div 中,加入一个样 ...