【iOS】—— 响应者链和事件传递链

这篇具有很好参考价值的文章主要介绍了【iOS】—— 响应者链和事件传递链。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


iOS事件链有两条: 事件的响应链Hit-Testing事件的传递链
  • 响应连:
    • 由离用户最近的view向系统传递。
    • initial view –> super view –> …… –> view controller –> window –> Application –> AppDelegate
  • 传递链:
    • 由系统向离用户最近的view传递。
    • UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

【iOS】—— 响应者链和事件传递链

iOS中的三大事件

iOS中的事件类型:

  • 触摸事件(手机在屏幕上触摸)
  • 加速计事件(手机摇一摇)
  • 远程遥控事件(遥控器控制)

【iOS】—— 响应者链和事件传递链

UIKit继承图

【iOS】—— 响应者链和事件传递链
通过继承图我们能知道,我们平时在使用的UI大多数都是继承自UIResponder的,只有继承自UIResponder的对象才能接收并处理事件,我们把这类对象称为“响应者”。就像UIApplicationUIViewControllerUIView都继承自UIResponder,因此他们都可以接收处理事件,并且UIResponder中提供了三种处理事件的方法(触摸事件、加速计事件、远程控制事件),所以我们才能在UI中实现各种点击事件:

// 触摸事件
// 开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

// 加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

// 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

通过这三种处理事件的方法就可以知道事件的整个过程,我们平时经常使用的就是触摸事件,其中有两个参数,UITouchUIEvent

除了以上方法,现在还有一个类,UIGestureRecognizer,它是一个抽象类,使用它的子类能帮助我们轻松识别view上的各种手势。

UITapGestureRecognizer // 敲击
UIPinchGestureRecognizer // 捏合,用于缩放
UIPanGestureRecognizer // 拖拽
UISwipeGestureRecognizer // 轻扫
UIRotationGestureRecognizer // 旋转
UILongPressGestureRecognizer // 长按

UITouch

当你用一根手指触摸屏幕时,会创建一个与之关联的UITouch对象,一个UITouch对象对应一根手指。在事件中可以根据NSSet中UITouch对象的数量得出此次触摸事件是单指触摸还是双指多指等等。

UITouch几个重要属性
// 触摸产生时所处的窗口
@property(nonatomic, readonly, retain) UIWindow *window;
// 触摸产生时所处的视图
@property(nonatomic, readonly, retain) UIView *view;
// 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic, readonly) NSUInteger tapCount;
// 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic, readonly) NSTimeInterval timestamp;
// 当前触摸事件所处的状态
@property(nonatomic, readonly) UITouchPhase phase;
UITouch的两个方法(可用于view的拖拽)
- (CGPoint)locationInView:(UIView *)view;
/*
  返回值表示触摸在view上的位置
  这里返回的位置是针对传入的view的坐标系(以view的左上角为原点(0, 0))
  调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*/

// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;

UIEvent

每产生一个事件,就会产生一个UIEvent事件,UIEvent称为事件对象,记录事件产生的时刻和类型等等。

UIEvent几个重要属性
// 事件类型
@property(nonatomic, readonly) UIEventType type;
@property(nonatomic, readonly) UIEventSubtype subtype;
// 事件产生的时间
@property(nonatomic, readonly) NSTimeInterval timestamp;

事件的产生与传递

传递链

