iOS 事件传递机制

作者: Mccc_ | 来源:发表于2020-01-03 16:36 被阅读0次

    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<UITouch>, with event: UIEvent?)
        open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
        open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
        open func touchesCancelled(_ touches: Set<UITouch>, 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<UITouch>, with event: UIEvent?)
    open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
    open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
    open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
    

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

    相关文章

      网友评论

        本文标题:iOS 事件传递机制

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