阅读 326

iOS 事件传递、响应者链条和基础事件知识

响应者链和事件传递

事件传递

事件是怎么产生和传递的?
  1. 发生触摸事件事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
  2. 先将事件对象由下往上传递(由父控件传给子控件)。
  3. 如果找到最合适的控件来处理调用最合适的控件的touches...(touchesBegan、touchesMoved、touchedEnded)方法。如果调用了[super touch…],就会将事件顺着响应者链往上传递,传给上一个响应者,接着上一个响应者就会调用touches...方法。
  4. 如果没有找到合适的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给 UIApplication。如果 UIApplication 不能处理事件,则将其丢弃。
  • 如何找到最合适的控件来处理事件?
    1. 自己是否能接收触摸事件? UIView不接收触摸事件的三种情况不接收用户交互:
      • userInteractionEnabled = NO隐藏
      • hidden = YES;
      • 透明:alpha = 0.0 ~ 0.01;
    2. 触摸点是否在自己身上? 判断触摸点在不在自己身上,view有一个方法pointInside,返回NO则不在自己身上,那就不再遍历子控件,返回YES,代表在自己身上,那就继续遍历子控件,从后往前遍历子控件,重复前面两个步骤如果没有符合条件的子控件,那么自己就是最适合处理的控件找到“最合适” 接收的控件后,调用控件touchesBegan,touchesMoved,touchedEnded的方法。
    3. 从后往前遍历子控件,重复前面的两个步骤
    4. 如果没有符合条件的子控件,那么就自己最适合处理
事件传递示例

触摸事件的传递是从父控件传递到子控件,如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件。

  1. 点击了绿色的view:UIApplication -> UIWindow -> 白色 -> 绿色
  2. 点击了蓝色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色
  3. 点击了黄色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色

事件响应

概念
  • 响应者对象
    在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。

  • 什么是响应者? 继承了UIResponder的对象就是响应者。

  • 什么是上一个响应者?
    如果当前这个view是控制器的view,那么控制器就是上一个响应者;
    如果当前这个view不是控制器的view,那么父控件就是上一个响应者。

  • 响应者链条是什么
    它是一种事件处理机制,由多个响应者对象连接起来的层次结构,使得事件可以沿着这些对象进行传递。利用响应者链条我们可以通过调用touches的super 方法,让多个响应者同时响应该事件。

  • 响应者链条作用
    利用响应者链条可以让多个控件处理同一个“触摸事件”; 在最后合适的控件调用super的touchBegan方法,这样就将事件传给上一个响应,上一个响应者也可以处理事件了。

响应者链的事件传递过程

如果view的控制器存在,就传递给控制器。 如果控制器不存在,则将其传递给它的父视图在视图层次结构的最顶级视图。 如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。 如果window对象也不处理,则其将事件或消息传递给UIApplication对象。 如果UIApplication也不能处理该事件或消息,则将其丢弃。

响应者链条示意图

  1. 系统首先检查当前触摸到的视图是否响应事件,如果响应事件传递结束,否则转步骤2
  2. 系统检查当前触摸到的视图的控制器,如果控制器响应则事件传递结束;如果该视图没有控制器或者控制器不响应该事件,则转步骤3
  3. 系统检查父视图,再检查父视图的控制器...
  4. 最后,如果最顶层的视图/控制器也不响应则交给window
事件响应链 hitTest:withEvent: 伪代码
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (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 = (int)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) {
            // 如果能找到最合适的view
            return fitView;
        }
     }
     // 4.没有找到更合适的view,也就是没有比自己更合适的view
     return self;
 }
复制代码

iOS中的各种事件

事件可以分为3大类型

  • 触摸事件
  • 加速计事件 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event; - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event; - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  • 远程控制事件 - (void)remoteControlReceivedWithEvent:(UIEvent *)event;

触摸事件(UITouch)

UITouch的属性
  • @property(nonatomic,readonly,retain) UIWindow *window; 触摸产生时所处的窗口
  • @property(nonatomic,readonly,retain) UIView *view; 触摸产生时所处的视图
  • @property(nonatomic,readonly) NSUInteger tapCount; 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
  • @property(nonatomic,readonly) NSTimeInterval timestamp; 记录了触摸事件产生或变化时的时间,单位是秒
  • @property(nonatomic,readonly) UITouchPhase phase; 当前触摸事件所处的状态
