热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOS开发之事件传递响应链

这篇文章主要为大家介绍了iOS开发之事件传递响应链,何为事件的响应链,本文为大家揭晓,感兴趣的小伙伴们可以参考一下

当我们在使用微信等工具,点击扫一扫,就能打开二维码扫描视图。在我们点击屏幕的时候,iphone OS获取到了用户进行了“单击”这一行为,操作系统把包含这些点击事件的信息包装成UITouch和UIEvent形式的实例,然后找到当前运行的程序,逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链,如下图所示,不用的响应者以链式的方式寻找

事件响应链

一、响应者

在iOS中,能够响应事件的对象都是UIResponder的子类对象。UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用。

UIResponder的点击事件

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

1、事件对象
iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的
2、点击对象
UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中,我们通过集合中对象获取用户点击的位置。其中通过- (CGPoint)locationInView:(nullable UIView *)view获取当前点击坐标点,- (CGPoint)previousLocationInView:(nullable UIView *)view获取上个点击位置的坐标点。
为了确认UIView确实是通过UIResponder的点击方法响应点击事件的,我创建了UIView的类别,并重写+ (void)load方法,使用method_swizzling的方式交换点击事件的实现

+ (void)load
  Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:));
  Method custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesBegan:withEvent:));
  method_exchangeImplementations(origin, custom);
 
  origin = class_getInstanceMethod([UIView class], @selector(touchesMoved:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesMoved:withEvent:));
  method_exchangeImplementations(origin, custom);
 
  origin = class_getInstanceMethod([UIView class], @selector(touchesEnded:withEvent:));
  custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesEnded:withEvent:));
  method_exchangeImplementations(origin, custom);
}
 
- (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- begin", self.class);
  [self lxd_touchesBegan: touches withEvent: event];
}
 
- (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- move", self.class);
  [self lxd_touchesMoved: touches withEvent: event];
}
 
- (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event
{
  NSLog(@"%@ --- end", self.class);
  [self lxd_touchesEnded: touches withEvent: event];
}

在新建的项目中,我分别创建了AView、BView、CView和DView四个UIView的子类,然后点击任意一个位置:

项目结构图

在我点击上图绿色视图的时候,控制台输出了下面的日志(日期部分已经去除):

CView --- begin
CView --- end

由此可见在我们点击UIView的时候,是通过touches相关的点击事件进行回调处理的。

除了touches回调的几个点击事件,手势UIGestureRecognizer对象也可以附加在view上,来实现其他丰富的手势事件。在view添加单击手势之后,原来的touchesEnded方法就无效了。最开始我一直认为view添加手势之后,原有的touches系列方法全部无效。但是在测试demo中,发现view添加手势之后,touchesBegan方法是有进行回调的,但是moved跟ended就没有进行回调。因此,在系统的touches事件处理中,在touchesBegan之后,应该是存在着一个调度后续事件(nextHandler)处理的方法,个人猜测事件调度的处理大致如下图示:

事件调度

二、响应链传递

上面已经介绍了某个控件在接收到点击事件时的处理,那么系统是怎么通过用户点击的位置找到处理点击事件的view的呢?
在上文我们已经说过了系统通过不断查找下一个响应者来响应点击事件,而所有的可交互控件都是UIResponder直接或者间接的子类,那么我们是否可以在这个类的头文件中找到关键的属性呢?

正好存在着这么一个方法:- (nullable UIResponder *)nextResponder,通过方法名我们不难发现这是获取当前view的下一个响应者,那么我们重写touchesBegan方法,逐级获取下一响应者,直到没有下一个响应者位置。相关代码如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  UIResponder * next = [self nextResponder];
  NSMutableString * prefix = @"".mutableCopy;
 
  while (next != nil) {
    NSLog(@"%@%@", prefix, [next class]);
    [prefix appendString: @"--"];
    next = [next nextResponder];
  }  
}

控制台输出的所有下级事件响应者如下:

AView
--UIView
----ViewController
------UIWindow
--------UIApplication
----------AppDelegate

虽然结果非常有层次,但是从系统逐级查找响应者的角度上来说,这个输出的顺序是刚好相反的。为什么会出现这种问题呢?我们可以看到输出中存在一个ViewController类,说明UIViewController也是UIResponder的子类。但是我们可以发现,controller是一个view的管理者,即便它是响应链的成员之一,但是按照逻辑来说,控制器不应该是系统查找对象之一,通过nextResponder方法查找的这个思路是不正确的。

后来,发现在UIView的头文件中存在这么两个方法,分别返回UIView和BOOL类型的方法:

