iOS的事件有好几种:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机上得按键来控制手机),其中最常用的应该就是Touch Events了,我们先来谈谈触摸事件。
凡是继承于UIResponder的类都可以响应事件;包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events,如插入耳机调节音量触发的事件);iOS里面通常将这些能响应事件的对象称之为响应者。
相关API:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; //决定谁响应事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event;//最终响应着的具体调用函数
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event; //判断触摸点是否在自己视图上
响应者链:
我们的app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的。如下图:
Hit-Test:
有了事件响应链,接下来的事情就是寻找响应事件的具体响应者了,我们称着为:Hit-Testing View,寻找这个View的过程我们称着为Hit-Test。
那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView,注意hit-test是UIView中的函数,hit-Test代码里面其实就是一个函数,UIView有如下两个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
point为点击点相对本view左上角的相对位置,此函数返回一个合适处理事件的view
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
此函数判断点击点是否在自己上面
每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,这个view和view上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,我们可以看到UITouch头文件里面有一个view和gestureRecognizers的属性,就是hitTest view和它的手势。
现在知道Hit-Test是什么时候调用了,那么接下来看看它是怎么工作的。Hit-Test是采用递归的方法从view层级的根节点开始遍历,看看下面这张图:
UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面),其中view A和view B有一部分重叠。如果手指在view B.1和view A.2重叠的上面点击,按照上面说的递归方式,顺序如下图所示:
注意:由于View C在最上面所以先hitTest View C视图
递归是向界面的根节点UIWindow发送hitTest:withEvent:消息开始的,从这个消息返回的是一个UIView,也就是手指当前位置最前面的那个 hittest view。 当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问)。如果当前的point没有在view上面,那么这个view的subview也就不会被遍历了。当事件遍历到了view B.1,发现point在view B.1里面,并且view B.1没有subview,那么他就是我们要找的hittest view了,找到之后就会一路返回直到根节点,而view B之后的view A也不会被遍历了
注意hitTest里面是有判断当前的view是否支持点击事件,比如userInteractionEnabled、hidden、alpha等属性,都会影响一个view是否可以相应事件,如果不响应则直接返回nil。 我们留意到还有一个pointInside:withEvent:方法,这个方法跟hittest:withEvent:一样都是UIView的一个方法,通过他开判断point是否在view的frame范围内。如果这些条件都满足了,那么遍历就可以继续往下走了,代码表现大概如下:
问题:如何父view和子view同时响应事件?
答:子view的nextResponder是父view,在子view中重写:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{
[self.nextRespondertouchesBegan:toucheswithEvent:event];
[supertouchesBegan:toucheswithEvent:event];
}
-(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event{
[self.nextRespondertouchesMoved:toucheswithEvent:event];
[supertouchesMoved:toucheswithEvent:event];
}
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event{
[self.nextRespondertouchesEnded:toucheswithEvent:event];
[supertouchesEnded:toucheswithEvent:event];
}
-(void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event{
[self.nextRespondertouchesCancelled:toucheswithEvent:event];
[supertouchesCancelled:toucheswithEvent:event];
}即可完成