UITouch的方法
  • - (CGPoint)locationInView:(UIView *)view; 返回值表示触摸在view上的位置,这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0));调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置。
  • - (CGPoint)previousLocationInView:(UIView *)view; 该方法记录了前一个触摸点的位置。
UIEvent

UIEvent:称为事件对象,记录事件产生的时刻和类型。 每产生一个事件,就会产生一个UIEvent对象。 UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)。 常见属性

  • @property(nonatomic,readonly) UIEventType type;@property(nonatomic,readonly) UIEventSubtype subtype; 事件类型
  • @property(nonatomic,readonly) NSTimeInterval timestamp; 事件产生的时间
UITouch的作用

保存着跟手指相关的信息,比如触摸的位置、时间、阶段。 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。 当手指离开屏幕时,系统会销毁相应的UITouch对象。 提示:iPhone开发中,要避免使用双击事件!

触摸过程

一次完整的触摸过程,会经历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个触摸事件处理方法中,都有NSSet *touchesUIEvent *event两个参数。 具体情况说明: 当用户用手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。一根手指对应一个UITouch对象。

  • 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数。
  • 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象。
  • 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。
  • 根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸。
  • 提示:touches中存放的都是UITouch对象
UITouch 代码举例
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    _imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    _imageView.image = [UIImage imageNamed:@"player2"];
    [self.view addSubview:_imageView];
    //userInteractionEnabled是从UIView继承来的一个属性,对很多视图来说其默认值都是YES,但是包括UIImageview在内的几个少数视图其默认值是NO
    _imageView.userInteractionEnabled = YES;//允许与用户交互(才能响应触摸事件)
    //1.一个视图要想与用户交互,除了自己的userInteractionEnabled为YES,其父视图的userInteractionEnabled也得为YES
    //2.一个视图如果坐标超出了父视图也不能与用户进行交互
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoLight];
    btn.frame = CGRectMake(20, 20, 50, 50);
    [_imageView addSubview:btn];//例子
    [btn addTarget:self action:@selector(onBtnClick) forControlEvents:UIControlEventTouchUpInside];
    //btn.userInteractionEnabled = NO;
}

-(void)onBtnClick {
    NSLog(@"按钮被戳了");
}

// 触摸开始,任何一个视图或视图控制器都有的方法,当触摸的时候系统自动调用
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"触摸开始");
    for (UITouch *touch in touches) {
        CGPoint p = [touch locationInView:self.view];
        _imageView.center = p;
    }
}
// 触摸移动,按下手指并移动时系统自动调用
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"手指动了");
    for (UITouch *touch in touches) {
        //touch.view表示当前触控到的视图
        NSLog(@"touch.view:%@",touch.view);
        if (touch.view == _imageView) {
            NSLog(@"摸到小人了");
            //[touch locationInView: ]用于获取当前触摸的点相对于某个视图的坐标
            CGPoint p = [touch locationInView:self.view];
            _imageView.center = p;
        }
    }
}
// 触摸移动,按下手指并离开时系统自动调用
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"触摸停止");
}
// 由于某种原因(比如系统内存严重不足)触摸事件被系统强行取消时调用
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {

}
复制代码
UIGestureRecognizer(手势识别器)

利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势。 UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势 UITapGestureRecognizer(敲击) UIPinchGestureRecognizer(捏合,用于缩放) UIPanGestureRecognizer(拖拽) UISwipeGestureRecognizer(轻扫) UIRotationGestureRecognizer(旋转) UILongPressGestureRecognizer(长按)

  • 手势识别的状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    // 没有触摸事件发生,所有手势识别的默认状态
    UIGestureRecognizerStatePossible,
    // 一个手势已经开始但尚未改变或者完成时
    UIGestureRecognizerStateBegan,
    // 手势状态改变
    UIGestureRecognizerStateChanged,
    // 手势完成
    UIGestureRecognizerStateEnded,
    // 手势取消,恢复至Possible状态
    UIGestureRecognizerStateCancelled, 
    // 手势失败,恢复至Possible状态
    UIGestureRecognizerStateFailed,
    // 识别到手势识别
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
复制代码
  • 具体代码
