iOS 事件响应链

响应链是如何形成的?


当我们触碰到屏幕的时候,整个iOS系统发生了什么呢?

这里有个思路需要转变一下,本质上,我们开发的app,里面所有的视图都是虚拟的,只是一堆代码,看起来,你的app有许多View的堆叠,而且是有层次的,你看起来触碰到了最上面的一个View,事实上屏幕只有一块啊,你触碰到的是冷冰冰的屏幕,因此第一个感知触摸事件的是操作系统,是他最先检测到屏幕上的压力,而不是你看上去触摸到的那个视图哟。

注意,这并不意味着系统会立刻处理此事件

虽然看起来app里面的控件和View是有层次的,但事实上屏幕只有一块,而且没有层次

我们如何才能构架出有层次的View呢?

苹果的办法是:画布模式,大家都见过画画,你如果在同一个地方画2笔,第二笔会覆盖第一笔,同样的,在屏幕上的同一地方有两个View,第二View会覆盖第一个View,因此在视觉上有了层次

也因此,当你触摸到屏幕上某个点的时候,其实系统并不能立刻确定你触摸的哪块View,因为多块View会有叠加、覆盖

系统会如何做呢?正如上文提到的,第一个响应的是底层系统


如上图,当我们点击View E的位置时,系统先响应,然后它会调用方法:

hitTest:withEvent:

该方法接受位置参数CGPoint,并从底层开始按照subview的顺序,测试该CGPoint在哪个View上,如果在该View上,则继续测试是否在View的subview上,对照上图,顺序如下:

1.触摸的CGPoint在View A上吗?在的,继续测试A的子视图View B、View C

2.在View B上吗?不。在View B上吗?在,继续B的子视图View D、View E

3.在View D上吗?不,且D无subview,结束此分支

4.在View E上吗?在,E无subview,结束此分支

5.结果形成了一个链,View A -->View C -->View E

还记得吗,系统第一时间检测到了触摸,但是并不立刻处理,而是通过上述的测试,得到了一个响应链,该链的最后一个,就是逻辑上最上层的视图,也就是这次触摸的First Responder

需要注意的是,所有的响应链都是父子视图的关系哟,如果View A、View C、 VIew E只是视觉上遮盖了,但是却不是superview、subview的关系,则事件是不会在两者之间传递的

从底层往上的好处就是,一层层测试的过程中,响应链已经形成,当View E无法处理此事件怎么办?按照响应链往上回溯即可,一直回溯到application,也无人处理此事件,则将事件丢弃,如下图

如果First Responder不响应,则往上传给下一个View,直到传给UIApplication,依然无人响应则丢弃事件


如何处理事件?


我们获取了触摸事件,那么如何响应这个触摸呢?

其中一个是,手势识别(Gesture Recognizer):iOS提供了UITapGestureRecognizer来识别手势,它必须挂在某个View上,当某个View上发生了触摸事件,UITapGestureRecognizer就会识别该触摸,如果符合我们定义的手势,则触发一个事件


UITapGestureRecognizer *tapG =[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick:)];

tapG.delegate=self;

tapG.numberOfTapsRequired=2; //需要触碰2次

tapG.numberOfTouchesRequired=1;//需要一个手指

[blueView addGestureRecognizer:tapG];//把手势识别关联到blueView上

上面就是一个手势识别,当你在blueView上,单指触碰2次,就会触发tapClick:方法

以上是比较固定,好识别的手势,有些手势是没有规律可循的,比如切水果游戏中,是一指滑动切水果,角色扮演游戏中,滑动手指会使人物走动,画图板app,手指移动会留下笔画,如何定义这些手势?

事实上,我们需要在手指移动的同时就响应它,而不是等他结束了再响应,否则你的游戏就会有延时,你的画板就会等你手指离开屏幕才出现笔画,多么low的app

苹果提供了方法,让你可以实时的响应手势

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

从命名就可以看出,以上方法是何作用,顺带提一句这也是我们推荐的代码风格,从命名即可猜出用途

以上方法是UIResponder的方法,只要是他的子类,都是可以响应触摸事件的,并且按响应链顺序调用,子视图不响应,就调父视图的

哪些是UIResponder的子类呢,大家可以打开Xcode,按下shift+commond+0,打开帮助文档,可以看到每个类的继承顺序,截取几个给大家看看


看到了吗,UIView、UIViewController都是UIResponder的方法,现在想想,为啥一个View覆盖了下面的UIScrollView,你在View上滑动手指,下面的scrollView就不会滑动吗?如果这个View是scrollView的子视图,你按住View滑动,整个scrollView都滑动了呢?

答案:覆盖的情况下,他们不是父子视图的关系,不在一条响应链上,事件并没有传给scrollView,当然不会出现滑动。如果是子视图,则在一个响应链上,View本身如果不加手势识别,并不会处理触摸事件,就会传给scrollView,而scrollView已经写好如何响应滑动,就出现滑动的效果

推荐阅读更多精彩内容