iOS事件的传递和响应者链

作者: 圣僧留步 | 来源:发表于2017-09-30 16:44 被阅读45次

    前言:

    在咱们平时的开发过程中,每天都在敲着带有各种响应事件的代码,那咱们iOS的响应事件是怎样传递的呢?系统是怎样找到发出该事件的控件并执行响应方法的?为此,我写下这篇文章算是自己对事件响应链的学习的理解和总结吧!希望对在这方面有疑惑的你有帮助。

    在iOS中有好几类,咱们今天只分析一下平时用的最多的触摸事件,学习触摸事件首先要了解一个比较重要的概念,响应者对象(UIResponder)

    UIApplicaiton、UIViewController、UIView这些都是继承自UIResponder的,那为啥继承自UIResponder的类就能接收和处理事件呢?那是因为UIResponder提供了一下方法来处理事件
    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;
    

    在了解哪些控件能接收和处理事件以及它们为什么能处理事件以后,咱们需要了解到事件的产生和传递的过程是怎样的

    一:事件的产生和传递

    1.1 事件的产生

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

    1.2事件的传递

    1.)触摸事件的传递是从父控件传递到子控件
    2.)也就是UIApplication->window->寻找处理事件最合适的view
    注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

    App是如何找到最合适的控件来处理事件呢?

    1.首先咱们的App在刚进入应用的时候会首先创建UIApplication对象,它是一个单例,在任何页面都能拿到它,当咱们点击一个视图UIView的时候,产生一个触摸事件A,这个触摸事件A会被添加到UIApplication的队列中,然后UIApplication会把事件A传递给应用程序的主窗口(keyWindow)
    2.判断触摸点是否在自己身上
    3.如果触摸点在自己身上,那就遍历自己的子控件数组,记得是从后往前遍历,因为最后加载上去的视图放在最上面,从后往前遍历会使遍历次数更少,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
    4。如果子控件没有任何一个符合条件的,那么系统就会认为它自己是最合适处理这个事件的控件,这是最适合的View。

    为了演示具体的传递过程是怎么样的,这里盗用网上的一张示意图来展示:

    上面演示的意思就是假如现在有一个fitView,点击fitView在调用系统方法

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

    发现触摸点在fitView身上,那么系统就会将fitView的子控件倒序遍历一遍,如果找到能够符合该触摸事件的控件,就交给该控件执行响应的触摸方法,如果没有子控件符合要求,那就默认fitView为符合该条件的View

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

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

    二:寻找最适合的View的详细分析

    在详细讲解之前,需要了解两个重要的方法:
    hitTest:withEvent:方法
    pointInside方法

    2.1 hitTest:withEvent:方法
    什么时候调用?

    只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

    方法作用:

    寻找并返回最合适的view(能够响应事件的那个最合适的view)
    注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

    拦截事件的处理

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

    事件传递给窗口或控件的后,就调用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:方法会在底层调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

    三:响应者链条的工作原理

    当用户点击屏幕产生一个触摸事件以后,经过一系列的事件传递,会找到最合适的视图控件来处理这个事件,找到最合适的视图控件以后,就会调用控件的touches方法来做具体的事件处理,而这些touches方法的默认做法是将事件顺着响应者链条向上传递,在代码上的最直接的展示如下:

    //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
    // 上一个响应者可能是父控件
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
    [super touchesBegan:touches withEvent:event]; 
    // 注意不是调用父控件的touches方法,而是调用父类的touches方法
    // super是父类 superview是父控件 
    }
    

    在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法,所以我们可以利用这个原理,做到一个事件多个对象处理,我们只需要在重写touches方法的时候,除了写自己需要处理的代码,同时调用[super touchesBegan:touches withEvent:event],将事件响应再交给父类处理。

    经过上面的分析,咱们可以轻易的得出结论:事件的传递方式是从上往下(父控件到子控件),事件的响应方式是从下往上(顺着响应者链条,向上传递,从子控件到父控件)。

    相关文章

      网友评论

        本文标题:iOS事件的传递和响应者链

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