iOS事件传递和响应者链

iOS中的事件

触摸事件,加速事件(摇一摇),远程控制事件(耳机线控,窗口播放)

以最常见的触摸事件为例,当触摸手机屏幕时操作系统会将这个事件添加到由UIApplication管理的事件队列中(FIFO)UIApplication发送事件到应用程序的主窗口(Window)Window会在图层结构中找到最合适的图层来处理事件。


UIResponder

UIResponder类是专门用来响应用户的操作处理各种事件的,iOS中大部分控件都继承自UIResponder,默认响应事件的方法如下(触摸事件)

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event; //触摸开始,手指接触屏幕
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event; //拖动
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;//触摸结束,手机离开屏幕
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;//中断,被手势或者系统中断

事件传递链

UIApplication传递事件到当前Window是明确的,接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:

hitTest方法继承自UIView(UIWindow是继承自UIView的)。从UIApplication开始调用Window的hitTest方法,默认是递归调用的。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return [super hitTest:point withEvent:event];
}

传递过程如下:

1.系统从UIApplication开始,当前window调用hitTest,hitTest内部会通过以下条件判断window能否能响应事件

  • 不允许交互:userInteractionEnabled=NO
  • 隐藏:hidden = YES
  • 透明度:alpha < 0.01,alpha小于0.01为全透明

2.如果能响应,该函数内部会调用pointInside判断当前触摸点是不是在视图范围内

3.如果在window范围内,开始反向遍历window的子视图列表subviews,遍历的同时会调用subviews中每个子视图的hitTest,判断逻辑和上面的一样,如果找到循环就会停止。

4.此过程会递归,直到找到最外层合适的view,最后返回的view就是最佳响应视图。

一种hitTest可能的实现方式如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!self.userInteractionEnabled|| self.hidden || self.alpha == 0.0){
        return nil;
    }
    if (![self pointInside:point withEvent:event]){
        return nil;
    }
    // 后加入的视图在图层上方,所以反向遍历是合理的
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--)
    {
        UIView *view = self.subviews[i];
        // 坐标的转换
        CGPoint subPoint = [self convertPoint:point toView:view];
        // 继续递归
        UIView *lastView = [view hitTest:subPoint withEvent:event];
        if (lastView)
        {
            return lastView;
        }
    }
    return self;
}

以上,这就是事件传递过程,由内往外的传递过程(从window开始到最外层视图 )

此过程查找结束返回最终的view,UIApplication会调用UIWindow的sendEvent,从而触发对应的响应方法:

PS:这里通过在UIWIndow中重写sendEvent而不调用super的实现,你会发现所有的点击事件都不会触发

- (void)sendEvent:(UIEvent *)event; 

以下是需要注意的点:

  • 实际调用hitTest过程,系统为了找到精准的触摸点会多次调用

  • 如果重写hitTest返回self,传递过程就会终止,当前view就是最合适的view;返回nil,传递也会终止,父视图superView就是最合适的view

  • 如果遍历subviews的过程都没找到合适的view,那么subviews中的子view的hitTest会都会被被调用一次

  • hitTest会调用pointInside判断当前视图是否在点击区域,所以超出父视图边界的控件无法响应事件

  • 同一个view上的两个子视图有重叠部分,后加入的视图会被加入到事件传递链


事件响应链

首先,响应者链中的各个响应者都继承自UIResponder,常见的UIView,viewController,UIWindow以及AppDelegate都继承自UIResponder。响应者链上的响应者在hitTest过程中就已经确定,可以通过迭代nextResponder查看所有的响应者。

事件响应链如下:

  1. 通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者

  2. 如果当前view默认不去重写,或者重写调用了父类的实现,响应就会就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)

  3. 如果上一个响应者是viewController,由viewController的view处理,view本身没处理,则传递给viewController本身

  4. 重复上述过程,直到传递到window,window如果也不能处理,传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,delegate也不处理最后丢弃

以上就是响应者链,事件响应过程是从外向内传递,和事件传递的过程正好相反

通过遍历查找所有响应者:

UIResponder *respon = self;
while (respon) {
    NSLog(@"%@",respon);
    respon = respon.nextResponder;
}

有手势的情况下

手势识别器的作用就是,识别到对应的手势后发送消息给target。iOS中的手势分为两种,Apple文档中有提到:

  • 离散型手势 (UITapGestureRecognizer,UISwipeGestureRecognizer)
  • 持续性手势 (UIPinchGestureRecognizer,UIPanGestureRecognizer,UIRotationGestureRecognizer,UILongPressGestureRecognizer)

离散型手势的情况:

view未添加点击手势,点击一次屏幕会调用touchesBegan和touchesEnde,当我们不考虑touchesEnde的时候可以认为它是一次性的

touchesBegan
touchesEnde

view添加tap手势,点击屏幕会触发手势对应的方法,touchesBegan和touchesCancelled,这里虽然调用了touchesCancelled,但实际上touchesBegan已经触发了

touchesBegan
tap
touchesCancelled

连续型手势的情况:

view未添加连续手势,当手指在屏幕上拖动时,先touchesBegan,然后touchesMoved随着手指拖动持续调用,停止后调用touchesEnde

touchesBegan
touchesMoved
...
touchesEnded

view添加pan拖拽手势,当手指在屏幕上拖动时,touchesBegan和touchesMoved会先调用,当pan手势方法触发以后,touchesMoved将不再出现,同时touchesCancelled也触发了

touchesBegan
touchesMoved
pan //识别到 pan之后,就只有pan手势会响应
touchesCancelled
pan
pan
...

以下结论主要针对连续型手势:

  • 若手势成功识别事件,就会取消第一响应者view对事件的响应(touchesCancelled)
  • 若手势没能识别事件,第一响应者view就会接手事件的处理

通过断点在sendEvent:处查看UIEvent事件,在event->_allTouchesMutable->_gestureRecognizers手势中可以看到当前touch对象中包含所有的手势对象,通过断点可以看到数组中第一个手势的对象地址0x10510a2d0正是添加的tap手势的地址。因此可以说明,手势会先响应

touch的gestureRecognizers数组:

_gestureRecognizers __NSArrayM *    @"6 elements"   0x0000000282bd46f0
[0] UITapGestureRecognizer *    0x10510a2d0 0x000000010510a2d0
[1] UIPanGestureRecognizer *    0x10510a3f0 0x000000010510a3f0
[2] UITapGestureRecognizer *    0x10510a1b0 0x000000010510a1b0
[3] UITapGestureRecognizer *    0x105107f70 0x0000000105107f70
[4] _UISystemGestureGateGestureRecognizer * 0x105011020 0x0000000105011020
[5] _UISystemGestureGateGestureRecognizer * 0x10500ff50 0x000000010500ff50
添加的tap手势对象:
[2890:328171] tap:; target= <(action=tap:, target=)>>

有UIControl(按钮)的情况

以Button为例,给Button添加添加tap手势和TouchDown类型target,结果和上面的例子一样,对于一次性手势都会响应

touchesBegan
TouchDown
tap
touchesCancelled

给Button只添加TouchDragInside类型target,touchesMoved和TouchDragInside都会响应

touchesMoved
TouchDragInside

给Button添加pan手势和TouchDragInside类型target,系统识别到pan手势后就会touchesCancelled,只有手势pan会执行

touchesMoved
TouchDragInside
pan //识别到 pan之后,就只有pan手势会响应
touchesCancelled
pan
pan
...

你可能感兴趣的:(iOS事件传递和响应者链)