事件传递与视图响应链

一、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、如果最终没有找到响应者,就会忽略到这个事件,也就是不会产生实质性的动作,不会引起崩溃。

你可能感兴趣的:(事件传递与视图响应链)