美文网首页
事件的 传递链 响应链 (ios)

事件的 传递链 响应链 (ios)

作者: 红色海_ | 来源:发表于2020-08-09 08:40 被阅读0次

    前言(铺垫一下)

    当发生事件时必须知道由谁来响应事件。

    所有事件响应的类都是UIResponder的子类,

    响应链是一个由不同对象组成的层次结构,
    其中的每个对象将依次获得响应事件消息的机会。

    当发生事件时 事件首先被发送给第一响应者,
    第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。

    事件将沿着响应者链一直向下传递,直到被接受并做出处理。

    一般来说第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理事件就会被传递给它的视图控制器对象viewcontroller(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推直到顶层视图。接下来会沿着顶层视图(top view)到窗口(UIWindow对象)再到程序(UIApplication对象)。

    如果整个过程都没有响应这个事件,该事件就被丢弃。
    在响应者链中只要由对象处理事件,事件就停止传递。

    典型的 事件响应链 路线图:
    由离用户最近的view向系统传递。
    First Responser -- >View -- >SuperView...-- >The Window -- >The Application -- > App Delegate

    响应者链流程经常被委托(delegation)打断,

    一个对象(通常是视图)可能将响应工作委托给另一个对象来完成(通常是视图控制器ViewController),所以 做事件响应时在ViewController中必须实现相应协议来实现事件委托。

    iOS中的UIResponder类,定义了响应者对象的所有方法。UIApplication、UIView等类都继承了UIResponder类,UIWindow和UIKit中的控件因为继承了UIView,所以也间接继承了UIResponder类,这些类的实例都可以当作响应者。




    下图网络出处:https://www.cnblogs.com/Julday/archive/2019/12/30/12119536.html

    事件的(传递,响应).png

    iOS事件的类型

    iOS用户操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。

    对应的事件类型如下:
    1、触屏事件 (Touch Event)
    2、运动事件 (Motion Event)
    3、远端控制事件(Remote-Control Event)

    惯例我们就以触屏事件Touch Event为例说明在Cocoa Touch框架中事件的处理流程。

    响应链(Responder Chain)

    响应者对象(Responder Object)指的是有响应和处理事件能力的对象。
    响应链 就是由一系列的 响应者对象 构成的一个层次结构。

    UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。


    响应链的基本构成.png

    上图可见 响应者链的一些特点:
    1、响应者链通常是由视图(UIView)构成的;
    2、视图的下一个响应者是它de视图控制器UIViewController(当然如果有它的话),然后再转给它的父视图Super View;
    3、视图控制器 的下一个响应者shi其管理的视图的父视图;
    4、单例的窗口UIWindow的内容视图将指向窗口本身作为它的下一个响应者;
    5、单例的应用UIApplication是一个响应者链的终点,它的下一个响应者指向nil,结束整个循环。

    Cocoa应用可以有多个UIWindow对象。
    Cocoa Touch应用只有一个UIWindow对象,整个响应者链比较简单。

    事件分发(Event Delivery)

    第一响应者First responder指的是当前接受触摸的响应者对象(通常是一个UIView对象),即当前该对象正在与用户交互,它是响应者链的开端。

    事件传递的最终目的:找出一个能处理并响应事件的对象(第一响应者First responder)

    事件传递的流程图.png

    如何寻找第一响应者(事件传递的过程)

    事件传递链:
    由系统向离用户最近的view传递。
    UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

    1、当iOS程序发生触摸事件后,系统会利用Runloop将事件加入到UIApplication的任务队列中,具体过程可以参考深入理解RunLoop
    2、UIApplication分发触摸事件到UIWindow,然后UIWindow依次向下分发给UIView
    3、UIView调用hitTest:withEvent:方法看看自己能否处理事件,以及触摸点是否在自己上面。
    4、如果满足条件,就遍历UIView上的子控件。重复上面的动作。
    5、直到找到最顶层的一个满足条件(既能处理触摸事件,触摸点又在上面)的子控件,此子控件就是我们需要找到的第一响应者。

    hitTest:withEvent:的处理流程

    (上面的查找其实就是由该方法递归调用实现的)
    1、首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
    2、若返回NO,则hitTest:withEvent:返回nil;
    3、若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
    4、若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
    5、如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

    hitTest:withEvent:方法的伪代码大致如下:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        if (!self.userInteractionEnabled || !self.hidden || self.alpha <= 0.01) {
            return nil;
        }
        
        if ([self pointInside:point withEvent:event]) {
            
            for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
                CGPoint subPoint = [subView convertPoint:point fromView:self];
                
                UIView *bestView = [subView hitTest:subPoint withEvent:event];
                if (bestView) {
                    return bestView;
                }
            }
            return self;
        }
    
        return nil;
    }
    

    事件的响应流程

    通过上面的 hitTest:withEvent: 寻找到第一响应者后,需要逆着寻找第一响应者的方向(从第一响应者->UIApplication)来响应事件。

    流程如下

    1.首先通过 hitTest:withEvent: 确定第一响应者,以及相应的响应链
    2.判断第一响应者能否响应事件,如果第一响应者能进行响应则事件在响应链中的传递终止。如果第一响应者不能响应则将事件传递给 nextResponder也就是通常的superview进行事件响应
    3.如果事件继续上报至UIWindow并且无法响应,它将会把事件继续上报给UIApplication
    4.如果事件继续上报至UIApplication并且也无法响应,它将会将事件上报给其Delegate
    5.如果最终事件依旧未被响应则会被系统抛弃

    哪些视图不响应呢?

    hidden = YES 视图被隐藏
    userInteractionEnabled = NO 不接受响应事件
    alpha <= 0.01,透明视图不接收响应事件
    子视图超出父视图范围
    需响应视图被其他视图盖住
    是否重写了其父视图以及自身的hitTest方法
    是否重写了其父视图以及自身的pointInside方法

    应用场景

    方形按钮点击四角无效,点击中间的圆形区域有效。
    核心思路:在pointInside: withEvent:方法中修改对应的区域。

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        // 如果控件不允许与用用户交互,那么返回nil
        if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
            return nil;
        }
    
        //判断当前视图是否在点击范围内
        if ([self pointInside:point withEvent:event]) {
            //遍历当前对象的子视图(倒序)
            __block UIView *hit = nil;
            [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                //坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
                CGPoint convertPoint = [self convertPoint:point toView:obj];
                //调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
                hit = [obj hitTest:convertPoint withEvent:event];
                //如果找到了就停止遍历
                if (hit) *stop = YES;
            }];
    
            //返回当前的视图对象
            return hit?hit:self;
        }else {
            return nil;
        }
    }
    
    // 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
    - (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;
        
        //判断是否在圆形区域内
        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/rvfldktx.html