×

事件传递

96
Little_Dragon
2015.10.07 22:15* 字数 2058

iOS的事件可以分为三类:触摸事件,加速计事件,远程控制事件

iOS中不是任何对象都能处理对象,只有继承了UIResponder的对象才能接收并处理事件. ---->响应者对象

我们可以观察到 UIView就是继承于UIResponder,所以所有可看到的控件都是可以接收到事件的.

UIApplication,UIViewController,UIView都继承自UIResponder,因此它们都是响应者对象, 都能够就收并处理事件

UIResponder内部提供了以下方法专门用来事件的处理

1.触摸事件:
--->我们经常用来测试用的方法:
- (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触摸事件被打断

2.加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event 加速计事件开始
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event 加速计事件结束
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event 加速计事件被打断

3.远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event 接收远程控制事件

UIView的触摸事件处理

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event触摸事件开始[一根或者多根手指开始触摸view,系统会自动调用]
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event拖拽事件[在view上移动的时候,调用这方法]
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event触摸结束事件[手指离开view,调用这个方法]
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event触摸事件被打断[在触摸结束前,被系统事件打断,(如打电话,短信等等),系统自动调用]

注意:touches中存放的都是:UITouch对象.

UITouch的属性:

1.触摸产生时所处的窗口
@property(nonatomic,readonly,retain)UIWindow *window;
2.触摸产生时所处的视图
@property(nonatomic,readonly,retain)UIView *view;
3.短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击[一般不常用]
@property(nonatomic,readonly)NSUInteger tapCount;
4.记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly)NSTimeInterval timestamp;
5.当前触摸事件所处的状态
@property(nonatomic,readonly)UITouchPhase phase;

UITouch的方法

- (CGPoint)locationInView:(UIView *)view
1.返回值表示触摸在view的位置
2.如果传入的view为nil的话,返回的触摸点在UIWindow的位置

- (CGPoint)previousLocationInView:(UIView *)view
记录前一个触摸点的位置

代码实现简单的拖拽,来解释这个属性和方法

// 验证点击事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    NSLog(@"%s",__func__);
}
// 验证拖拽移动事件
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch =  [touches anyObject];
    // 得到当前点按的位置,是针对与self(红色的view)来说的
    CGPoint curP = [touch locationInView:self];
    // 得到前一次点得位置
    CGPoint lastP = [touch previousLocationInView:self];
//    NSLog(@"%s",__func__);
//    NSLog(@"%@",touch);
    NSLog(@"%@-----%@",NSStringFromCGPoint(curP),NSStringFromCGPoint(lastP));
    // 计算 x轴方向的偏移量(都是针对于上一次,也就是针对与上一次的改变)
    CGFloat offsetX = curP.x - lastP.x;
    // 计算 y轴的偏移量
    CGFloat offsetY = curP.y - lastP.y;
    // 在原有的基础上进行平移
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

// 点击结束后调用
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{

    NSLog(@"%s",__func__);
}
// 程序被迫中断时调用,如电话打进了。
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{

    NSLog(@"%s",__func__);
}

UIEvent

每产生一个事件,就会产生一个UIEvent对象, 内部记录着事件产生的时刻和类型

常见属性

1.事件类型
@property(nonatomic,readonly)UIEventType type;
@property(nonatomic,readonly)UIEventSubtype subtype;
2.事件产生的时间
@property(nonatomic,readonly)NSTimeInterval timestamp;
lUIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)

touches和event参数

一次完整的触摸过程,会经历3个状态:
1触摸开始:
- (void)touchesBegan:(NSSet *)touches withEvent (UIEvent *)event
2.触摸移动:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
3.触摸结束:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

4.触摸取消(可能会经历):
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数

5.一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数

5.1如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
5.2如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
5.3根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸

事件的产生和传递

1.发生触摸事件后,系统会将该事件加入一个UIApplication管理的事件队列中
2.UIApplication会从事件队列中取出最前面的事件,并将事件分发下去进行处理.通常先发送事件给应用程序的主窗口(keyWindow)
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件., 这也是整个事件处理过程中的第一步,也是最重要的一部
4.找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理

5.如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件

那么如何找到最合适的控件来处理事件?
1.先查看自己是否能接收触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复1,2步骤
4.如果没有符合条件的子控件,那么就是自己最适合处理

图形说明:


Snip20151007_3.png

触摸事件的传递;
1.点击了绿色的view
UIApplication-->UIWidow(keyWindow)-->白色的view-->绿色
2.点击了蓝色view
UIApplication-->UIWindow-->白色-->橙色-->蓝色
3.点击了黄色view
UIApplication-->UIWindow-->白色-->橙色-->蓝色-->黄色

UIView不接收触摸事件的三种情况:

1.不接收用户交互:
userInteractionEnabled = NO
2.隐藏
hidden = YES
3.透明度(近乎透明)
alpha = 0.0~0.01之间
提示: UIImageView的userInteractionEnabled默认为NO,因此UIImageView以及它的子控件默认是不接收触摸事件的

模拟苹果来完成它的底部实现过程,

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

    NSLog(@"%@----%s",[self class],__func__);

}

