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

前言(铺垫一下)

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

所有事件响应的类都是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

事件的 传递链 响应链 (ios)_第1张图片
事件的(传递,响应).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,所以它们的实例都是可以构成响应者链的响应者对象。


事件的 传递链 响应链 (ios)_第2张图片
响应链的基本构成.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)

事件的 传递链 响应链 (ios)_第3张图片
事件传递的流程图.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))