美文网首页将来跳槽用收藏ios
iOS之事件的传递和响应机制

iOS之事件的传递和响应机制

作者: 雪_晟 | 来源:发表于2019-04-19 23:45 被阅读4次

    渐渐发现有些东西还是得形成文字才能记得更准确吧。

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

    触摸事件
    加速计事件
    远程控制事件
    这里我们只讨论iOS中的触摸事件。
    

    在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder

    UIApplication
    UIViewController
    UIView
    

    那么为什么继承自UIResponder的类就能够接收并处理事件呢?

    因为UIResponder中提供了以下4个对象方法来处理触摸事件。
    
    UIResponder内部提供了以下方法来处理事件触摸事件
    - (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;
    加速计事件
    - (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;
    

    因为苹果不开源,没有把UIView.m文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件(注意:我说的是UIView触摸事件而不是说的 UIViewController的触摸事件)。
    有人认为,我要是处理控制器的自带的view的事件就不需要自定义UIView子类继承于UIView,因为可以在viewController.m 文件中重写touchBegan:withEvent:方法,但是,我们此处讨论的是处理UIView的触摸事件,而不是处理 UIViewController的触摸事件。你如果是在viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,因为viewController也是继承自UIResponder,所以会给人一种错觉。
    所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView

    事件的传递与响应:

    1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

    2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital viewsuperView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

    3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

    事件的传递和响应的区别:

    事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

    找到最合适的响应View

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

    不允许交互:userInteractionEnabled = NO
    隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
    透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
    

    1.0事件产生

    • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?因为队列的特定是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
    • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
    • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
      找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

    2.0事件传递

    • 触摸事件的传递是从父控件传递到子控件
    • 也就是UIApplication->window->寻找处理事件最合适的view

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

    • 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
    • 2.判断触摸点是否在自己身上
    • 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
    • 4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
    • 5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view

    找到最底层的View到底是怎么实现的

    先看两个api:
    第一个:-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

    只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法,不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法。

    拦截事件的处理

    • 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view
    • 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
    • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

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

    所以事件的传递顺序是这样的
      产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

    事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view
    不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
    技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view

    原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题,因为会存在这么一种情况,当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view

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

    第二个:-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    作用:判断下传入过来的点在不在方法调用者的坐标系上
    point:是方法调用者坐标系上的点。

    第一个api用来做拦截处理,第二api可以扩大按钮的点击范围,示例扩大按钮的点击范围

    相关文章

      网友评论

        本文标题:iOS之事件的传递和响应机制

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