// ponit是方法调用者坐标系上的触摸点位置--->当触摸事件产生时,会调用这个方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判断能否接收触摸事件 (继承与uirespond)(隐藏,交互,透明度)
    if (self.hidden == YES || self.userInteractionEnabled == NO || self.alpha < 0.01 )
        return nil;
    
    // 触摸点的位置在不在控件上
    if (![self pointInside:point withEvent:event]) return nil;
    
    // 如果在控件上,则遍历控件的子控件(由后往前)
    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];
        // 如果找到合适的view,则返回合适的view
        if (fitView) {
            return fitView;
        }
        
    }
    // 没有合适的view 则返回自己。
    return self;
}
点击事件.gif

控制台的打印


Snip20151007_5.png

既然已经知道了,事件的传递过程,那么我们完全可能做出一些特殊的行为,去拦截操作, 一下两个方法都可以进行事件的拦截

// 判断触摸点在view上,是否能够接收事件
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // 将点得坐标系改变
    CGPoint blueP = [self convertPoint:point toView:_blueBtn];
    // 判断是否在蓝色按钮上 ,
    if ([self.blueBtn pointInside:blueP withEvent:event]) {
    // 如果在,则不让它进行相应。
        return NO;
    }   
    // 判断继续(由系统自行判断)
    return [super pointInside:point withEvent:event];
}

// 重写hittest 方法,可以拦截监听。 或者说,想让谁听,谁就听
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 将点得坐标系改变
    CGPoint blueP = [self convertPoint:point toView:_blueBtn];
   // 判断是否在蓝色按钮上 ,
    if ([self.blueBtn pointInside:blueP withEvent:event]) {
        return _blueBtn;
    }
    // 继续判断(由系统内部做决定)
    return [super hitTest:point withEvent:
            event];
    //return self; 不能返回self,这样会造成,无论条件是否符合(能不能监听,点有没有在它上面),点都在yellow上
}
Snip20151007_6.png

点击拦截.gif

层级结构就是黄色的view蓝色的按钮上面
按照,事件传递的规律,点击黄色区域应该是view做出响应,当然点击覆盖在按钮上的区域,也应该是view作出相应,但是由于我们将事件传递做出了改变,所以发生了事件拦截现象. 按钮能够监听点击事件

触摸事件处理的详细过程

1.用户点击了屏幕后产生一个触摸事件,经过一系列传递以后,会找到最合适的视图控件来处理这个事件
2.找到最合适的视图空间后,就会调用控件的touches方法来做具体事件处理
3.这些touches方法的默认做法是将事件顺着响应链条向上传递,将事件交给上一个响应者进行处理

响应者链条示意图

1.响应者链条:是由多个响应者对象连接起来的链条
2.作用:能很清楚的看见每个响应者之间的对象,并且可以让一个事件多个对象处理
3.响应者对象:能处理事件的对象


Snip20151007_4.png

事件传递的完整过程

1.先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件
2.调用最合适控件的touches方法
3.如果调用了[super touches...];就会将事件顺着响应者链条往上传递,传递给上一个响应者
4.接着就会调用上一个响应者的touches...方法

如何判断上一个响应者

1.如果当前这个view是控制器的view,那么控制器就是上一个响应者
2.如果当前这个view不是控制器的view,那么父控件就是上一个响应者

响应者链的事件传递过程

1.如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图

2.在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

3.如果window对象也不处理,则其将事件或消息传递给UIApplication对象

4.如果UIApplication也不能处理该事件或消息,则将其丢弃

一个小应用来讲解事件的拦截

点击应用.gif
// 监听按钮的点击
- (IBAction)PopClick:(PopButton *)sender {
    
    UIButton *chatView = [UIButton buttonWithType:UIButtonTypeCustom];
    
    [chatView setImage:[UIImage imageNamed:@"对话框"] forState:UIControlStateNormal];
    
    [chatView setImage:[UIImage imageNamed:@"小孩"] forState:UIControlStateHighlighted];
    
    chatView.bounds = CGRectMake(0, -200, 200, 200);
    
    // 它的父控件为基准
    chatView.center = CGPointMake(100, -100);
    
    // 将对话框加入父控件中 ,超出的部分仍然可以显示, 只是不能点击
    [sender addSubview:chatView];    
    sender.chatView = chatView;
}

///自定义按钮,来实现变态拦截功能
// 外界提供_chatView用来接收新的对话框
// 重写hitTest方法,让能点击对话框。 只要拦截点击就行了
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
     // 将点得坐标系转换
    CGPoint chatP =[self convertPoint:point toView:_chatView];
    
    // 判断点是否在 控件上
    if ([_chatView pointInside:chatP withEvent:event]) {
        return _chatView;
    }
    // 若不在,执行以前的方式
    return [super hitTest:point withEvent:event];

}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint curOffset = [touch locationInView:self];
    CGPoint lastOffset = [touch previousLocationInView:self];
    
    CGFloat offsetX = curOffset.x -lastOffset.x;
    CGFloat offsetY = curOffset.y - lastOffset.y;
    
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);

}
iOS深入学习
Web note ad 1