iOS 事件传递机制

iOS的事件分为以下几种

  • Touch Events 触摸事件
  • Shake-motion events 运动事件,比如重力感应
  • Remote-control events 远程控制事件,穿戴设备控制手机
  • Press events 按压事件
  • Editing menu messages 编辑菜单信息事件? 应该叫啥?

苹果官方的事件文章

下面主要讲解触摸事件: 分为两个过程:

  • 传递: 触摸屏幕的时候,寻找合适的View;
  • 响应: 找到最合适的View之后,继续寻找能响应此事件的View;

事件过程1:事件的传递

1. 什么是响应者对象?

答:有响应和处理事件能力的对象。

所有的响应者都是继承于 UIResponder,也就是说UIResponder是所有响应事件类的基类。UIView, UIViewController, UIApplication都是响应事件的载体。

open class UIResponder : NSObject, UIResponderStandardEditActions {

    open var next: UIResponder? { get }

    open func touchesBegan(_ touches: Set, with event: UIEvent?)
    open func touchesMoved(_ touches: Set, with event: UIEvent?)
    open func touchesEnded(_ touches: Set, with event: UIEvent?)
    open func touchesCancelled(_ touches: Set, with event: UIEvent?)
}

2. 什么是响应链?

由一系列的响应者对象构成的一个层级结构。

3. 什么是next( oc中叫nextResponder)

答:用于获取响应链中当前对象的下一个响应者。

  • UIView
    若view是控制器的根视图,则其nextResponder是控制器对象;否则nextResponderController(父视图)。
  • UIViewController
    若控制器的视图是window的根视图,则nextResponder为窗口对象,否则nextResponder为该控制器的view的父视图。
  • UIWindow
    nextResponder为UIApplication对象。
  • UIApplication
    若当前的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate

3. 响应链如何构建的?

  • 当一个 viewaddsuperView上,它的 nextResponder 就被指向了它的 superView ;
  • controller 被初始化的时候,self.viewnextResponder 就被指向了所在的 controllercontrollernextResponder会指向self.viewsuperView,这样整个app就被串成了一条响应链条;

4. 寻找响应者(Hit-Test View)

通过Hit-Test可以找到手指点击点处于屏幕最前面的那个UIView。

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   

只要事件一传递给一个控件,这个控件就会调用自己的该方法寻找并返回能响应事件的合适view。

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  

执行了hitTest方法,系统底层会调用该方法,判断触摸点是否在当前的view上,如果不在当前view上,就不能处理事件。

iOS 系统检测到手指触摸操作时,通过递归方法向界面的根节点UIWindow发送hitTest:withEvent:消息。

当前的window调用pointInside:withEvent:,判断触摸点是否在范围内,如果在,则 倒叙遍历 window的subviews,然后依次对subview发送hitTest:withEvent:消息。如果当前subview调用pointInside:withEvent:判断触摸点是否在自己范围内,如果不在,这个view的subviews也就不会被遍历了。

直到遍历到触摸点在view里面,并且view没有subview,那么它就是我们要找的hit-test view了,找到之后就会一路返回直到根节点,而该view之后的view就不会被遍历了。

  • 这是一个由底到上的过程:由window开始遍历,直到找到合适的view。
  • 倒叙遍历子视图: 因为视图的层级结构会出现挡住的情况。
  • hitTest:withEvent:返回了此对象,不再遍历其他view,处理结束。
UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面),其中view A和view B有一部分重叠

当事件遍历到了view B.1,发现point在view B.1里面,并且view B.1没有subview,那么他就是我们要找的hittest view了,找到之后就会一路返回直到根节点,而view B之后的view A也不会被遍历了。

  • 图文引用内容来自这里

注意hitTest里面是有判断当前的view是否支持点击事件,比如userInteractionEnabled、hidden、alpha等属性,都会影响一个view是否可以相应事件,如果不响应则直接返回nil。 还有一个pointInside:withEvent:方法,这个方法跟hittest:withEvent:一样都是UIView的一个方法,通过他判断point是否在view的frame范围内。如果这些条件都满足了,那么遍历就可以继续往下走了

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

注意: 此时将事件交给找到的Hit-Test View,但是并不一定是Hit-Test View 来响应事件。

## 事件过程2:事件的响应

当我们知道最合适的 View 后,事件会 由上向下【子view -> 父view,控制器view -> 控制器】来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了如下的方法:

open func touchesBegan(_ touches: Set, with event: UIEvent?)
open func touchesMoved(_ touches: Set, with event: UIEvent?)
open func touchesEnded(_ touches: Set, with event: UIEvent?)
open func touchesCancelled(_ touches: Set, with event: UIEvent?)

如果有实现那么就由此 View 响应,如果没有实现,那么就会传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器】。一旦找到最合适响应的View就结束, 在执行响应的绑定的事件,如果没有就抛弃此事件。

你可能感兴趣的:(iOS 事件传递机制)