iOS 响应者及响应者链

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

  • 首先在视图层次中找到能响应消息的那个视图即 button;

  • 然后在找到的视图 button 中进行事件处理;

UIButton 继承关系:

UIButton < UIControl < UIView < UIResponder

UIButton 之所以能够处理事件,是因为它继承自 UIResponder。也就是说只有继承自UIResponder的类才能处理事件

找响应者

如图,找响应者是从父 View 到子 View 过程查找。主要用到了 UIView 的hitTest:withEvent: 以及 pointInside:withEvent: 两个方法。

iOS 响应者及响应者链_第1张图片

原理如下:

  • 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由 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 响应者及响应者链_第2张图片

当用户点击ViewD所在的区域时会进行以下hit-Testing:

  • 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 响应者及响应者链_第3张图片

我们也可以通过代码打印响应者链:

- (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 响应者及响应者链_第4张图片

参考资料:

官方文档

https://www.cnblogs.com/Quains/p/3369132.html

https://www.cnblogs.com/wengzilin/p/4720550.html

http://shellhue.github.io/2017/03/04/FlowOfUITouch/

你可能感兴趣的:(iOS 响应者及响应者链)