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

  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(-15, -15, -15, -15);
    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(-15, -15, -15, -15);
    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 RunLoop的介绍

ios 电量测试实践

ios 逻辑自动化测试实践



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

原文地址:https://www.cnblogs.com/qcloud1001/p/8276110.html

时间: 2024-08-01 22:48:00

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

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

探究View的事件分发 在Activity中,只有一个按钮,注册一个点击事件 [java] view plaincopy button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "onClick execute"); } }); 如果在需要一个触摸事件 [java] view plaincopy button.setO

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

首先,我们需要 知道什么是ViewGroup,它和普通的View有什么区别? ViewGroup就是一组View的集合,它包含很多子View和ViewGroup,是Android 所有布局的父类或间接父类. 但ViewGroup也是一个View,只不过比起View,它可以包含子View和定义布局参数的功能. 现在,通过一个Demo演示Android中ViewGroup的事件分发机制. 首先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下 所示: public c

Cocos2d-X研究之v3.x 事件分发机制详解

事件分发机制 新事件分发机制:在2.x 版本事件处理时,将要触发的事件交给代理(delegate)处理,再通过实现代理里面的onTouchBegan等方法接收事件,最后完成事件的响应.而在新的事件分发机制中,只需通过创建一个事件监听器-用来实现各种触发后的逻辑,然后添加到事件分发器_eventDispatcher,所有事件监听器由这个分发器统一管理,即可完成事件响应.请参考更多3.0资料... 事件监听器有以下几种: 触摸事件 (EventListenerTouch) 键盘响应事件 (Event

Android事件分发机制详解:史上最全面、最易懂

前言 Android事件分发机制是每个Android开发者必须了解的基础知识 网上有大量关于Android事件分发机制的文章,但存在一些问题:内容不全.思路不清晰.无源码分析.简单问题复杂化等等 今天,我将全面总结Android的事件分发机制,我能保证这是市面上的最全面.最清晰.最易懂的 本文秉着"结论先行.详细分析在后"的原则,即先让大家感性认识,再通过理性分析从而理解问题: 所以,请各位读者先记住结论,再往下继续看分析: 文章较长,阅读需要较长时间,建议收藏等充足时间再进行阅读 目

Android 事件分发机制详解

更多内容请参照我的个人站点: http://stackvoid.com/ 网上很多关于Android事件分发机制的解释,大多数描述的都不够清晰,没有吧来龙去脉搞清楚,本文将带你从Touch事件产生到Touch事件被消费这一全过程作全面的剖析. 产生Touch事件 这部分牵扯到硬件和Linux内核部分:我们简单讲述一下这部分内容,如果有兴趣的话可以参考这篇文章. 传递Touch事件 触摸事件是由Linux内核的一个Input子系统来管理的(InputManager),Linux子系统会在/dev/

Cocos2d-X 3.x 事件分发机制详解

事件分发机制 新事件分发机制:在2.x 版本事件处理时,将要触发的事件交给代理(delegate)处理,再通过实现代理里面的onTouchBegan等方法接收事件,最后完成事件的响应.而在新的事件分发机制中,只需通过创建一个事件监听器-用来实现各种触发后的逻辑,然后添加到事件分发器_eventDispatcher,所有事件监听器由这个分发器统一管理,即可完成事件响应.请参考更多3.0资料... 事件监听器有以下几种: 触摸事件 (EventListenerTouch) 键盘响应事件 (Event

Android6.0 ViewGroup/View 事件分发机制详解

之前自认为对于Android的事件分发机制还算比较了解,直到前一阵偶然跟人探讨该问题,才发现自己以前的理解有误,惭愧之余遂决定研习源码,彻底弄明白Android的事件分发机制,好了废话少说,直接开干. 首先,我们对Android中的touch事件做一下总结,主要分为以下几类: 1.Action_Down  用户手指触碰到屏幕的那一刻,会触发该事件: 2.Action_Move   在触碰到屏幕之后,手指开始在屏幕上滑动,会触发Action_Move事件: 3.Action_Up       在用

Android事件分发机制详解

我们通过一个示例来分析Touch事件的分发过程. 示例: 布局文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id=&

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

昨天做东西做到触摸事件冲突,以前也经常碰到事件冲突,想到要研究一下Android的事件冲突机制,于是从昨天开始到今天整整一天时间都要了解这方面的知识,这才懂了安卓的触摸和点击事件的机制.探究如下: 首先重写三个View布局,用来做测试: package com.example.yzj.android_8_10; import android.content.Context; import android.util.AttributeSet; import android.util.Log; im