序言

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

事件响应链

响应者

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

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 获取上个点击位置的坐标点。

为了确认UIView确实是通过UIResponder的点击方法响应点击事件的,我创建了UIView的类别,并重写+ (void)load方法,使用method_swizzling的方式交换点击事件的实现

  1. + (void)load
  2. Method origin = class_getInstanceMethod([UIView class], @selector(touchesBegan:withEvent:));
  3. Method custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesBegan:withEvent:));
  4. method_exchangeImplementations(origin, custom);
  5.  
  6. origin = class_getInstanceMethod([UIView class], @selector(touchesMoved:withEvent:));
  7. custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesMoved:withEvent:));
  8. method_exchangeImplementations(origin, custom);
  9.  
  10. origin = class_getInstanceMethod([UIView class], @selector(touchesEnded:withEvent:));
  11. custom = class_getInstanceMethod([UIView class], @selector(lxd_touchesEnded:withEvent:));
  12. method_exchangeImplementations(origin, custom);
  13. }
  14. - (void)lxd_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event
  15. {
  16. NSLog(@"%@ --- begin", self.class);
  17. [self lxd_touchesBegan: touches withEvent: event];
  18. }
  19. - (void)lxd_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event
  20. {
  21. NSLog(@"%@ --- move", self.class);
  22. [self lxd_touchesMoved: touches withEvent: event];
  23. }
  24. - (void)lxd_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event
  25. {
  26. NSLog(@"%@ --- end", self.class);
  27. [self lxd_touchesEnded: touches withEvent: event];
  28. }

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

项目结构图

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

  1. CView --- begin
  2. 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方法,逐级获取下一响应者,直到没有下一个响应者位置。相关代码如下:

  1. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  2. {
  3. UIResponder * next = [self nextResponder];
  4. NSMutableString * prefix = @"".mutableCopy;
  5.  
  6. while (next != nil) {
  7. NSLog(@"%@%@", prefix, [next class]);
  8. [prefix appendString: @"--"];
  9. next = [next nextResponder];
  10. }
  11. }

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

  1. AView
  2. --UIView
  3. ----ViewController
  4. ------UIWindow
  5. --------UIApplication
  6. ----------AppDelegate

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

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

  1. - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

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

  1. UIStatusBarWindow can answer 1
  2. UIStatusBar can answer 0
  3. UIStatusBarForegroundView can answer 0
  4. UIStatusBarServiceItemView can answer 0
  5. UIStatusBarDataNetworkItemView can answer 0
  6. UIStatusBarBatteryItemView can answer 0
  7. UIStatusBarTimeItemView can answer 0
  8. hit view: UIStatusBar
  9. hit view: UIStatusBarWindow
  10. UIWindow can answer 1
  11. UIView can answer 1
  12. hit view: _UILayoutGuide
  13. hit view: _UILayoutGuide
  14. AView can answer 1
  15. DView can answer 0
  16. hit view: DView
  17. BView can answer 0
  18. hit view: BView
  19. hit view: AView
  20. hit view: UIView
  21. hit view: UIWindow
  22. ...... //下面是touches方法的输出

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

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

响应者查找流程

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

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

响应者栈

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

响应链应用

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

  1. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  2. {
  3. const CGFloat halfWidth = 100;
  4. CGFloat xOffset = point.x - 100;
  5. CGFloat yOffset = point.y - 100;
  6. CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
  7. return radius <= halfWidth;
  8. }

最终的效果图如下:

不规则形状点击
demo地址

