【iOS小结】事件和响应者链

作者: WellsCai | 来源:发表于2017-12-23 08:44 被阅读46次

    之前面试问到一个响应者链的问题,结果让我很尴尬。于是,就想着写篇关于响应链的总结。当然,响应者链也包含事件、响应者的知识点,所以就一起总结复习一下。

    一. 事件(UIEvent)

    一个UIEvent对象代表iOS中的事件(简单理解,事件就是用户对设备的操作)。事件分为三类:触摸事件、晃动事件、远程控制事件(比如耳机按钮操控)。

    要学习UIEvent之前,我们先来简单了解一下UITouch(你可以理解为触碰点)。
    当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象。UITouch的作用是保存着跟手指相关的信息,比如触摸的位置、时间、阶段。当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置,当手指离开屏幕时,系统会销毁相应的UITouch对象。

    UITouch常用属性和方法有:

    @property(nonatomic,readonly) NSTimeInterval      timestamp;//记录了触摸事件产生或变化时的时间
    @property(nonatomic,readonly) UITouchPhase        phase;//当前触摸事件所处的状态
    @property(nonatomic,readonly) NSUInteger          tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
    @property(nullable,nonatomic,readonly,strong) UIWindow   *window;//触摸产生时所处的窗口
    @property(nullable,nonatomic,readonly,strong) UIView    *view;//触摸产生时所处的视图
    
    //触碰点在所处view的位置(以view的左上角为原点(0, 0))
    //调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
    - (CGPoint)locationInView:(nullable UIView *)view;
    //取得移动的前一个位置
    - (CGPoint)previousLocationInView:(nullable UIView *)view;
    

    以常见的触摸事件为例:
    一个触摸事件包含一个或者多个手指,每个手指是一个UITouch对象;每产生一个事件,就会产生一个UIEvent对象,用于记录事件产生的时刻和类型。
    一次完整的触摸过程,会经历3个状态(开始,移动,结束。也可能会有取消)

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
    

    4个触摸事件处理方法中,都有touches和event两个参数。一次完整的触摸过程中,只会产生一个事件对象,所以4个触摸方法都是同一个event参数。如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象。简单点说,一次触摸事件就是一个event,touches的数量取决于你用了几个手指。

    同样的,我们也看看UIEvent常用属性和方法,最主要的还是关注里面的touches:

    @property(nonatomic,readonly) UIEventType     type;
    @property(nonatomic,readonly) UIEventSubtype  subtype;
    @property(nonatomic,readonly) NSTimeInterval  timestamp;
    @property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
    
    - (nullable NSSet <UITouch *> *)allTouches;
    - (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
    - (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
    

    二. 响应链(UIResponder Chain)

    在讲响应链之前,我们先来讲讲事件的处理机制(事件的传递)。

    有这么一个问题:当我们触碰屏幕时,会产生一个事件(UIEvent),系统是怎么找到查找事件触发者?(或者这么想:当我们触碰屏幕时,程序是怎么知道我们在触碰哪一个控件的?),这就涉及到事件的分发。
    事件的传递涉及到了UIView中的两个方法:

    //询问当前点击事件最优响应者是谁(nil为没有最优响应者)
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
    
    //判断当前点击是否在控件的Bounds之内(用来判断某个view能不能成为最优响应者)
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
    

    在iOS中发生触摸后,事件会加入UIApplication事件队列,UIApplication会从事件队列取出最前面的事件并分发处理,通常,先发送事件给应用程序的主窗口(UIWindow),主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView(事件触发视图)来处理触摸事件。

    具体步骤如下:
    ① 在顶级视图(key window的视图)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内。

    ② 如果返回NO,那么hitTest:withEvent:返回nil(说明这个点都不在我的视图里,我这边肯定没有你的最优响应者)。

    ③ 如果返回YES,那么它会向当前视图的所有子视图(key window的子视图)发送hitTest:withEvent:消息(我这边有最优响应者,要嘛是我,要嘛是我的子视图,我再帮你找找)。遍历所有子视图的顺序是从subviews数组的末尾向前遍历(从界面最上方开始向下遍历)。

    ④ 如果subview(没有子视图了)的hitTest:withEvent:返回非空对象则顶级视图的hitTest:withEvent:也返回此对象,处理结束(注意所有视图的hitTest:withEvent:都是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。如果该视图的hidden=YESuserInteractionEnabled=NO或者alpha<0.1都会直接返回nil)。

    ⑤ 如果所有subview遍历结束仍然没有返回非空对象,则顶级视图的hitTest:withEvent:返回它自己(儿子都不是最优响应视图,只能我这当爹的来了)。

    具体的一个伪代码就是如下:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        //先判断alpha,userInteractionEnabled,hidden
        if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) {
            return nil;
        }
        //再判断是不是在我的范围内
        if (![self pointInside:point withEvent:event]) {
            return nil;
        }
        
        //在我的范围内,我就问我的儿子,有一个儿子回答是它,我就返回它,如果都没人回答,我就回答是我了。
        __block UIView *hitView = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
            hitView = [subview hitTest:point withEvent:event];
            if (hitView) {
                *stop = YES;
            }
        }];
        return hitView ? : self;
    }
    
    事件的传递.png

    知道事件怎么传递的,那再让我们来探探我们的重点:响应者链

    我们知道在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。
    当然,在iOS中不是所有的对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件,称之为响应者对象,比如UIApplication、UIViewController、UIView都继承自UIResponder。


    响应者链.png

    之前提到的事件的传递,其实就是在找事件触发者的过程。但是事件触发者(触摸对象)并非就是事件的响应者。
    比如这么一个例子:在视图控制器放一个UIImageView,通过touchesMoved:来控制imageView的位置。

    -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
        //取得一个触摸对象(对于多点触摸可能有多个对象)
        UITouch *touch=[touches anyObject];
        //NSLog(@"%@",touch);
        
        //取得当前位置
        CGPoint current=[touch locationInView:self.view];
        //取得前一个位置
        CGPoint previous=[touch previousLocationInView:self.view];
        
        //移动前的中点位置
        CGPoint center=_image.center;
        //移动偏移量
        CGPoint offset=CGPointMake(current.x-previous.x, current.y-previous.y);
        
        //重新设置新位置
        _image.center=CGPointMake(center.x+offset.x, center.y+offset.y);
    }
    

    你会发现我们即使在imageView上移动,也会执行视图控制器的touchesMoved:,这也意味着这时的响应者是视图控制器,而不是触摸对象imageView。这也说明了触摸对象imageView不自己处理事件,把它转移给视图控制器。为什么会这样呢?
    其实,当某个视图的属性满足这样条件时,意味着它不处理事件,会把事件转移给响应者链的下一个去处理。对于视图控制器这种,你没有实现开始触摸方法,就意味着你不处理事件。

    • userInteractionEnabled = NO
    • hidden = YES
    • alpha = 0~0.01
    • 没有实现开始触摸方法(只针对视图控制器这种类型的响应者)

    例子中的imageView的userInteractionEnabled默认为NO,所以会把事件转移给响应者链的下一个(self.view),self.view的userInteractionEnabled默认为NO,也不处理事件,就继续转移给下一个(ViewController),刚好ViewController实现了触摸方法,可以处理事件。要是ViewController也没用实现了触摸方法的话,就会继续传递下去。
    要是例子中的imageView换成button就不一样了,button的userInteractionEnabled默认为YES,自己就能处理事件,就没有ViewController什么事了(也就是touchesMoved:不会执行了)。

    所以,一个完整的流程是这样的:
    ① 当一个事件发生后首先看initial view(触摸对象)能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)。

    ② 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递。(对于视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理)。

    ③ 一直到window,如果window还是不能处理此事件则继续交给application(UIApplication单例对象)处理,如果最后application还是不能处理此事件则将其丢弃。

    三. 总结

    以常见的触摸操作来说,我们来串一下:

    事件

    我们主要知道事件(UIEvent)就是保存这次触摸的信息(时间,手指,类型等),当然手指的信息(位置,所处视图,点击次数等)是UITouch来保存。一次触摸从开始到结束就是一个事件,不管你用几个手指。

    响应者链

    触碰操作产生一个事件,事件是通过UIApplication分发找到对应的最优响应者(触摸对象)。找到归找到,又不一定是这个触摸对象来处理事件,要是你不能处理,你就交给你的上级(响应者链的上一级),直到有人处理,或者都没人处理。

    相关文章

      网友评论

        本文标题:【iOS小结】事件和响应者链

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