- (void)creatGestureRecognizer {
    //点击手势
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(onTap:)];
    //tap.numberOfTapsRequired = 2;//点几下才响应,默认值是1
    //tap.numberOfTouchesRequired = 2;//几根手指点击,默认值也是一
    //给视图加手势
    [_view addGestureRecognizer:tap];//会增加tap的引用计数
    //缩放手势(捏合手势)
    UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(onPinch:)];
    [_view addGestureRecognizer:pinch];
    pinch.delegate = self; //需要系统同时识别缩放和旋转,设置代理
    //旋转手势
    UIRotationGestureRecognizer *rot = [[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(onRotata:)];
    [_view addGestureRecognizer:rot];
    rot.delegate = self;//需要系统同时识别缩放和旋转,设置代理
    //滑动手势,每个滑动只支持(上下左右)一个方向,如果想支持多个方向就要建多个滑动手势
    UISwipeGestureRecognizer *swipeDown = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(onSwipe:)];
    swipeDown.direction = UISwipeGestureRecognizerDirectionDown;//设置滑动方向为向下
    [_view addGestureRecognizer:swipeDown];
    [swipeDown release];

    UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(onSwipe:)];
    swipeUp.direction = UISwipeGestureRecognizerDirectionUp;//设置滑动方向为向上 
    [_view addGestureRecognizer:swipeUp];
    //拖动手势,一个视图不能同时响应拖动滑动两种手势,如果两个都加响应后加的那个
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(onPan:)];
    [_view addGestureRecognizer:pan];
    //长按手势
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(onLongPress:)];
    [_view addGestureRecognizer:longPress];
    [longPress release];
}

- (void)onTap:(UITapGestureRecognizer *)sender {
    NSLog(@"点击手势");
    //用于获取手势相对于某视图的坐标
    //sender.view表示sender所在的视图
    CGPoint p = [sender locationInView:sender.view];
    NSLog(@"%f,%f",p.x,p.y);
}

- (void)onPinch:(UIPinchGestureRecognizer *)sender {
    NSLog(@"缩放手势,比例:%f",sender.scale);
    //sender.scale表示缩放比例
    sender.view.transform = CGAffineTransformScale(sender.view.transform, sender.scale, sender.scale);
    sender.scale = 1;//告诉缩放手势以当前的比例为基准(默认值是以手势开始时的比例为基准)
}

- (void)onRotata:(UIRotationGestureRecognizer *)sender {
    NSLog(@"旋转手势:%f",sender.rotation);//表示手势旋转弧度
    sender.view.transform = CGAffineTransformRotate(sender.view.transform, sender.rotation);
    sender.rotation = 0;//表示告诉旋转手势以当前的弧度为基准
}

- (void)onSwipe:(UISwipeGestureRecognizer *)sender {
    NSLog(@"滑动手势");
    if (sender.direction == UISwipeGestureRecognizerDirectionUp) {
        CGPoint p = sender.view.center;
        p.y -= 10;
        sender.view.center = p;
    }else {
        CGPoint p = sender.view.center;
        p.y += 10;
        sender.view.center = p;
    }
}

- (void)onPan:(UIPanGestureRecognizer *)sender {
    //sender.state表示手势的状态:手势开始,结束,正在进行等
    NSLog(@"拖动手势");
    if (sender.state == UIGestureRecognizerStateBegan) {
        NSLog(@"手势开始");
    }else if (sender.state == UIGestureRecognizerStateChanged) {
        NSLog(@"手势进行中");
    }else if (sender.state == UIGestureRecognizerStateEnded) {
        NSLog(@"手势结束");
    }
    CGPoint p = [sender locationInView:self.view];
    sender.view.center = p;
}

- (void)onLongPress:(UILongPressGestureRecognizer *)sender {
    NSLog(@"长按手势");
    if (sender.state == UIGestureRecognizerStateBegan) {
        NSLog(@"手势开始");
    }else if (sender.state == UIGestureRecognizerStateChanged) {
        NSLog(@"手势进行中");
    }else if (sender.state == UIGestureRecognizerStateEnded) {
        NSLog(@"手势结束");
    }
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;//任何两个手势都能同时识别
}
复制代码