事件传递

作者: Little_Dragon | 来源:发表于2015-10-07 22:15 被阅读1207次

    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);
    
    }
    

    相关文章

      网友评论

      本文标题:事件传递

      本文链接:https://www.haomeiwen.com/subject/brcncttx.html