当我们触摸手机屏幕到事件触发可以分为两步: 事件传递和事件响应
一、传递: 当我们触摸屏幕时,需要找到一个最合适的view,查找方式是从内向外,UIApplication -> UIWindow -> view
查找过程:
查找过程中需要判断透明度是否小于0.01,交互userInteractionEnabled是否关闭,动画交互是否关闭,如果有一项不成立,则该view不是最合适的view,也就是事件不会由该view和该view的子视图接收,都成立后继续判断- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event;方法,该方法的返回值就是最合适的view,内部实现是先判断点击的区域是否在该view上,再遍历view的子视图,子视图的子视图的hitTest方法,可以看出这是一个递归,而且是倒序reverseObjectEnumerator遍历子视图,说白了就是从最外层的子view开始遍历,也就是最后addSubView上的view最先被遍历到(可以自己去验证倒序问题),这样效率会更高,因为最外层响应事件的view可能性最大
二、响应: 当我们找出最适合的view后,还需要找出可以响应此事件的view,查找顺序就是从这个最合适的view开始一直到UIApplication,从外到内查找,view -> UIWindow -> UIApplication
查找过程:
先查看最合适的view是否有绑定手势或者重写touch的四个方法,如果满足其中一个则处理响应事件,整个事件响应链结束,如果都没有则继续向下查看nextResponder也就是我们通常说的父view(也不一定是父view,如果控制器A跳转到控制器B,那么控制器B的nextResponder就是控制器A,还有就是控制器A作为根视图,那控制器A的nextResponder就是UIWindow了)是否有绑定手势或者重写touch方法,一直到UIApplication,直到事件被抛弃
验证响应顺序可以利用runtime Method Swizzling交换UIView的touchesBegan方法来验证
废话不多说,直接看下面的场景!
有三个view,v1(红色)、v2(蓝色)、v3(黑色)
场景一:v1在最下面,父视图是控制器的view,v2在v1的上面(挡住了v1半边),父视图也是控制器的view,v3是v2的子视图,而且挡住了v1一部分,当分别点击v1、v2、v3的各个部位,接收事件的view是哪一个,或者多个?
验证得出,不管点击哪个view,都只有最外层与手指直接接触的view产生响应,就算点击多个view层叠的部位也只有最上层的view响应
场景二:将v2的userInteractionEnabled属性置为NO,不接收事件
可以发现,在点击v2或者v3的时候,v2、v3都不会接收响应,v2不接收响应是由于交互被关闭了,但v3为什么不能接收响应呢?原因是由于在第一步传递过程查找最合适的view时,查找方式是从内到外,也就是说作为v3的父视图v2是优先被找到,当发现v2的交互关闭时不会遍历v2的子视图了,所以导致v3也不会响应,但是如果点击v2或者v3区域的下方是v1,那么v1就会接收到事件响应
场景三:想做到在点击子视图v3的时候,v2也要同时接收响应事件
可以看出,只需要在子视图v3中重写touchesBegan方法,并加上[self.nextResponder touchesBegan:touches withEvent:event]这一句代码将事件传递给下一个响应者就可以实现上面的需求,这里顺便提一下touchesBegan这个方法,该方法是UIResponder中的方法,UIView又是继承自UIResponder的;看下面的touchesBegan方法的源码可以看出touchesBegan里面的实现就是将事件传递给下一个响应者,如果重写了touchesBegan方法,而且没有调用[self.nextResponder touchesBegan:touches withEvent:event]; 那么响应事件就不会继续传下去,所以要想事件继续传下去必须加上[self.nextResponder touchesBegan:touches withEvent:event];这一句代码,需要注意的是这里不能写成[self.superview touchesBegan:touches withEvent:event]; 由于上面已经提到过,当前view的下一个响应者有可能不是父子关系!
场景四:不管点击哪里(包括空白区域),v3都需要接收响应事件
可以看出,只需要重写v1中的hitTest方法并返回v3就可以达到上面的需求,至于原因上面也提到过,查找合适的view时遍历到v1就直接找到最合适的view,也就是v3,查找过程就结束了,重写hitTest方法剪切到v2中,也是实现相同的效果,因为v1和v2是平级的,都是self.view的直系子视图,都会被遍历到,但是如果v2的父视图时v1,v3的父视图时v2,这样就又不一样了
可以看出hitTest方法写在v2里面,点击空白处或者v1以外的区域(包括点击v2或者v3的区域不在v1内)是不会有任何反应的,因为在查找的时候遍历到v1时,会判断触摸的区域是否在v1上面,如果不在则不会遍历v1的子视图v2和v3,只有在v1的区域才会触发v2中的hitTest方法,进而达到由v3来接收事件的效果