事件传递与视图响应链
一、UIView与CALayer区别
![](https://img.haomeiwen.com/i7911324/9f1a85ba26f7630c.jpg)
备注:这里的backing store指的是位图。位图最终是给计算机硬件操作的。
- CALayer为UIView提供显示的内容,只负责内容显示,不参与事件处理。
- UIView作为CALayer的代理,提供交互操作;负责处理触摸事件,参与响应链。
二、为什么UIView只负责事件传递、CALayer负责视图显示
这个问题等同于为什么iOS中提供UIView和CALayer两个平行的层级结构。
答:主要是为了做到单一职责原则,做到职责分离,避免过多重复代码。
三、为什么CALayer不能响应触摸事件
从继承关系图来回答,响应事件必须继承自UIResponder。
![](https://img.haomeiwen.com/i7911324/27889ec4808151a7.jpg)
四、事件传递
4.1、相关事件方法
//返回当前响应事件的视图View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
//判断当前点击的位置point是否在视图范围内
//在hitTest: withEvent:内部使用,用来判断点击了哪个View
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
4.2、简述事件传递流程
![](https://img.haomeiwen.com/i7911324/7ffff558596275ae.jpg)
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:内部实现
![](https://img.haomeiwen.com/i7911324/2368717980acca94.jpg)
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、响应事件流程
![](https://img.haomeiwen.com/i7911324/4fb6508733615661.jpg)
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、如果最终没有找到响应者,就会忽略到这个事件,也就是不会产生实质性的动作,不会引起崩溃。