UIApplication传递事件到当前Window是明确的(即一定会的),接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // 递归调用的事件,从最底层开始,一直往上找,直到找到一个最上层的能响应的视图就返回该视图
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // 判断点击区域是否在该视图的范围中,不在该视图范围中就结束递归,返回nil
传递过程
  • 1.发生触摸事件后,压力转为电信号,系统将产生UIEvent事件,记录事件产生的时间和类型。
  • 2.系统会将该事件添加到UIApplication管理的事件队列中。
  • 3.UIApplication将事件队列中的第一个事件分发给UIWindow,这时就会调用UIWindow的hitTest:withEvent:方法。
  • 4.当前 window/视图 调用hitTest:withEvent:方法,hitTest:withEvent:方法内部会通过以下条件判断 window/视图 能否能响应事件,以下判断条件都是不能响应事件的:
    • 不允许交互:userInteractionEnabled=NO
    • 隐藏:hidden = YES
    • 透明度:alpha < 0.01,alpha小于0.01为全透明
  • 5.如果能响应,该函数又会调用pointInside方法判断当前触摸点是不是在视图范围内,不在视图范围内也是不会响应的。
  • 6.如果在 window/视图 范围内,开始反向遍历 window/视图 的子视图列表subviews,遍历的同时会调用subviews中每个子视图的hitTest:withEvent:方法,判断逻辑和上面的一样,直到找到离用户最近的、能响应事件的视图。
  • 4.5.6过程会递归判断,直到找到最外层合适的view,最后返回的view就是最佳响应视图。
hitTest:withEvent:方法的可能实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha) return nil;     
    // 2. 判断点在不在当前控件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历自己的子控件
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        // 把当前控件上的坐标系转换成子控件上的坐标系
        CGPoint childP = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childP withEvent:event];
        if (fitView) { // 直到寻找到最合适的view
            return fitView;
        }
    }
    // 循环结束,表示没有比自己更合适的view
    return self;
}
注意
  • 查找结束,返回最终的view,UIApplication会调用UIWindow的sendEvent,从而触发对应的响应方法,如果我们在UIWindow中重写sendEvent而不调用super的实现,所有的点击事件都不会触发,因为事件是从最底层传递上来的,你切断了最底层的传递,肯定就无法响应了
  • 实际调用hitTest的过程,系统为了找到精准的触摸点会多次调用hitTest方法
  • 如果重写hitTest返回self,传递过程就会终止,那么当前view就是最合适的view;返回nil,传递也会终止,父视图superView就是最合适的view
  • 如果遍历subviews的过程都没找到合适的view,那么subviews中的子view的hitTest方法都会被调用一次
  • hitTest方法会调用pointInside判断当前视图是否在点击区域,所以超出父视图边界的控件无法响应事件
  • 同一个view上的两个子视图有重叠部分,后加入的视图会被加入到事件传递链
  • 在打印视图层级结构中部分视图执行hitTestpointInSide方法中可以看到,viewController并没有执行这两个方法。所以传递链中没有viewController,因为viewController本身不具有大小的概念。而响应链中有viewController,因为viewController继承UIResponder。

响应链

当找到最合适的响应者之后,便会调用控件相应的touch方法来作具体处理,然而这些方法默认都是不做处理的,但是我们要是想让该响应者响应该事件就可以重写一开始说的那几个响应事件方法,并且我们也可以在重写touch方法中加入[super touch],使多个响应者同时响应同一事件。如果我们对响应事件的方法不做处理那么将该事件随着响应者链条往回传递,交给上一个响应者来处理(即调用super的touch方法),直到找到一个能响应该事件的响应者。

响应过程
  • 1.通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者
  • 2.如果当前view默认不去重写响应事件方法,或者重写调用了父类的响应事件方法,响应就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)
  • 3.如果上一个响应者是viewController,由viewController的view处理,如果view本身没处理,则传递给viewController本身
  • 4.重复上述过程,直到传递到window,window如果也不能处理,则传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,如果delegate也不处理最后丢弃

UIControl的Target-Action设计模式

在 UIControl 及其子类(UIButton等控件)的设计上,iOS Api 采用了Target-Action的设计模式。宏观上来看,这并不属于响应者链的一部分,它只是事件处理的一个末端机制。

[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];

这种代码我们几乎天天都在写,这就是典型的Target-Action的设计模式

  • 第一个参数,Target,UIEvent的新的作用对象(响应者)
  • 第二个参数,Action,是对该UIEvent做出响应的具体动作
  • 第三个参数,是对UIEvent的抽象映射

UIControl 通过这种Target-Action的方式对 UIEvent 进行了转发,从而可以把 UIEvent 事件转发给任意对象处理(原本只有 UIReponder 对象和手势识别器对象才能处理)。

Target-Action设计模式的具体实现

