iOS|iOS 响应者及响应者链

当我们点击一个 button 时,button 的响应消息机制分为两块:

  • 首先在视图层次中找到能响应消息的那个视图即 button;
  • 然后在找到的视图 button 中进行事件处理;
UIButton 继承关系:
UIButton < UIControl < UIView < UIResponder
UIButton 之所以能够处理事件,是因为它继承自 UIResponder。也就是说只有继承自UIResponder的类才能处理事件。
找响应者
如图,找响应者是从父 View 到子 View 过程查找。主要用到了 UIView 的hitTest:withEvent: 以及 pointInside:withEvent: 两个方法。
iOS|iOS 响应者及响应者链
文章图片
原理如下:
  • 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由 UIApplication 管理的事件队列中;
  • UIApplication 会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow);
  • 主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的 UIView 来处理触摸事件
    (hitTest:withEvent:其实是 UIView 的一个方法,UIWindow 继承自 UIView,因此主窗口 UIWindow 也是属于视图的一种);
hitTest:withEvent: 方法处理机制: 当前 view 调用自身的 pointInside: withEvent:方法判断触摸点是否在自己范围内:
  • pointInside: withEvent:方法返回 NO,则说明触摸点不在自己范围内,则当前 view 的hitTest: withEvent:方法返回 nil,当前 view上 的所有 subview 都不做判断。有点领导的意见一票否决的味道。
  • pointInside: withEvent:方法返回 YES,则说明触摸点在自己的范围内。但无法判断是否在自己身上还是在 subview 的身上。此时,遍历所有的 subviews,对每个 subview 调用 hitTest 方法。这里要注意,遍历的顺序是从当前 view 的 subviews 数组的尾部开始遍历。因此离用户最近的上层的 subview 会优先被调用 hitTest 方法。
  • 一旦 hitTest 方法返回非空的 view,则被返回的 view 就是最终相应触摸事件的 view,寻找 hitTesting view 的阶段到此结束,不再遍历。
    若当前 view 的所有 subviews 的 hitTest 方法都返回 nil,则当前 view 的 hitTest 方法返回 self 作为最终的 hitTesting view,处理结束。
举个例子,更加清晰的了解下:
如图:
iOS|iOS 响应者及响应者链
文章图片
当用户点击ViewD所在的区域时会进行以下hit-Testing:
  • 【iOS|iOS 响应者及响应者链】ViewA 的 pointInside 返回 YES,因为触摸点在其 bounds 内。遍历 ViewA 的两个 subview;
  • ViewB 的 pointInside 返回 NO,因为触摸点不在其 bounds 内,ViewB 的 hitTest 方法返回 nil。而且发生一票否决,在 ViewB 上的所有 subviews 受到牵连将不再进行 hit-Testing 处理。
  • ViewC 的 pointInside 返回 YES,因为触摸点在其 bounds 范围内,ViewC 的 hitTest 方法返回默认处理,也就是 return [super hitTest:point withEvent:event]; 遍历 ViewC 的两个 subview;
  • ViewD的 pointInside 返回 YES,因为触摸点在其 bounds 范围内,且ViewD 没有 subview,因此 hitTest 方法返回其自己。hitTesting view 找到,结束处理
需要注意的地方: 1、hitTest 方法调用 pointInside 方法;
2、hit-Testing 过程是从 superView 向 subView 逐级传递,也就是从层次树的根节点向叶子节点传递;
3、遇到以下设置时,view 的 pointInside 将返回NO,hitTest 方法返回 nil:
  • view.isHidden=YES;
  • view.alpah<=0.01;
  • view.userInterfaceEnable=NO;
  • control.enable=NO; (UIControl的属性)
事件响应
在上一部分已经找到了响应者,这个响应者就会执行相应的 touch 系列方法,系统默认处理事件之后将不继续向下一响应者传递。我们自己可以根据需要,通过复写方法把当前事件向下一响应者进行传递。
可以复写下列方法对事件进行处理
//触摸开始,手指触碰屏幕 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event //触摸结束,手指离开屏幕 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event //触摸取消(如电话接入的时候) - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event //手指移动(会调用多次) - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event //3D touch 9.1之后加入的3D触摸事件 - (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches

举个例子:
新建一个 Single View App,在 ViewController.m 中:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{//加上这一句,事件就可以向下一个响应者传递 [super touchesBegan:touches withEvent:event]; NSLog(@"viewController touch begin"); }

在 AppDelegate.m 中:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"appDelegate touch begin"); }

这样我们就实现了将事件向下一个响应者传递。
响应者链
下图响应者链链来自官网
iOS|iOS 响应者及响应者链
文章图片
我们也可以通过代码打印响应者链:
- (IBAction)click:(id)sender { UIResponder *res = sender; while (res) { NSLog(@"*************************************\n%@",res); res = [res nextResponder]; } }

  • UIView 的 nextResponder 属性,如果有管理此 view 的 UIViewController 对象,则为此 UIViewController 对象;否则 nextResponder 即为其 superview。
  • UIViewController 的 nextResponder 属性为其管理 view 的 superview.
  • UIWindow 的 nextResponder 属性为 UIApplication 对象。
  • UIApplication 的 nextResponder 属性为 nil。
解释一下:
1、如果 hit-test view 或 first responder 不处理此事件,则将事件传递给其 nextResponder 处理,若有 UIViewController 对象则传递给 UIViewController,传递给其 superView。
2、如果 view 的 viewController 也不处理事件,则 viewController 将事件传递给其管理 view 的 superView。
3、视图层级结构的顶级为 UIWindow 对象,如果 window 仍不处理此事件,传递给 UIApplication.
4、若 UIApplication 对象不处理此事件,则事件被丢弃。
了解响应者链有时候可以帮我解决一些实际问题。我举个例子,我们知道,当提供给你一个ViewController你可以很容易得到它的view,一句代码的事情:
viewWanted = someViewController.view;

但如果反过来呢?当给你一个view,让你找到其所在的ViewController呢?这时候响应者链可以帮上忙了,代码如下:
@implementation UIView (FindController) -(UIViewController*)parentController{ UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController*)responder; } responder = [responder nextResponder]; } return nil; } @end

放一张完整的图来理解下iOS触摸事件的流动:
iOS|iOS 响应者及响应者链
文章图片
参考资料:
官方文档
https://www.cnblogs.com/Quains/p/3369132.html
https://www.cnblogs.com/wengzilin/p/4720550.html
http://shellhue.github.io/2017/03/04/FlowOfUITouch/

    推荐阅读