GitHub Demo:https://github.com/shaozhe-chen/ResponderTest
首先我先提出三个问题:
1、点击屏幕如何找到最合适的view来响应事件?
2、pointInside:withEvent:先调用?还是hitTest:withEvent:?还是touchBegin:withEvent:?
3、如何确定响应链?
大家平时在做UI开发的时候,我估计大家都很少关注这些问题的,我也是在一次面试中发现了自己的不足,所以赶紧研究一番,查漏补缺。
在正式介绍之前,先理解一下pointInside:withEvent和hitTest:withEvent:
pointInside:withEvent:判断触摸点是否落在视图内,则return YES,否则为NO。
hitTest:withEvent:是返回一个view,则表示这个view是最合适响应事件的,如果返回nil,则表示这个视图不是最适合响应的视图。
一、点击屏幕如何找到最合适的view响应事件?
以下我不考虑不接收用户交互的三种情况:userInteractionEnabled = NO,当一个控件隐藏时 Hidden = YES, 当一个控件为透明时alpha <= 0.01时。
首先先来看看这张图,布局如上图所示:控制器根视图View上有一个蓝色的子视图A,子视图A上有两个子视图C和B,D区域表示C跟B重合的部分,且B视图在上面,C视图在下面。
当点击B视图时,先执行pointInside:withEvent:方法,执行顺序是:UIWindow -> 控制器根视图 -> 蓝色视图A -> 红色视图B(如果点击的是C视图:执行顺序是:UIWindow -> 控制器根视图 -> 蓝色视图A -> 红色视图B ->红色视图C) 。大家有没有注意到,为什么没有调用C的pointInside:withEvent:?原因是当遍历子视图的时候,有多个子视图,会优先遍历后加入的视图,而点落在B视图上,那么pointInside:withEvent:的调用就结束了,不会继续调用C的pointInside:withEvent:方法了(这就是为什么点击D区域,点都是落在C和B的bounds的范围内,但是响应的却是B视图)。
当找到B视图之后,就会调用B的 hitTest:withEvent:,并返回B视图。由于视图的遍历采用了递归的形式,所以 hitTest:withEvent:的调用顺序是:B视图 -> A视图 -> 控制器根视图View ->UIWindow。并且 hitTest:withEvent:都是返回B视图。
到此是否就能确定B视图是最适合是响应视图呢?答案是否!
原因是,如果视图是延伸到状态栏底下的呢?我实际上点的是状态栏,所以状态栏才是最适合的响应视图,所以系统必须确认,这个点是否在状态栏上。遍历过程如果所示:
当确定了点不在状态栏之后,系统还会继续从从UIWindow到B视图开始遍历一次,只是此次遍历不会再遍历状态栏。总的遍历过程可以压缩为:UIWindow到B视图 -> UIStatusBarWindow到到UIStatusBarItem -> UIWindow到B视图
当然了,如果点确实是落在了状态栏上,遍历会不一样:UIWindow到B视图 -> UIStatusBarWindow到UIStatusBarItem -> UIWindow到B视图 -> UIStatusBarWindow到UIStatusBarItem -> UIWindow到B视图
到这里,就已经完完全全确定了最合适的响应视图了。接下来可以回答是
二、pointInside:withEvent:先调用?还是hitTest:withEvent:?还是touchBegin:withEvent:?
根据上面的分析,我们知道是通过先调用pointInside:withEvent:确定点是否落在该视图上,如果是,才在该视图上遍历该视图的子视图,直到成功遍历到最合适的视图之后,才会调用hitTest:withEvent:返回该视图作为最适合响应的视图。至于touchBegin:withEvent:,当然是在确定完适合响应的视图之后,才会调用的。
三、如何确定响应链
当我们成功确定过了最适合响应的视图B之后,响应链的确定就容易得多了,不需要再像寻找视图递归遍历那么麻烦了。而是直接就根据视图递归遍历的结果(打印nextResponder):视图B -> 视图A - > 控制器根视图View --> UIWindow --> UIApplication --> AppDelegate。当然了,UIApplication , AppDelegate并没有在寻找最适合的响应视图的遍历流程中,原因是底层的事件,首先是传给AppDelegate,AppDelegate把事件交给UIApplication,通过UIApplication 的window把事件交给window。