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 的事件传递机制也是知道一些,可是感觉自己知道的 ...
随机推荐
- iOS8 UILocalNotification 添加启动授权
猴子原创.欢迎转载.转载请注明: 转载自Cocos2Der-CSDN,谢谢! 原文地址: http://blog.csdn.net/cocos2der/article/details/46810357 ...
- Fiddler使用总结一(使用Fiddler捕获手机所有http/https通信)
与后端数据通信是前端日常开发的重要一环,在与后端接口联调的时候往往需要通过查看后端返回的数据进行调试.如果在PC端,Chrome自带的DevTools就已经足够用了,Network面板可以记录所有网络 ...
- 前端笔记---塌陷top
一.在设置盒子div的子元素的外边框margin-top,子元素属性不起作用,父元素下沉: <!DOCTYPE html> <html lang="en"> ...
- 用IFeatureWorkspaceAnno.CreateAnnotationClass 创建注记图层时报“The application is not licensed to modify or create schema”的错误的解决方案。
用IFeatureWorkspaceAnno.CreateAnnotationClass 的方法创建注记图层的时候报"The application is not licensed to m ...
- JPA 单向一对多关联关系
映射单向一对多的关联关系 1.首先在一的一端加入多的一端的实体类集合 2.使用@OneToMany 来映射一对多的关联关系3.使用@JoinColumn 来映射外键列的名称4.可以使用@OneToMa ...
- CentOS6.9下安装rabbitmq消息队列
安装如下步骤: 首先安装erlang yum install erlang 安装rabbitmq rpm包 wget http://www.rabbitmq.com/releases/rabbitmq ...
- DWR3.0 服务器推送及解惑
前言:在慕课网上学习一下服务器推送给客户端技术,代码亲测过,没毛病,今天整理记录一下: 一.环境搭建 直接上图,简单粗暴,myeclipse上file->new->WebProject 二 ...
- 【java】HashSet
package com.tn.hashSet; public class Person { private int id; private String name; private String bi ...
- iOS手机截屏使用
.截屏 保存 .data //登录成功进行截屏 //截取屏幕大小 UIGraphicsBeginImageContext([[UIScreen mainScreen]bounds].size); [s ...
- IDS 源镜像端口添加
把核心交换机的G1/2口镜像到目的交换机的G1/4口,两个交换机之间都是连接的24口 1.核心交换机配置 Ruijie# configure tRuijie(config)# vlan 77Ruiji ...