UIControl 实现的具体做法其实是,重写touchesBegan相关方法,通过改变响应者链来实现事件转发的:

  • 首先在touchesBegan方法中调用sendAction:to:forEvent:把消息先转发给UIApplication,让其统一处理。
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self sendAction:@selector(buttonClicked:) to:target forEvent:event];
    }
    
  • 然后UIApplication调用sendAction:to:from:forEvent:把消息交给具体的Target(对象)处理。
    - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
        [[UIApplication sharedApplication] sendAction:action to:target from:self forEvent:event];
    }
    

【iOS】—— 响应者链和事件传递链

简要概括

简单的来说,Target-Action设计模式其实就是重写了UIControl类的touch响应事件,通过重写的类的touch响应事件将事件转发给了UIApplication(子类没有重写touch方法默认就会调用父类的),然后UIApplication通过响应事件传递过来的新响应者,将这个事件的具体调用方法(即我们自定义的方法)传递给了这个新响应者,这样新响应者就实现了方法的调用,执行了我们自定义的方法。

扩大点击范围

扩大点击范围,用到了两个主要方法:

// 返回矩形是否包含指定的点
// rect 要检查的矩形
// point 检查的点
CG_EXTERN bool CGRectContainsPoint(CGRect rect, CGPoint point)
    CG_AVAILABLE_STARTING(10.0, 2.0);


// 返回一个比源矩形小或大且具有相同中心点的矩形
// rect 原CGRect结构
// dx 用于调整源矩形的x坐标值。若要缩小原矩形,请指定一个正值。若要扩大原矩形,请指定负值。
// dy 用于调整源矩形的y坐标值。若要缩小原矩形,请指定一个正值。若要扩大原矩形,请指定负值。
CG_EXTERN CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy) __attribute__ ((warn_unused_result))
    CG_AVAILABLE_STARTING(10.0, 2.0);

举例说明

先创建一个UIButton的子类,并重写其pointInside方法:

// 该方法返回YES,就会触发其响应事件
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 将该button的原bounds大小扩大,x扩大20,y也扩大20
    CGRect bounds = CGRectInset(self.bounds, -20, -20);
    // 判断点击的点是否在更改后的bouns中
    return CGRectContainsPoint(bounds, point);
}

或者我们重写hitTest方法,使其成为最佳响应者:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	// 将该button的原bounds大小扩大,x扩大20,y也扩大20
    CGRect bounds = CGRectInset(self.bounds, -20, -20);
    // 判断点击的点是否在更改后的bouns中
    if (CGRectContainsPoint(bounds, point)) {
        return self;
    } else {
        return nil;
    }
}

这里我们扩大其响应范围,创建的button大小是不会变化的,变化的只是我们看不到的其可以响应的范围。

点击穿透事件

点击穿透事件就比较麻烦了,它要在重写的UIButton中再传入一个你想要执行事件的button,通过它来响应点击事件。

举例说明

如图,视图1与视图2有重合部分3,当点击3时,我们希望视图1来响应这个点击事件

【iOS】—— 响应者链和事件传递链
这时应该重写绿色部分的hitTest的方法,同时还需要给绿色部分传入紫色部分的对象:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	// 将紫色视图的响应范围转换出来,给point1
    CGPoint point1 = [self convertPoint:point toView:_purpleView];
    // 判断点击范围是否在紫色视图范围中,如果是就通过紫色视图来执行事件
    if ([_purpleView pointInside:point1 withEvent:event]) {
        return _purpleView;
    } else { // 否则就执行父类的方法
        return [super hitTest:point withEvent:event];
    }
}

iOS事件传递及响应者链条
参考
iOS触摸事件全家桶文章来源地址https://www.toymoban.com/news/detail-411884.html

