美文网首页
iOS--事件传递/响应者链

iOS--事件传递/响应者链

作者: mayuee | 来源:发表于2020-09-23 18:40 被阅读0次

    应用程序使用响应者对象接收和处理事件。响应者对象是UIResponder类的任何实例,常见的子类包括UIView、UIViewController和UIApplication。响应者接收原始事件数据,必须处理该事件或将其转发给另一个响应程序对象。当应用程序接收到事件时,UIKit会自动将该事件定向到最合适的响应程序对象,即第一响应者。

    在iOS程序中响应者对象的摆放是有前后关系的,多个响应者对象有序的连接起来的链条就叫“响应者链”。

    创建一个UIView的子类ChainView,重写touchesBegan方法:

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

    创建一个ChainView实例添加到ViewController.view,点击log输出:

    2020-09-24 11:08:56.494864+0800 001--ClientDemo[83858:1377593] greenView --- touchesBegan withEvent ---
    ClientDemo[83858:1377593] UIView
    ClientDemo[83858:1377593] --ViewController
    ClientDemo[83858:1377593] ----UIWindow
    ClientDemo[83858:1377593] ------UIApplication
    ClientDemo[83858:1377593] --------AppDelegate
    

    选择不同版本iOS有不同输出,在iOS12之前是上面输出,iOS12及以后输出如下

    2020-09-24 11:11:41.146777+0800 001--ClientDemo[84116:1402680] greenView --- touchesBegan withEvent ---
    ClientDemo[84116:1402680] UIView
    ClientDemo[84116:1402680] --ViewController
    ClientDemo[84116:1402680] ----UIDropShadowView
    ClientDemo[84116:1402680] ------UITransitionView
    ClientDemo[84116:1402680] --------UIWindow
    ClientDemo[84116:1402680] ----------UIWindowScene
    ClientDemo[84116:1402680] ------------UIApplication
    ClientDemo[84116:1402680] --------------AppDelegate
    

    这里及以下都以iOS11上为例。上面打印的是本例中事件响应传递关系也即事件响应者链,响应者链是事件传递链的逆序,则事件传递链如下图:

    事件传递链

    UIView 的 controller 是直接管理它的 UIViewController (也就是 VC.view.nextResponder = VC ),如果当前 View 不是 ViewController 直接管理的 View,则 nextResponder 是它的 superView( view.nextResponder = view.superView )。
    UIViewController 的 nextResponder 是它直接管理的 View 的 superView( VC.nextResponder = VC.view.superView )。
    UIWindow 的 nextResponder 是 UIApplication 。
    UIApplication 的 nextResponder 是 AppDelegate。

    有了响应链,并且找到了第一个响应事件的对象,接下来就是把事件发送给这个响应者了。 UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的API,把事件发送给hit-testview。

    响应链工作步骤

    1. 事件产生
    2. 事件顺着传递链 AppDelegate---> UIApplication --->UIWindow ---> 查找第一响应者,查找过程主要用到两个函数:
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    
    1. 找到最合适响应者视图后,视图会调用自己的touches方法处理事件。如果自身没有做处理,UIKit那么会逆着事件传递链,向上查找,直到找到能够响应这个事件的视图。当将事件传递给UIApplication对象时,如果该对象是UIResponder实例,而不属于响应程序链的一部分,则可能会将事件传递给应用程序委托AppDelegate。最终如果没有对象响应该事件,该事件会被丢弃(不是Crash!)。

    示例

    左侧是实现效果,其中设置了cyanView.userInteractionEnabled = NO,右侧是当前程序的响应者链(同级视图左边线与右边添加到父视图上)。


    1600846386263.jpg

    修改上面提到的的ChainView实现:

    @interface ChainView : UIView
    @property(nonatomic, copy) NSString *name;
    @end
    
    @implementation ChainView
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"%@ --- touchesBegan withEvent ---", self.name);
    }
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        NSLog(@"%@ --- hitTest withEvent", self.name);
        UIView * view = [super hitTest:point withEvent:event];
        NSLog(@"%@ --- hitTest withEvent --- hitTestView:%@", self.name, ((ChainView *)view).name);
        return view;
    }
    
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
        BOOL isInside = [super pointInside:point withEvent:event];
        NSLog(@"%@ --- pointInside withEvent --- isInside:%d", self.name, isInside);
        return isInside;
    }
    @end
    

    点击blueView和cyanView的重叠处,log输出:

    2020-09-23 16:48:39.439271+0800 TT[72680:1155021] redView --- hitTest withEvent
    2020-09-23 16:48:39.439426+0800 TT[72680:1155021] redView --- pointInside withEvent
    2020-09-23 16:48:39.439538+0800 TT[72680:1155021] redView --- pointInside withEvent --- isInside:1
    2020-09-23 16:48:39.439644+0800 TT[72680:1155021] yllowView --- hitTest withEvent
    2020-09-23 16:48:39.439933+0800 TT[72680:1155021] yllowView --- pointInside withEvent
    2020-09-23 16:48:39.440452+0800 TT[72680:1155021] yllowView --- pointInside withEvent --- isInside:1
    2020-09-23 16:48:39.440935+0800 TT[72680:1155021] purpleView --- hitTest withEvent
    2020-09-23 16:48:39.441447+0800 TT[72680:1155021] purpleView --- pointInside withEvent
    2020-09-23 16:48:39.442443+0800 TT[72680:1155021] purpleView --- pointInside withEvent --- isInside:0
    2020-09-23 16:48:39.442865+0800 TT[72680:1155021] purpleView --- hitTest withEvent --- hitTestView:(null)
    2020-09-23 16:48:39.443304+0800 TT[72680:1155021] orangeView --- hitTest withEvent
    2020-09-23 16:48:39.443728+0800 TT[72680:1155021] orangeView --- pointInside withEvent
    2020-09-23 16:48:39.444398+0800 TT[72680:1155021] orangeView --- pointInside withEvent --- isInside:1
    2020-09-23 16:48:39.444861+0800 TT[72680:1155021] cyanView --- hitTest withEvent
    2020-09-23 16:48:39.445325+0800 TT[72680:1155021] cyanView --- hitTest withEvent --- hitTestView:(null)
    2020-09-23 16:48:39.445743+0800 TT[72680:1155021] blueView --- hitTest withEvent
    2020-09-23 16:48:39.446166+0800 TT[72680:1155021] blueView --- pointInside withEvent
    2020-09-23 16:48:39.446590+0800 TT[72680:1155021] blueView --- pointInside withEvent --- isInside:1
    2020-09-23 16:48:39.446985+0800 TT[72680:1155021] blueView --- hitTest withEvent --- hitTestView:blueView
    2020-09-23 16:48:39.447281+0800 TT[72680:1155021] orangeView --- hitTest withEvent --- hitTestView:blueView
    2020-09-23 16:48:39.447678+0800 TT[72680:1155021] yllowView --- hitTest withEvent --- hitTestView:blueView
    2020-09-23 16:48:39.448096+0800 TT[72680:1155021] redView --- hitTest withEvent --- hitTestView:blueView
    
    1. 首先是redView的hitTest被调用,hitTest里通过pointInside判断事件在redView相应范围内,向下判断redView的子视图;
    2. 子视图的判断是逆序的,所以先判断yellowView,事件在yellowView响应范围,继续判断yellowView的子视图;
    3. 依然是因为子视图的判断是逆序的,所以先判断purpleView,没有在purpleView响应范围;判断orangeView通过,继续向下判断orangeView的子视图;
    4. 在判断cyanView的时候,因为设置了cyanView.userInteractionEnabled = NO,所以cyanView不能响应事件;
    5. 最后判断blueView是第一响应者。逆着响应者链通知前面的响应者 blueView是第一响应者,blueView的touches事件调用。

    事件拦截

    改变第一响应者

    image.png

    在需要拦截的 view 中重写 hitTest 方法改变第一响应者。
    比如上面示例中,让事件在yellowView中断,由yellowView响应:
    在ChainView中重新实现hitTest:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 
        0.01) {
            return nil;
        }
        if ([self pointInside:point withEvent:event]) {
            if([self.name isEqualToString:@"yellowView"]){
                 return self;
             }
        }
        return nil;
    }
    

    限制第一响应者范围

    下图中的按钮,只有中间圆形部分可以点击,四个角上不响应事件:


    CustomButton

    创建一个继承自UIButton的CustomButton,实现:

    @implementation CustomButton
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
            return nil;
        }
        // 判断触摸位置是否在当前视图内
        if ([self pointInside:point withEvent:event]) {
            //逆序遍历当前对象的子视图
            __block UIView *hitView = nil;
            [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                //坐标转换系 使坐标基于子视图
                CGPoint convertedPoint = [self convertPoint:point toView:obj];
                //调用子视图的 hitTest 方法
                hitView = [obj hitTest:convertedPoint withEvent:event];
                // 如果子视图返回一个view 遍历终止
                if (hitView) {
                    *stop = YES;
                }
            }];
            //如果子视图返回一个view 返回这个view
            if(hitView) {
                return hitView;
            }
            
            //所有子视图都返回 nil, 则返回自身.
            return self;
        }
        return nil;
    }
    
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
        CGFloat x1 = point.x;
        CGFloat y1 = point.y;
        
        CGFloat x2 = self.frame.size.width/2;
        CGFloat y2 = self.frame.size.height/2;
        
        double dist = sqrt((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2));
        
        //在以当前控件中心为圆心,直径为控件宽度的圆内
        if(dist <= self.frame.size.width/2){
            return YES;
        }
        else{
            return NO;
        }
        
    //    return [super pointInside:point withEvent:event];
    }
    

    具体使用和UIButton一样,有兴趣可以自己测试一下效果。

    事件转发

    有时候还需要将事件转发出去。让本不能响应事件的 view 响应事件,最常用的场景就是让子视图超出父视图的部分也能响应事件


    image.png

    cyanView的右下区域超出了父视图 orangeView 的区域,如果不作处理,那么点击超出的区域是无法响应事件的,因为超出区域的坐标不在orangeView的范围内,当执行到orangeView的 pointInside 的时候就会返回 NO。
    想要让超出区域响应事件,就需要重写父视图的 pointInside 或 hitTest 方法让 pointInside 返回 YES 或 让hitTest 直接返回cyanView
    重写hitTest

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        NSLog(@"%@ --- hitTest withEvent", self.name);
        
        if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
            return nil;
        }
        
        // 触摸点在视图范围内 则交由父类处理
        if ([self pointInside:point withEvent:event]) {
            return [super hitTest:point withEvent:event];
        }
    
        // 如果触摸点不在范围内 而在子视图范围内依旧返回子视图
        NSArray<UIView *> * superViews = self.subviews;
        // 倒序 从最上面的一个视图开始查找
        for (NSUInteger i = superViews.count; i > 0; i--) {
            UIView * subview = superViews[i - 1];
            // 转换坐标系 使坐标基于子视图
            CGPoint newPoint = [self convertPoint:point toView:subview];
            // 得到子视图 hitTest 方法返回的值
            UIView * view = [subview hitTest:newPoint withEvent:event];
            // 如果子视图返回一个view 就直接返回 不在继续遍历
            if (view) {
                return view;
            }
        }
        return nil;
    
    

    重写 pointInside 方法原理相同, 重点注意转换坐标系,就算他们不是一条响应链上,也可以通过重写 hitTest 方法转发事件。

    参考: https://www.jianshu.com/p/69c578165054

    相关文章

      网友评论

          本文标题:iOS--事件传递/响应者链

          本文链接:https://www.haomeiwen.com/subject/mizwyktx.html