- (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

根据方法名,一个是根据点击坐标返回事件是否发生在本视图以内,另一个方法是返回响应点击事件的对象。通过这两个方法,我们可以猜到,系统在收到点击事件的时候通过不断遍历当前视图上的子视图的这些方法,获取下一个响应的视图。因此,继续通过method_swizzling方式修改这两个方法的实现,并且测试输出如下:

UIStatusBarWindow can answer 1
UIStatusBar can answer 0
UIStatusBarForegroundView can answer 0
UIStatusBarServiceItemView can answer 0
UIStatusBarDataNetworkItemView can answer 0
UIStatusBarBatteryItemView can answer 0
UIStatusBarTimeItemView can answer 0
hit view: UIStatusBar
hit view: UIStatusBarWindow
UIWindow can answer 1
UIView can answer 1
hit view: _UILayoutGuide
hit view: _UILayoutGuide
AView can answer 1
DView can answer 0
hit view: DView
BView can answer 0
hit view: BView
hit view: AView
hit view: UIView
hit view: UIWindow
...... //下面是touches方法的输出

最上面的UIStatusBar开头的类型大家可能没见过,但是不妨碍我们猜到这是状态栏相关的一些视图,具体可以查找苹果的文档中心(Xcode中快捷键shift+command+0打开)。从输出中不难看出系统先调用pointInSide: WithEvent:判断当前视图以及这些视图的子视图是否能接收这次点击事件,然后在调用hitTest: withEvent:依次获取处理这个事件的所有视图对象,在获取所有的可处理事件对象后,开始调用这些对象的touches回调方法

通过输出的方法调用,我们可以看到响应查找的顺序是: UIStatusBar相关的视图 -> UIWindow -> UIView -> AView -> DView -> BView(系统在事件链传递的过程中一定会遍历所有的子视图判断是否能够响应点击事件),以本文demo为例,我们可以得出事件响应链查找的图示如下:

响应者查找流程

那么在上面的查找响应者流程完成之后,系统会将本次事件中的点击转换成UITouch对象,然后将这些对象和UIEvent类型的事件对象传递给touchesBegan方法,you

不仅如此,从上面输出的nextResponder来看,所有的响应者都是在查找中返回可响应点击的视图。因此,我们可以推测出UIApplication对象维护着自己的一个响应者栈,当pointInSide: withEvent:返回yes的时候,响应者入栈。

响应者栈

栈顶的响应者作为最优先处理事件的对象,假设AView不处理事件,那么出栈,移交给UIView,以此下去,直到事件得到了处理或者到达AppDelegate后依旧未响应,事件被摒弃为止。通过这个机制我们也可以看到controller是响应者栈中的例外,即便没有pointInSide: withEvent:的方法返回可响应,controller依旧能够入栈成为UIView的下一个响应者。

、响应链应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
  const CGFloat halfWidth = 100;
  CGFloat xOffset = point.x - 100;
  CGFloat yOffset = point.y - 100;
  CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
  return radius <= halfWidth;
}

最终的效果图如下:

以上就是本文的全部内容,希望对大家的学习有所帮助。


推荐阅读
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了在Vue项目中如何结合Element UI解决连续上传多张图片及图片编辑的问题。作者强调了在编码前要明确需求和所需要的结果,并详细描述了自己的代码实现过程。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
  • 本文介绍了ASP.NET Core MVC的入门及基础使用教程,根据微软的文档学习,建议阅读英文文档以便更好理解,微软的工具化使用方便且开发速度快。通过vs2017新建项目,可以创建一个基础的ASP.NET网站,也可以实现动态网站开发。ASP.NET MVC框架及其工具简化了开发过程,包括建立业务的数据模型和控制器等步骤。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • MVC设计模式的介绍和演化过程
    本文介绍了MVC设计模式的基本概念和原理,以及在实际项目中的演化过程。通过分离视图、模型和控制器,实现了代码的解耦和重用,提高了项目的可维护性和可扩展性。详细讲解了分离视图、分离模型和分离控制器的具体步骤和规则,以及它们在项目中的应用。同时,还介绍了基础模型的封装和控制器的命名规则。该文章适合对MVC设计模式感兴趣的读者阅读和学习。 ... [详细]
  • 如何在HTML中获取鼠标的当前位置
    本文介绍了在HTML中获取鼠标当前位置的三种方法,分别是相对于屏幕的位置、相对于窗口的位置以及考虑了页面滚动因素的位置。通过这些方法可以准确获取鼠标的坐标信息。 ... [详细]
  • 本文介绍了MVP架构模式及其在国庆技术博客中的应用。MVP架构模式是一种演变自MVC架构的新模式,其中View和Model之间的通信通过Presenter进行。相比MVC架构,MVP架构将交互逻辑放在Presenter内部,而View直接从Model中读取数据而不是通过Controller。本文还探讨了MVP架构在国庆技术博客中的具体应用。 ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • wpf+mvvm代码组织结构及实现方式
    本文介绍了wpf+mvvm代码组织结构的由来和实现方式。作者回顾了自己大学时期接触wpf开发和mvvm模式的经历,认为mvvm模式使得开发更加专注于业务且高效。与此同时,作者指出mvvm模式相较于mvc模式的优势。文章还提到了当没有mvvm时处理数据和UI交互的例子,以及前后端分离和组件化的概念。作者希望能够只关注原始数据结构,将数据交给UI自行改变,从而解放劳动力,避免加班。 ... [详细]
author-avatar
书友56183408
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有