到了这里,关于【iOS】—— 响应者链和事件传递链的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 小程序View点击响应传递多个参数

    小程序开发中,view的点击事件是通过bindtap绑定的,比如: 在js文件中是这样获取参数id的: 如果要传递多个参数,就要用到data-xxx属性了,xxx的意思是这个名称可以随便取: 打印一下传递到js的数据,会看到一个json格式的数据: 所以我们要获取点击的参数,可以这样写:

    2024年04月14日
    浏览(40)
  • 【SpringMVC】参数传递与用户请求和响应

    目录 一、Postman 工具使用 1.1 Postman安装 1.2 Postman的使用 1.2.1 创建WorkSpace工作空间 1.2.2 创建请求   二、参数传递 2.1 添加 Slf4j 依赖 2.2 普通传参 知识点1:@RequestMapping 知识点2:@RequestParam 2.3 路径传参 知识点3:@PathVariable 2.4 Json数据传参  知识点4:@RequestBody   三、响应 3.1 响

    2024年02月09日
    浏览(46)
  • json数据、日期数据的参数传递及响应

    首先在maven中添加json坐标 1.1 postman如何发送json数据 1.2 发送json数据,控制器如何接收 在springMVC配置文件中开启@EnableWebMvc才可以将json数据转换成各种对象数据,作用就是根据传参类型匹配对应的类型转换器 @RequestParam与@RequestBody注解之间的区别 2.1 日期类型参数如何指定格式

    2024年02月11日
    浏览(61)
  • SpringCloud - OpenFeign 参数传递和响应处理(全网最详细)

    目录 一、OpenFeign 参数传递和响应处理 1.1、feign 客户端参数传递 1.1.1、零散类型参数传递 1. 例如 querystring 方式传参 2. 例如路径方式传参 1.1.2、对象参数传递 1. 对象参数传递案例 1.1.3、数组参数传递 1. 数组传参案例 1.1.4、集合类型的参数传递(了解) 1.2、feign 客户端响应处

    2024年02月02日
    浏览(42)
  • vue3中父组件与组件之间参数传递,使用(defineProps/defineEmits),涉及属性传递,对象传递,数组传递,以及事件传递

    传递属性 父组件: 子组件: 传递对象或者数组 父组件: 子组件: 父组件: 子组件:

    2024年02月13日
    浏览(44)
  • vue 子组件 emit传递事件和事件数据给父组件

    1 子组件通过emit 函数 传递事件名\\\'init-complete 和 数据dateRange 2  父组件 创建方法 接收数据 3 父组件 创建的方法 和 子组件事件绑定 4 完整代码 4.1 子组件 4.2 父组件 ps: 不能传递list 类型

    2024年02月11日
    浏览(45)
  • SpringMVC进阶:常用注解、参数传递和请求响应以及页面跳转

    目录 一、常用注解 1.1.@RequestMapping 1.2.@RequestParam 1.3.@ModelAttribute 1.4.@SessionAttributes 1.5.@RequestBody 1.6.@RequestHeader 1.7.@PathVariable 1.8.@CookieValue 二、参数传递 2.1.基础类型+String 2.2.复杂类型 2.3.@RequestParam 2.4.@PathVariable 2.5.@RequestBody 2.6.@RequestHeader 三、返回值 3.1.void 3.2.String 3.3.String+Mod

    2024年02月09日
    浏览(42)
  • 小程序数据传递和自定义事件

    数据传递 1、使用全局变量:可以在app.js中定义一个全局的变量,然后在其他页面中通过 getApp().globalData.xxx 的方式来访问该变量。 在其他页面或组件中,可以通过 getApp().globalData 获取应用实例的全局变量进行访问。 2、使用缓存API:可以使用小程序提供的缓存API,如wx.setSto

    2024年02月10日
    浏览(34)
  • Qt事件传递及相关的性能问题

    在使用Qt时,我们都知道能通过mousePressEvent,eventFilter等虚函数的重写来处理事件,那么当我们向一个界面发送事件,控件和它的父控件之间的事件传递过程是什么样的呢? 本文将以下图所示界面为例,结合源码介绍Qt事件传递的过程。 父到子的关系依次为: MyWindow-MyButton-

    2024年01月24日
    浏览(40)
  • vue实现axios和事件Bus等父子组件的事件传递实现

    发送请求的配置 vue.config.js vue中bus的事件线传递接收 路由守卫中,使用bus事件传递信息,弹出事件 父子组件之间的事件传递接收 接收组件的信息

    2024年04月11日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包