前言

试想一下假如你是一台手机,当有人触摸屏幕之后,你需要找到他具体触摸什么东西,他可能触摸一个按钮,或一个列表,也有可能一个一不小心的误触,你会设计一个怎么样的机制系统处理呢?假如两个按钮重叠了,或者遇到滚动列表需要拖动某个按钮的情况,你设计机制能正常的运作嘛?在 iOS 中系统通过 UIKit 已经为我们设计好了一套方案,也是本文浅谈的内容: iOS 中的事件传递响应机制

事件产生

下图所示点击屏幕时,首先UIApplication对象收到点击事件,再依次传递给它上面的所有子view,直到传递到最上层,即UIApplication——>UIWindow——>RootViewController——>View——>Button,即传递链。而反之Button——>View——>RootViewController——>UIWindow——>UIApplication则为响应链。简单总结,事件链包含传递链和响应链,事件通过传递传递下去,通过响应找到相应的UIResponse
在这里插入图片描述

UIResponder的点击事件

自定义UIView基类控件时,我们可以重写这几个方法来进行点击回调。在回调中,我们可以看到方法接收两个参数一个UITouch对象集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象

事件对象

iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到一个UIEventType类型属性这个属性表示当前响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一

点击对象

UITouch表示单个点击,其类文件存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动停止不动、结束取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder回调方法中,我们通过集合中对象获取用户点击的位置。其中通过- (CGPoint)locationInView:(nullable UIView *)view获取当前点击坐标点,- (CGPoint)previousLocationInView:(nullable UIView *)view获取上个点击位置坐标点。

传递链

传递链: Application -> window -> root view -> … -> first view
UIResponse:响应对象的基类定义事件处理接口
常见子类UIViewUIViewControllerUIApplication以及所有继承UIViewUIKit类都直接或间接的继承UIResponder

在这里插入图片描述

hitTest

hitTest作用:当在一个view上添加一屏蔽罩,但又不影响对下面view操作,也就是可以透过屏蔽罩对下面的view进行操作

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

}

调用过程

iOS系统检测手指触摸(Touch操作时会将其放入当前活动应用程序事件队列UIApplication会从事件队列取出触摸事件并传递给关键窗口(当前接收用户事件的窗口)处理,窗口对象首先会使用hitTest:withEvent:方法寻找此次触摸操作初始点所在的视图View),即需要触摸事件传递给其处理的视图,称之为hit-test视图

hitTest:withEvent:方法方法的处理流程如下

hitTest:withEvent:方法忽略隐藏hidden = YES)的视图,禁止用户操作userInteractionEnabled = YES)的视图,以及alpha级别小于0.01alpha <0.01)的视图。如果一个子视图的区域超过父视图的约束区域(父视图的clipsToBounds 属性为NO,这样超过父视图绑定区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法。方法会返回NO,这样就不会继续向下遍历子视图了当然,也可以重写pointInside:withEvent:方法方法来处理这种情况。

对于每个触摸操作都会有一个UITouch对象,UITouch对象用来表示一个触摸操作,即一个手指屏幕按下移动,离开的整个过程UITouch对象在触摸操作的过程中在不断变化,所以在使用UITouch对象时,不能直接保留,而需要使用其他手段存储UITouch内部信息UITouch对象有一个视图属性,表示此触摸操作初始发生所在的视图,即上面检测到的命中测试视图,此属性在UITouch生命周期不再改变,即使触摸操作后续移动到其他视图之上。

pointInside

  • 这个函数的用处是判断当前的点击或者触摸事件的点是否在当前的view中。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
}

它被hitTest:withEvent:调用,通过对每个子视图调用pointInside:withEvent:决定最终哪个视图来响应此事件。如果 pointInside:withEvent:返回YES然后子视图的继承树就会被遍历遍历顺序中最先响应的为:与用户最接近的那个视图。 it starts from the toplevel subview),即子视图的子视图继续调用递归这个函数,直到找到可以响应的子视图(这个子视图的hitTest:withEvent:会返回self,而不是nil);否则,视图的继承树就会被忽略。

响应链

响应链:由离用户最近的view系统传递。如下所示

在这里插入图片描述

谁来响应事件

UIKit 中我们使用响应者对象(Responder)接收和处理事件。一个响应者对象一般是 UIResponder 类的实例,它常见的子类包括 UIViewUIViewControllerUIApplication,这意味着几乎所有我们日常使用控件都是响应者,如 UIButtonUILabel 等等。

  • UIResponder 及其子类中,我们是通过有关触摸(UITouch)的方法来处理和传递事件(UIEvent

UIResponder 还可以处理 UIPress、加速计、远程控制事件,这里讨论触摸事件。

UITouch 内,存储了大量触摸相关数据,当手指屏幕移动时,所对应UITouch 数据也会更新例如这个触摸是在哪个 window 或者哪个 view 内发生的?当前触摸点的坐标是?前一个触摸点的坐标是?当前触摸事件的状态是?这些都存储UITouch 里面。另外需要注意的是,在这四个方法的参数中,传递的是 UITouch 类型的一个集合(而不是一个 UITouch),这对应了两根及以上手指触摸同一个视图的情况。

第一响应者

当有人用触摸了屏幕之后,我们需要找到使用者到底触摸了一个什么东西,或者可以理解为我们要找到,在这次使用者触摸之后,使用者最想要哪个控件发起响应。这个过程就是确定这次触摸事件的第一响应者是谁。

这里我们使用 UIView 来作为视图层级的主要组成元素,便于理解。但不止 UIView 可以响应事件,实际只要是 UIResponder 的子类,都可以响应和传递事件。

在这里插入图片描述
回到开头问题,我现在变成了一台手机,并且我知道有人触摸了屏幕。我所拥有的信息是触摸点的坐标,我知道应该就是图层级中其中的某一个,但我无法直接知道用户是想点哪个视图。我需要一个策略来找到这个第一响应者,UIKit 为我们提供了命中测试(hit-test)来确定触摸事件的响应者,这个策略具体是这样运作的:

在这里插入图片描述

注意:

  • 检查自身可否接收事件 中,如果视图符合以下三个条件中的任一个,都会无法接收事件:
view.isUserInteractionEnabled = false
view.alpha <= 0.01
view.isHidden = true

原文地址:https://blog.csdn.net/kochunk1t/article/details/125134149

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_36290.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注