美文网首页iOS面试剖析
事件传递与视图响应链

事件传递与视图响应链

作者: huoshe2019 | 来源:发表于2019-10-08 11:00 被阅读0次

    一、UIView与CALayer区别

    图1 UIView与CALayer关系图

    备注:这里的backing store指的是位图。位图最终是给计算机硬件操作的。

    • CALayer为UIView提供显示的内容,只负责内容显示,不参与事件处理。
    • UIView作为CALayer的代理,提供交互操作;负责处理触摸事件,参与响应链。

    二、为什么UIView只负责事件传递、CALayer负责视图显示

    这个问题等同于为什么iOS中提供UIView和CALayer两个平行的层级结构

    答:主要是为了做到单一职责原则,做到职责分离,避免过多重复代码。

    三、为什么CALayer不能响应触摸事件

    从继承关系图来回答,响应事件必须继承自UIResponder。


    图2 UIView和CALayer继承关系

    四、事件传递

    4.1、相关事件方法

    //返回当前响应事件的视图View
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    
    //判断当前点击的位置point是否在视图范围内
    //在hitTest: withEvent:内部使用,用来判断点击了哪个View
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
    

    4.2、简述事件传递流程

    图3 事件传递流程

    1、点击屏幕某一位置,这个事件会传递给UIApplication。
    2、UIApplication传递给当前的UIWindow。
    3、在UIWindow里面就会使用hitTest:withEvent:方法,返回最终响应的视图。
    4、在hitTest:withEvent:方法里,会调用pointInside:withEvent:方法,来判断当前点击的位置是否在UIWindow内。
    5、如果当前的点在UIWindow内,则遍历UIWindow子视图,查找最终响应事件的视图;需要注意的是,这里的遍历是倒序遍历,即后添加的最优先被点击。
    6、在Subviews中的子视图中,采用倒序遍历views。在每个view中都会调用hitTest:withEvent:,在view的子视图中同样会调用hitTest:withEvent:,也就是一直递归调用。
    如果当前view的hitTest:withEvent返回的不为nil,则这个视图就作为事件响应的视图,结束了事件传递的流程;否则,继续遍历其它view。
    如果整个Subviews都没有找到,则当前UIWindow就作为事件响应的视图

    备注:这里的事件响应视图,不如叫做命中视图。因为点击了这个视图,但是这个视图不一定能为当前事件绑定了一个触发函数,也就是不能响应了。
    这个时候,就会沿着响应链向上寻找,看看父节点是否能够响应,这就是下面的响应链

    4.3、hitTest:withEvent:内部实现

    图4 hitTest内部实现

    1、判断hidden=YES、userInteractionEnabled=YES、alpha<0.01。
    如果不满足上述条件,则会返回nil,父类继续遍历其它子视图。
    2、使用pointInside:withEvent:,判断点击的point是否在视图范围内。
    如果不满足上述条件,则会返回nil,父类继续遍历其它子视图。
    3、上述条件满足,则会采用倒序方法遍历当前子视图
    4、遍历过程中,子视图调用hitTest:withEvent:方法,如果返回不为nil,则将当前子视图作为事件响应视图,返回给调用方。否则,继续遍历其它子视图
    5、如果没有找到子视图,由于点击位置在当前视图范围内,则会把当前视图作为事件响应视图返回给调用方。

    4.4、扩大按钮的点击区域

    核心点重写pointInside:withEvent:方法、在里面写明point在什么区域返回YES,什么区域返回NO即可。

    - (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 NO;
        }
        else{
            return YES;
        }
    }
    

    备注:上面例题产生的效果,只有点击区域大于某个圆形区域,才会有效,否则无效。

    上面讲的是事件的传递流程,这里讲的是事件的响应流程

    五、响应链

    5.1、响应事件流程

    图5 响应流程

    1、点击UILabel、UITextField、UIButton后,它们的下一个响应者是UIView(容器)。
    2、容器View继续传递给UIView(可能是UIViewController的View)。如果有UIViewController,则下一个响应者是UIViewController。
    3、如果上面都没有响应者,则会传递儿UIWindow。
    4、UIWindow传递给UIApplication。
    5、UIApplication传递给UIApplicationDelegate。

    简单总结如下:First Responser -> The Window ->The
    Applicationn->AppDelegate

    备注:事件传递给UIApplication,代表这个事件没有实际的响应动作,响应循环也就结束了。

    5.2、视图响应事件

    //一根或者多根手指开始触摸view(手指按下)
    -(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
    
    //一根或者多根手指在view上移动(随着手指的移动,会持续调用该方法)
    -(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
    
    //一根或者多根手指离开view(手指抬起)
    -(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
    
    //某个系统事件(例如电话呼入)打断触摸过程
    -(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
    

    总结

    如果问相关响应链的问题,可以从下面两个方面回答:
    1、hitTest寻找命中视图。
    2、从命中视图开始,沿着响应链向上寻找真正的响应者。
    3、如果最终没有找到响应者,就会忽略到这个事件,也就是不会产生实质性的动作,不会引起崩溃。

    相关文章

      网友评论

        本文标题:事件传递与视图响应链

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