iOS开发之中级iOS DeveloperiOS程序猿

响应者链条和手势识别器

2017-05-03  本文已影响27人  asaBoat

相关资源

GitHub粒子发射和复制图层示例
GitHub粘性控件示例
GitHub弹性动画
CALayer分析
CAEmitter分析

简介

  1. Responder Chain for Action Messages

    macOS Cocoa Application,macOS开发使用的到的响应者知识。

  2. Responder Chain for Event Messages

    UIApplication,iOS开发使用的到的响应者知识,这里重点介绍。应用程序接收和处理事件使用响应对象。接受对象是任何实例UIResponder类和普通子类,包括UIView,UIViewController,UIApplication。UIKit自动管理大多数相应相关的行为,包括事件是如何从一个响应者传送到下一个响应者。当然,您可以修改默认行为以更改事件在应用程序中的传递方式。

    UIKit将大部分事件(很多种类的事件)传递给最恰当的响应者对象。如果对象不处理该事件,UIKit转发给在活跃的响应者链条中的下一个响应者,这是一个动态的应用程序的响应对象的配置。因为它是动态的,在你的应用程序中没有单一的响应链。然而,它很容易确定在链中的下一个响应器,因为事件总是从特定的响应对象流到更一般的响应对象。例如,一个视图的下一个响应者要么是它的父视图要么是管理它的视图控制器。事件一直向响应链流,直到它们被处理为止。

    分析:UIKit帮我们做了默认的事件传递,然而我们可以修改默认行为,比如,nextResponder属性、在touchesBegan: withEvent:中修改。

事件类型

对于每一个事件,UIKit会制定一个第一响应者(first responder ),并且把事件首先传递给该对象。第一响应者根据以下事件类型变化而变化。

  1. Touch events:第一响应者就是触摸发生所在的控件。
  2. Press events:第一响应者就是获取焦点的响应者(see App Programming Guide for tvOS)
  3. Motion events:第一响应者就是你指定处理事件的响应者.
  4. Shake-motion events. 第一响应者就是你(or UIKit)指定处理事件的响应者。
  5. Remote-control events: 第一响应者就是你(or UIKit)指定处理事件的响应者。
  6. Editing-menu messages:第一响应者就是你(or UIKit)指定处理事件的响应者。For information about the UIKit editing commands, see UIResponderStandardEditActions.

获取Touch事件的第一响应者

UIKit使用基于视图View的hit测试(点击测试)来确定触摸事件的发生地。具体来说,UIKit会对比触摸位置和View对象的范围(这里理解为frame)。UIView对象的hitTest:withEvent:方法会穿越视图层次寻找最深的包含指定触摸的子视图。该视图成为触摸事件的第一响应者。

1.图片分析

对于触摸事件的响应,首先要找到能够响应该事件的对象,iOS是用hit-testing 来找到哪个视图被触摸了(hit-test view),也就是以keyWindow为起点,hit-test view为终点,逐级调用hitTest:withEvent。


MJ大神的图

2.hitTest:withEvent:方法内部的处理流程:

  1. 首先判断自己能否接收事件,以下情况视图的hitTest:withEvent:方法会返回nil,导致自身和其所有子视图不能被hit-testing发现:

    • 隐藏(hidden=YES)的视图
    • 禁止用户操作(userInteractionEnabled=NO)的视图
    • 视图View.alpha < 0.01的视图
    • 视图超出父视图的区域
  2. 再用调用pointInside:withEvent:判断触摸点是否在当前视图内:

    • 如果返回YES,那么该视图的所有子视图调用hitTest:withEvent,调用顺序由层级低到高(top->bottom)依次调用。
    • 如果返回NO,说明触摸不在控件内,那么hitTest:withEvent返回nil,该视图的所有子视图的分支全部被忽略。
  3. 在此视图的pointInside:withEvent:返回YES的情况下,并且他的所有子视图hitTest:withEvent:都返回nil,或者该视图没有子视图,那么该视图的hitTest:withEvent:返回自己。

  4. 在此视图的pointInside:withEvent:返回YES的情况下,如果子视图的hitTest:withEvent:返回非空对象,那么当前视图的hitTest:withEvent:也返回这个对象,也就是沿原路回推,最终将hit-test view传递给keyWindow。

3 代码示例(系统默认原理模拟)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判断下自己能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    
    // 2.判断下点在不在当前控件上
    if ([self pointInside:point withEvent:event] == NO) return  nil; // 点不在当前控件
        
    
    // 3.从后往前遍历自己的子控件
    int count = self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        // 获取子控件
       UIView *childView = self.subviews[i];
        
        // 把当前坐标系上的点转换成子控件上的点
      CGPoint childP =  [self convertPoint:point toView:childView];
        
       UIView *fitView = [childView hitTest:childP withEvent:event];
        
        if (fitView) {
            return fitView;
        }  
    }
    
    // 4.如果没有比自己合适的子控件,最合适的view就是自己
    return self;
    
}

// 判断下当前这个点在不在方法调用者上(我们还可以重写此方法改变寻找结果)
// 使用注意点:点必须是方法调用者上的坐标系,才会判断准备
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//    [super pointInside:point withEvent:event];
//    return NO;
//}

Tip:我们可以通过自定义- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 方法忽略前两个判断做到意想不到的效果。代码如下:

// 处理不在此控件上但是在子控件上的点(超出本控件的子控件的部分)
 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    
    // 判断下point点在不在特定子控件上(这个点明显不在此控件上)
    CGPoint chatP = [self convertPoint:point toView:子控件];
    if ([子控件 pointInside:chatP withEvent:event] == YES) {
        return self.chatView;
    } 
    return [super hitTest:point withEvent:event];
    
}

响应者链条(responder chain)

responderChain.png
  1. 事件往上传递(nextResponder)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:  (UIEvent *)event {
  [super touchesBegan:touches withEvent:event];
  NSLog(@"%s",__func__);
  NSLog(@"nextResponder = %@",self.nextResponder);
}
  1. 重置nextResponder
- (UIResponder *)nextResponder {
   return [super nextResponder];
   }
  1. 我们可以通过以下两种方式中断响应者链条

    - (void)touchesBegan:(NSSet<UITouch *> *)touches    withEvent:(UIEvent *)event {
    NSLog(@"nextResponder = %@",self.nextResponder);
    }
    
     - (UIResponder *)nextResponder {
    return nil;
    }
    
    

手势识别器(UIGestureRecognizer)

简介

手势识别器是处理view中touch or press events的一种简单方式,我们可以为一个控件添加多种识别器。它把事件和处理逻辑封装成了一种简单方便的形式。

gestureRecognizer.png

手势识别器有两种类型:离散和连续。手势一旦被识别后,一个离散手势识别器会调用你的动作方法。一个连续的手势识别器可以多次调用你的动作方法,包括手势的开始和结束,以及每次跟踪事件的细节变化。例如,一个uipangesturerecognizer对象每当一个触摸事件的位置变化都会调用你的动作方法。
界面生成器(nterface Builder)内部包含每个标准UIKit手势识别的对象。它还包括一个自定义手势识别对象,您可以使用它作为你的自定义的UIGestureRecognizer子类。

分析

  1. 由于UIGestureRecognizer处理的是事件相关,所以它也是遵循hitTest: withEvent:方法找到第一响应者。
  2. 响应者链条这里并不适用UIGestureRecognizer。如果一个控件成为第一响应者,那么如果它身上没有添加相应的手势识别器,就会查找它的父控件(而不是它的nextResponder,通过中断响应者链条测试)能不能match相应的UIGestureRecognizer。
上一篇下一篇

猜你喜欢

热点阅读