iOS 响应者链

iOS中的响应者链是指UIKit 生成的UIResponder对象组成的链表,它是iOS里一切事件相关(触摸事件、运动事件、远程控制事件、按下事件)的基础

  • 触摸事件是最常见的事件,一般表示手指在屏幕上的各种操作,它会被传递给最初发生触摸的视图
  • 运动事件是由UIKit触发的,例如通过加速器、陀螺仪等内置硬件触发的事件,比如摇一摇(运动事件相关的加速度计、陀螺仪、磁强计都不属于响应者链。而是由CoreMotion传递事件给你指定的对象)
  • 远程控制事件允许响应对象接收来自外部附件或耳机的命令,以便它能够管理音频和视频——例如,播放视频或跳转到下一个音频轨道
  • 按下事件表示与游戏控制器、AppleTV遥控器或其他具有物理按钮的设备的交互

首先在响应者链中有几个类需要了解:UIResponder、UIEvent、UITouch

  • UITouch 是用于处理用户触摸交互的底层对象,每个UITouch代表当前触摸屏幕的一根手指,如果手指移动,系统会不断更新UITouch的属性,在手势发生的过程中(触摸序列中),UITouch对象会被创建,被销毁,还会改变状态。
  • UIEvent代表一个事件(事件分为很多种),如果是触摸事件,UIEvent中保存着同当前触摸序列相关的UITouch对象(一个或多个)
  • UIResponder是用于响应和处理事件的类,直接继承于NSObject,所有能响应用户事件的类都要继承于它,比如UIApplication、UIViewController、UIView(包括UIWindow),UIResponder类中定义了响应各种事件的接口

当我们点击了屏幕上的某个View,这个动作由硬件层传导到操作系统,然后底层会将这个动作封装成一个事件(Event)拴着view的层级网上传导,一直找到含有这个触摸点并且层级最高(相当于离手指最近)的view来响应这个事件,这个view叫做hit-test view

在这个过程中,决定最后的hit-test view是谁 是通过递归调用响应者链中每个view的-(UIView *) hitTest:withEvent:方法和-(BOOL)pointInside:withEvent:方法来实现的

用一个Demo来试一试, 我们自定义几个view,然后重写相关的方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"进入AView hitTest:withEvent:");
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"离开AView hitTest:withEvent: ----view: %@",[view class]);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"进入AView pointInside:withEvent:");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"离开AView pointInside:withEvent: ---- isInside: %d",isInside);
    return isInside;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"AView touchesBegan");
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"AView touchesMoved");
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"AView touchesEnded");
}

在rootController的view中添加这几个view:


iOS 响应者链_第1张图片
WX20190612-143438.png

此时我们点击AView,打印的内容为:


iOS 响应者链_第2张图片
WX20190612-144015.png

可以看到触摸事件最先传递到了AView(其实在这之前已经经过了UIWindow和rootViewController的view,只不过我们没有重写他们相应的方法),说明AView可以处理这个事件,然后判断触摸点是否在AView上,结果是ture,然后继续遍历AView的子控件,就是BView和CView,但是触摸点都不在这两个View中,所以这两个view的hitTest:withEvent:方法返回的View都为空,最后得到的结果是AView是最适合处理这个事件的View,由AView来处理这个事件

同时我们看到,系统会在决定了由哪个View来处理事件后,才会触发这个View的touches系列方法

我们再点击一次CView,此时的打印内容为:


iOS 响应者链_第3张图片
WX20190612-150438.png

可以看到这次点击事件经过AView之后,遍历子控件,进入了CView,此时触摸点在CView内,而CView也没有任何子视图了,所以系统就会直接将CView作为了处理这次事件的View(事件并没有进入BView)。

我们再点击一次DView,打印内容为:


iOS 响应者链_第4张图片
WX20190612-150356.png

可以看到这次依次经过了AView -> CView -> BView -> EView -> DView,当事件传递给某一个View的时候,这个View会调用hitTest:withEvent:方法寻找最适合的View,如果子View更合适,就会一直遍历,直到找到那个能响应事件并且触摸点在本身范围内的View,然后所有在这个有效的链上的View的hitTest:withEvent:方法都会返回这个最适合的View,使用此View来响应这个事件。

需要注意的是:当某个view符合以下三种条件时,它是不可以响应事件的

  • isUserInteractionEnabled 属性为 NO
  • isHidden 属性为YES
  • alpha属性的值小于等于0.01

我们可以试试,将DView的isUserInteractionEnabled 属性设置为NO,然后再点击DView

iOS 响应者链_第5张图片
WX20190612-152114.png

可以看到此时响应点击事件的View为BView

这个寻找最适合View的流程用代码表示大概是这样:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    //如果符合这三个条件,则这个View不能响应事件
    if (!self.userInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    
    //如果触摸点在这个View的范围内
    if ([self pointInside:point withEvent:event]) {
        //遍历这个View的所有子视图,用reverseObjectEnumerator个人猜想是因为如果你在同一个父视图上添加若干个子视图,如果子视图有重叠的情况下,应该是最上层的这个视图来响应事件,而这个视图应该是后被添加的,这样可以更加快速地找到响应者
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            //转换坐标
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

找到hit-test view后,这个view会有最高的优先级来响应诸暨传递过来的Event,如果它不能响应就会传递给它的superView,以此类推,一直到UIApplication都无响应者的话这个Event就被系统丢弃了

此时我们把EView的Frame改变一下,让它一部分超出父视图以外,然后点击它超出父视图的那部分

iOS 响应者链_第6张图片
WechatIMG158.png

此时的打印内容为:


iOS 响应者链_第7张图片
WX20190612-160624.png

可以看到这次响应点击事件的View是AView,个人理解是因为触摸点没有在BView范围内,所以寻找响应者对象的时候就不会遍历BView的子视图了,最后由AView来响应

我们将BView的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 重写 直接返回YES 此时EView就可以响应了

在实际开发中,我们可以通过这个机制来实现一些比较实用的功能,比如一个button很小,我们想让他的点击范围扩大,又不想将改变他的外观,就可以重写UIBUtton的-(BOOL)pointInside:withEvent:方法来实现
我们新建一个UIButton的子类,重写这个方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect bounds = self.bounds;
    bounds = CGRectInset(bounds, -20, -20);
    return CGRectContainsPoint(bounds, point);
}

此时我们就会发现当点击button周围的范围时也会触发button的action

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