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

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

一. 事件(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 *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)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  *allTouches;

- (nullable NSSet  *)allTouches;
- (nullable NSSet  *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet  *)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;
}
【iOS小结】事件和响应者链_第1张图片
事件的传递.png

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

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


【iOS小结】事件和响应者链_第2张图片
响应者链.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小结】事件和响应者链)