iOS开发 - 事件传递响应链的更多相关文章

  1. iOS 中事件的响应链和传递链

    iOS事件链有两条:事件的响应链:Hit-Testing事件的传递链 响应链:由离用户最近的view向系统传递.initial view –> super view –> ….. –> ...

  2. iOS 事件传递响应链

    iOS中加载的时候会先执行main函数 int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain( ...

  3. 事件分发&响应链

    iOS的三种事件:触摸事件/运动事件/远程控制事件 typedef enum { UIEventTypeTouches, UIEventTypeMotion, UIEventTypeRemoteCon ...

  4. iOS开发事件分发机制—响应链—手势影响

    1.提纲 什么是iOS的事件分发机制 ? 一个事件UIEvent又是如何响应的? 手势对于响应链有何影响? 2.事件分发机制 2.1.来源 以直接触摸事件为例: 当用户一个手指触摸屏幕是会生成一个UI ...

  5. iOS中事件传递过程

    iOS中,UIApplication管理着一个事件的队列,当系统获取用户的点击或滑动等事件后,就会将这些事件按顺序插入UIApplication管理的这个队里中,UIApplication再从这个队列 ...

  6. Android 手机卫士--事件传递&响应规则

    问题的提出: 本文地址:http://www.cnblogs.com/wuyudong/p/5911187.html ,转载请注明源地址. 前面的文章实现了点击SettingItemView条目的时候 ...

  7. iOS开发技巧系列---使用链式编程和Block来实现UIAlertView

    UIAlertView是iOS开发过程中最常用的控件之一,是提醒用户做出选择最主要的工具.在iOS8及后来的系统中,苹果更推荐使用UIAlertController来代替UIAlertView.所以本 ...

  8. 消息点击事件的响应链---hitTest:withEvent:方法

    *当用户点击屏幕时,会产生一个触摸事件,系统会将触摸事件加入到 UIApplication管理事件队里中 *UIApplication 会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件 ...

  9. iOS开发--Swift RAC响应式编程初探

    时间不是很充足, 先少说点, RAC的好处是响应式编程, 不需要自己去设置代理委托, target, 而是主要以信息流(signal), block为主, 看到这里激动吧, 它可以帮你监听你的事件, ...

随机推荐

  1. Gradle 的下载安装配置以及创建第一个Gradle 项目

    1. 什么是Gradle? Gradle是一个开源的构建自动化工具,专注于灵活性和性能. Gradle构建脚本使用Groovy或Kotlin DSL编写. 阅读Gradle功能,了解Gradle的功能 ...

  2. Public key for ambari-server-2.4.2.0-136.x86_64.rpm is not installed 安装ambari报错总结

    提示;# rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release 就是导入这个安装包的key 可以使用http的协议 比如我用的就是 rpm ...

  3. Linux SD/MMC/SDIO驱动分析_转

    转自:Linux SD/MMC/SDIO驱动分析    https://www.cnblogs.com/cslunatic/p/3678045.html#3053341 一.SD/MMC/SDIO概念 ...

  4. cannot open shared object file: No such file or directory

    一般我们在Linux下执行某些外部程序的时候可能会提示找不到共享库的错误, 比如:error while loading shared libraries: libxxx.so: cannot ope ...

  5. myeclipse创建hibernate工程

    1.创建数据库: from blog http://www.cnblogs.com/zhaocundang/p/9061959.html 使用navicat mysql IDE: 创建数据库 book ...

  6. [IR] Arithmetic Coding

    Statistical methods的除了huffman外的另一种常见压缩方式. Huffman coding的非连续数值特性成为了无法达到香农极限的先天无法弥补的缺陷,但Arithmetic co ...

  7. SpringBoot------Maven Install报错

    报错信息: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin::compile (default-compil ...

  8. python单引号(')、双引号(")、三引号(''',""")

    python对字符串的表示方法比c更有灵活性,但是也更难理解. 为了在平时使用.看代码过程中对着单引号(').双引号(").三引号(''',""")不混淆,知道 ...

  9. rayleighchan实现瑞利多径衰落信

    rayleighchan实现瑞利多径衰落信道 1.命令格式: chan = rayleighchan(ts,fd,tau,pdb) 其中: ts—为输入信号的采样周期, fd—就是Doppler频偏, ...

  10. jenkins之 pipeline 小尝试

    最近,一个小需求,动态建立slave节点来执行自动化用例,原有jenkins 老方式不满足需求,就用到jenkins2的pipeline来实现,但在实现过程中,2个小坑记录下 1.jenkins不能读 ...