欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~

作者: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的对象,如果一个普通的对象想成为第一响应者,只需要做两件事情:

  1. 重写canBecomeFirstResponder方法返回YES
  2. 调用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

参考文献

UIResponder Class Reference

UIResponder Class Reference

Event Handling Guide for iOS

深入浅出iOS事件机制

相关阅读


此文已由作者授权云+社区发布,转载请注明原文出处

IOS 触摸事件分发机制详解的更多相关文章

  1. Android事件分发机制详解

    事件分发机制详解 一.基础知识介绍 1.经常用的事件有:MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP等 2 ...

  2. Android开发——事件分发机制详解

    0. 前言   转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52566965 深入学习事件分发机制,是为了解决在Android开发中 ...

  3. Android事件分发机制详解(2)----分析ViewGruop的事件分发

    首先,我们需要 知道什么是ViewGroup,它和普通的View有什么区别? ViewGroup就是一组View的集合,它包含很多子View和ViewGroup,是Android 所有布局的父类或间接 ...

  4. Android View 事件分发机制详解

    想必很多android开发者都遇到过手势冲突的情况,我们一般都是通过内部拦截和外部拦截法解决此类问题.要想搞明白原理就必须了解View的分发机制.在此之前我们先来了解一下以下三个非常重要的方法: di ...

  5. 【Android面试查漏补缺】之事件分发机制详解

    前言 查漏补缺,查漏补缺,你不知道哪里漏了,怎么补缺呢?本文属于[Android面试查漏补缺]系列文章第一篇,持续更新中,感兴趣的朋友可以[关注+收藏]哦~ 本系列文章是对自己的前段时间面试经历的总结 ...

  6. android 事件分发机制详解(OnTouchListener,OnClick)

    昨天做东西做到触摸事件冲突,以前也经常碰到事件冲突,想到要研究一下Android的事件冲突机制,于是从昨天开始到今天整整一天时间都要了解这方面的知识,这才懂了安卓的触摸和点击事件的机制.探究如下: 首 ...

  7. Android事件分发机制详解(1)----探究View的事件分发

    探究View的事件分发 在Activity中,只有一个按钮,注册一个点击事件 [java] view plaincopy button.setOnClickListener(new OnClickLi ...

  8. Android事件传递机制详解及最新源码分析——ViewGroup篇

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴 ...

  9. Android 的事件传递机制,详解

    Android 的事件传递机制,详解 前两天和一个朋友聊天的时候.然后说到事件传递机制.然后让我说的时候,忽然发现说的不是非常清楚,事实上Android 的事件传递机制也是知道一些,可是感觉自己知道的 ...

随机推荐

  1. PKI(公钥基础设施)基础知识笔记

    数字签名 数字签名(又称公钥数字签名.电子签章)是一种类似写在纸上的普通的物理签名,可是使用了公钥加密领域的技术实现.用于鉴别数字信息的方法. 一套数字签名通常定义两种互补的运算.一个用于签名,还有一 ...

  2. nyoj 123 士兵杀敌(四)【树状数组】+【插线问点】

    树状数组有两种情况:插点问线和插线问点.这道题是插线问点. 由于树状数组最简单的作用是计算1~x的和,所以给出(a, b. c).表示(a,b)区间添加c, 那我们仅仅须要在a点原来的基础上添加c,然 ...

  3. (转)java内部类详解

    本文转自http://www.cnblogs.com/dolphin0520/p/3811445.html,谢谢作者 说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能 ...

  4. SignalR简单Demo

    我们实现一个简单的消息通知的Demo 在NuGet中添加SignalR引用 install-package Microsoft.AspNet.SignalR 然后我们创建一个类来引用Hub类 name ...

  5. 什么是JWT(JSON WEB TOKEN)

    转自于:http://www.jianshu.com/p/576dbf44b2ae 什么是JWT Json web token(JWT)是为了网络应用环境间传递声明而执行的一种基于JSON的开发标准( ...

  6. 《深入浅出node.js(朴灵)》【PDF】下载

    <深入浅出node.js(朴灵)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062563 内容简介 <深入浅出Node. ...

  7. 《深入理解Java虚拟机:JVM高级特性与最佳实践》【PDF】下载

    <深入理解Java虚拟机:JVM高级特性与最佳实践>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062566 内容简介 作为一位 ...

  8. 58、js扩展

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理. 一.js的作用域 任何程序设计语言都有作用域的概念,简单的说,作用 ...

  9. Java零碎总结

    获取当前类运行的根目录(即classpath,如bin.classes.AppName等)的方式有: 1.Thread.currentThread().getContextClassLoader(). ...

  10. 小白的Python之路 day4 装饰器高潮

    首先装饰器实现的条件: 高阶函数+嵌套函数 =>装饰器 1.首先,我们先定义一个高级函数,去装饰test1函数,得不到我们想要的操作方式 import time #定义高阶函数 def deco ...