美文网首页事件的产生、传递
iOS中的事件的产生和传递

iOS中的事件的产生和传递

作者: 蔚尼 | 来源:发表于2018-06-01 17:48 被阅读115次

    一.触摸事件由触屏生成后如何传递到当前应用?

    触屏后的传递

    系统响应

    1.手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。

    2.IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。

    mach port 进程端口,各进程之间通过它进行通信。
    SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。

    1. SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。

    此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。

    所以,触屏生成触摸事件后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。

    二.触摸事件的产生和传递:

    iOS中的事件可以分为3大类型:

    • 触摸事件
    • 加速计事件(例如手机摇一摇)
    • 远程控制事件(例如使用遥控给使用iOS系统的设备发出指令)

    今天记录的是触摸事件,即app接收到触摸事件后如何做的。

    1.事件的产生和传递

    • 发生触摸事件后,系统将触摸事件放到UIApplication队列中(队列:先进先出,栈:先进后出)(先来的事件先处理);
    • 按顺序取出事件,UIApplication的keyWindow作为主窗口,在keywindow上面找到合适的view进行处理;
    • 找到合适的view之后,就会调用控件的touches方法来做具体的事件处理:
      touchesBegan...
      touchesMoved...
      touchedEnded...
      这是事件传递的开始;

    也就是说:
    触摸事件的传递是从父控件传递到子控件;
    先把事件放到UIApplication的队列里面,然后UIApplication->window->寻找处理事件最合适的view
    注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

    如何找到合适的控件来处理事件?

    1. 询问自己是否能接收触摸事件?
    2. 触摸点是否在自己身上?(在自己身上pointInside方法会返回yes)
    3. 前面两条都满足之后:从后往前(就是先看后添加的)遍历子控件,重复前面两个步骤。找到合适的view来接收事件
    4. 找到合适的view后,再遍历这个合适view的子控件,直到没有合适的view。
    5. 如果遍历到最后没有找到合适的view,自己就是合适的view
    1. 询问自己是否能接收触摸事件?/不可以接收触摸事件的条件:

    透明度0.0~0.01之间;
    隐藏(hidden = yes);
    userInteractionEnabled = no;

    2. 触摸点是否在自己身上?
    • hitTest方法(返回合适的view)
      //当事件传递给当前view时,会调用hitTest方法
      //作用:寻找最适合的view
      //返回值:返回谁,谁就是最适合的view响应事件,就会调用谁的touchs方法(通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。)
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    
        UIView * fitView = [super hitTest:point withEvent:event];
        return fitView;
    }
    

    注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

    不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

    • pointInside方法(触摸点在当前view身上返回yes,否则返回no)
      //作用:判断点在不在自己身上,是调用当前view的一个方法
      //什么时候调用:在hitTest内部调用
      //返回值:YES--》在当前viwe身上;No--》不在当前view身上;
      //point:当前触摸点
      //注意:必须得要跟方法调用者在同一个坐标系
    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
        return [super pointInside:point withEvent:event];
    //    return YES/NO;
    }
    
    3. 从后往前(就是看后添加的先看)遍历子控件,重复前面两个步骤
    事件传递过程

    2.模拟实现事件传递底层

    #import "WSWindow.h"
    @implementation WSWindow
    // 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
    // 作用:寻找并返回最合适的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;
        }
        // 作用:判断下传入过来的点在不在方法调用者的坐标系上
        // point:是方法调用者坐标系上的点
        //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
        //{
        // return NO;
        //}
        - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
        NSLog(@"%s",__func__);
        }
        @end
    

    三.小测验

    1.HitTest方法运用:

    在controller里面,添加了一下几个view,顺序如下:
    白色WhiteView上面添加:1.1绿色 Greenview、1.2橙色 OrangeView
    橙色上面添加:2.1 蓝色 BlueView、2.2 红色 RedView
    蓝色上面添加:3 黄色
    黄色上面添加:4

    添加View

    如下:WhiteView的hitTest里面 return [super hitTest:point withEvent:event];
    绿色、橙色、蓝色view的hitTest里面 return self;

    问:点击空白处(WhiteView),哪个view响应事件

    @implementation WhiteView
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touche:%s",__func__);
    }
    
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        return [super hitTest:point withEvent:event];
    }
    @end
    
    @implementation GreenView
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touche:%s",__func__);
    }
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        return self;
    }
    
    @end
    
    @implementation OrangeView
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touche:%s",__func__);
    }
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        return self;
    }
    @end
    
    @implementation BlueView
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touche:%s",__func__);
    }
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        return self;
    }
    
    @end
    

    答案:橙色;打印: touche:-[OrangeView touchesBegan:withEvent:]
    原因:
    1.白色view可以接收事件(没有隐藏、不透明)
    2.点在白色view身上(pointInside 返回yes)
    3.从后往前遍历子控件看哪个view做适合的view(hitTest返回的view):
    顺序:橙色-->绿色
    遍历到橙色的时候,橙色里面的hitTest返回self,那橙色的view就是合适的view。所以橙色view的touche事件响应。

    2.pointInside方法运用:

    问:在1问的基础上,WhiteView里面添加以下方法,点击空白的地方,谁响应事件?

    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
        
        return NO;
    }
    

    答:window响应。因为Whiteview的pointInside返回no,whiteview不响应事件,就是他的父控件响应。

    四.hitTest方法实例运用

    1.运用1

    • 问题:如下图,在Main.storyboard界面上添加橙色按钮(OrangeBtn)、绿色view(GreenView)。在两个控件重叠的红色区域,橙色按钮事件无法响应,如何解决?
    效果图
    • 解决思路:
      每次事件传递的时候都会调用hitTest方法来获取合适的view作为响应对象;
      当触摸重合区域的时候,让橙色按钮成为合适的view。

    • 解决:
      1.在绿色view(GreenView)里面获取到橙色按钮
      但是在GreenView无法从storyboard里面引入OrangeBtn。我们反向思维,在GreenView先写好要引入的控件,再反向拖进来,如下图:

    获取橙色按钮

    2.处理GreenView里面的hitTest方法,重叠的时候让橙色按钮响应

    #import "GreenView.h"
    
    @implementation GreenView
    
    //传递事件的时候会掉这里
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        
        //如果触摸点在橙色按钮上面,就让橙色按钮成为合适的view
        CGPoint inOrangePoint = [self convertPoint:point toView:self.orangeBtn];
        if ([self.orangeBtn hitTest:inOrangePoint withEvent:event]) {
            return self.orangeBtn;
        }
        //否则,按原本的操作进行
        return [super hitTest:point withEvent:event];
    }
    @end
    

    2.运用2

    • 问题:在storyBoard上面添加红色按钮(RedBtn),点击红色按钮后添加黄色按钮(yellowBtn)在红色按钮上面。因为黄色按钮在红色按钮外面,所以黄色按钮的点击事件失效,现在需要黄色按钮可以响应事件。
    效果图
    viewcontroller里面:
    //红色按钮点击事件
    - (IBAction)redBtnClick:(id)sender {
        
       //点击后添加黄色按钮
        UIButton * yellowBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        yellowBtn.frame = CGRectMake(20, - 20, 100, 30);
        [yellowBtn setBackgroundColor:[UIColor yellowColor]];
        [_redBtn addSubview:yellowBtn];
        
        _redBtn.yellowBtn = yellowBtn;//把黄色按钮传给红色按钮
        
        [yellowBtn addTarget:self action:@selector(yellowBtnClick) forControlEvents:UIControlEventTouchUpInside];
        
    }
    //黄色按钮的点击事件
    -(void)yellowBtnClick{
        NSLog(@"newBtnclick");
    }
    
    • 解决:
      如果触摸点点在黄色按钮身上,就让黄色按钮成为合适的view;
      这样点击黄色view的时候,黄色view的事件就可以执行啦!!
    //事件传递的时候会掉这里
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        
        //当一个控件不在父控件里面的时候无法接收到事件
        
        //如果点击的按钮在_yellowBtn上,就让_yellowBtn成为合适的view,触发_yellowBtn的事件
        CGPoint getPoint = [self convertPoint:point toView:_yellowBtn];
        if ([_yellowBtn pointInside:getPoint withEvent:event]) {
            
            return _yellowBtn;
        }
        return [super hitTest:point withEvent:event];
    }
    
    

    所以,想让谁响应事件,就让谁成为合适的view就行。

    五.事件的响应

    前面有记录:找到合适的view之后,就会执行合适view的touchesBegan方法。
    而touchesBegan方法的默认做法是,将事件顺着响应链交给上一个响应者处理。
    那么响应链条是什么呢,上一个响应者又是谁呢?

    响应链条

    • 响应者链条:是由多个响应者链接起来的链条
    • 作用:能看清楚每个响应者之间的联系,并且让一个事件多个对象处理
    • 响应者对象:能处理事件的对象


      响应者链条示意图

    上一个响应者是谁?

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

    证明

    如下图,有一个ViewController;
    ViewController的view取名为WhiteView;
    WhiteView里面有一个OrangeView;

    效果图
    证明当前view不是控制器的view,那么父控件就是上一个响应者

    我们证明OrangeView为适合view的时候,父控件WhiteView就是上一个响应者。

    分析:

    1. OrangeView为适合view的时候会调用OrangeView的touchesBegan方法;
    2. 如果OrangeView的touchesBegan方法我们不重写/ [super touchesBegan:touches withEvent:event]; 方法,就会调用上一个响应者的方法。
    3. 我们不重写OrangeView的touchesBegan方法,看它是否调用WhiteView的touchesBegan方法。如果是,那父控件WhiteView的就是上一个响应者。

    操作:
    如下,WhiteView的touchesBegan调用会打印,OrangeView里面没有写任何代码。
    触摸OrangeView,打印:touche:-[WhiteView touchesBegan:withEvent:]

    结论:
    OrangeView为适合view的时候,父控件WhiteView就是上一个响应者。

    #import "WhiteView.h"
    
    @implementation WhiteView
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"touche:%s",__func__); 
    }
    @end
    
    #import "OrangeView.h"
    
    @implementation OrangeView
    
    @end
    
    如果当前view是控制器的view,那么控制器就是上一个响应者

    我们证明WhiteView为适合view的时候,控制器ViewController就是上一个响应者。

    分析:

    1. WhiteView是适合view的时候会调用WhiteView的touchesBegan方法;
    2. 如果WhiteView的touchesBegan方法我们不重写/或者[super touchesBegan:touches withEvent:event];,就会调用上一个响应者的方法。
    3. 我们不重写WhiteView的touchesBegan方法,看它是否调用ViewController的touchesBegan方法。如果是,控制器ViewController就是上一个响应者。

    操作:
    如下,WhiteView的touchesBegan调用 [super touchesBegan:touches withEvent:event];,ViewControlledr的touchesBegan调用会打印。

    触摸WhiteView,打印:-[ViewController touchesBegan:withEvent:]

    结论:
    WhiteView为适合view的时候,控制器ViewController就是上一个响应者。

    #import "WhiteView.h"
    @implementation WhiteView
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [super touchesBegan:touches withEvent:event];
    }
    @end
    
    @implementation ViewController
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        NSLog(@"%s",__func__);
    }
    @end
    

    六.事件完整的响应过程(事件如何传递、如何响应)

    事件完整的响应过程
    1. 先把事件放到Application队列里面,按照先进先出的方式取出事件;
    2. Application一般取出keyWindow作为主窗口,由上往向下传递(由父控件传递给子控件),找到合适的view;

    3.合适的view对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

    4.响应者对于接收到的事件有3种操作:

    • 不拦截,默认操作
      事件会自动沿着默认的响应链往下传递

    • 拦截,不再往下分发事件
      重写 touchesBegan:withEvent: 进行事件处理,不调用[super touchesBegan]

    • 拦截,继续往下分发事件
      重写 touchesBegan:withEvent: 进行事件处理,同时调用[super touchesBegan] 将事件往下传递

    关于如何判断上一个响应者:

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

    • UIViewController
      若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。

    • UIWindow
      nextResponder为UIApplication对象。

    • UIApplication
      若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。

    嗯。。。事件的传递和响应记录完了。属于很简单的一个知识点,这次简单的学习也发现了之前写法的一些漏洞,也感受到了学习的喜悦,祝愿自己能再接再力,多多学习吧!


    这部分的面试考点:

    1.事件如何传递和响应的?

    传递的时候:
    主要通过hitTest和pointInside两个方法判断;
    1.先把事件加入到Application队列中,然后以keywindow作为主窗口传递事件;
    2.判断view是否可以点击(比如是否隐藏、透明度是否是0-0.01之间,userInteractionEnabled是否为no)
    3.通过pointInside判断点是否在这个view上面;
    4.第2、3两条都满足之后,从后往前遍历view的子控件,调用子控件的hitTest方法(即2、3)两步寻找合适的view;
    5.如果子控件里面都没有找到合适的view,父控件就是合适的view;

    事件响应:
    合适的view默认把事件顺着响应链向下传递。它也可以通过touchesBegan方法拦截事件,自己处理。
    如果合适的view重写touchesBegan方法,不调用[super touchesBegan] ,就自己处理了。
    如果合适的view重写touchesBegan方法,调用[super touchesBegan] ,自己处理后也会把事件向下传递。

    2.事件响应,如果一直都没有响应对象会怎么样?

    会忽略掉,无任何反应。(就像我们在controller上门添加了一个view,对view什么都不处理,触摸的时候也不会崩溃)

    3.UIView和CALayer的关系(美团的面试题)

    • UIView提供内容,处理触摸等事件,参与响应链;
    • CALayer负责显示内容contents

    4.为什么UIView只提供事件处理,CALyer只负责内容显示?

    单一职责原则:UIView只提供事件处理;CALyer只负责内容显示

    5.如下,红色正方形按钮中,有一个圆。实现红色部分不可点击,圆形部分可以点击。

    示例图

    思路:判断触摸的点是否在圆内,如果圆内就可以点击,否则不可点击;

    判断一个点是否在圆内:判断这个点到圆心的距离>半径,那么点不在圆内;
    例如判断A点到圆心B的距离,就是求距离c的大小。
    直角三角形的斜边 = (直角边的平方 + 直角边的平方)的平方根

    分析
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
        CGFloat x1 = point.x;
        CGFloat y1 = point.y;
        
        CGFloat x2 = self.frame.size.width / 2;
        CGFloat y2 = self.frame.size.height / 2;
        
        //sqrt:求平方根
        //直角三角形的斜边 = (直角边的平方 + 直角边的平方)的平方根  ; C的平方 = a的平方 + b的平方
        double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    
        //这个点到圆心的距离小于半径,点就在圆内,可以点击
        if (dis <= self.frame.size.width / 2) {
            return YES;
        }
        else{
            return NO;
        }
    }
    

    相关文章

      网友评论

        本文标题:iOS中的事件的产生和传递

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