事件处理以及响应者链条(三)
事件的产生和传递
先调用hittest找到最适合响应的控件,然后再用touchbegan把事件往下传
完整过程
1> 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。
2> 调用最合适控件的touches….方法
3> 如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者
4> 接着就会调用上一个响应者的touches….方法
事件的发生
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
包装成UIEvent后,系统会将该事件加入到一个由UIApplication
管理的事件队列中
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
,这也是整个事件处理过程的第一步
第一步的具体流程,控件不断调用以下代码:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//判断当前状态是否不能响应事件
if(self.hidden == YES || self.alpha <= 0.01 || self.userInteractionEnabled == NO){
return nil;
}else if ([self pointInside:point withEvent:event] == NO){//判断是否包含这个点,没有就返回
return nil;
}else{//继续遍历
return [super hitTest:point withEvent:event];
}
}
hitest方法
4> 03-hitText方法和pointInside方法(02-事件传递代码)
* (了解hitText)学习一个方法必须了解:什么时候调用和这个方法有什么用
1. hitText什么时候调用:当一个事件传递给一个控件的时候,控件就会调用这个方法
2. hitText作用: 寻找到最合适的view。
* (回顾下事件传递),UIApplication -> UIWindow
* UIWindow去寻找最合适的view? [UIWindow hitTest:withEvent:]里面做了什么事情?
1> 判断窗口能不能处理事件? 如果不能,意味着窗口不是最合适的view,而且也不会去寻找比自己更合适的view,直接返回nil,通知UIApplication,没有最合适的view。
2> 判断点在不在窗口
3> 遍历自己的子控件,寻找有没有比自己更合适的view
4> 如果子控件不接收事件,意味着子控件没有找到最合适的view,然后返回nil,告诉窗口没有找到更合适的view,窗口就知道没有比自己更合适的view,就自己处理事件。
* 验证下hitTest方法返回nil,里面的子控件能处理事件吗? 重写根控制器view的hitTest:withEvent:方法,
* 验证这个方法是否真能找到最合适的view?
* 如果点击屏幕任何一个地方,都是白色的view,怎么做。直接返回白色的view,就不会继续去找白色view的子控件了。
* pointInside作用:判断一个点在不在一个控件上
* point参数:方法调用者坐标系上的点,PPT画图分析原理。
示例
Snip20150711_1.png触摸事件的传递是从父控件传递到子控件
- 点击了绿色的view:
UIApplication -> UIWindow -> 白色 -> 绿色 - 点击了蓝色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 - 点击了黄色的view:
UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色
如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件(掌握)
具体流程如下:
- 如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?
- 触摸点是否在自己身上?
- 从后往前遍历子控件,重复前面的两个步骤
- 如果没有符合条件的子控件,那么就自己最适合处理
三种情况下,控件不接收触摸事件
- 控件的userinterfaceEnable = NO;
- 透明度低于0.01
- 控件被隐藏了
而且,某个控件隐藏,或者透明度低于0.01,它的子控件都会看不见,同样无法接受触摸事件, 同时,UIImageView的userinterfaceEnable默认为NO。
触摸事件处理的详细过程,响应者链条
用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理
touchesBegan…
touchesMoved…
touchedEnded…
这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理
响应者链条示意图
响应者链条:是由多个响应者对象连接起来的链条
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
响应者对象:能处理事件的对象
Snip20150711_3.png
在顶级视图(key window的视图)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;
如果返回NO,那么A返回nil;
如果返回YES,那么它会向当前视图的所有子视图(key window的子视图)发送hitTest:withEvent:消息,遍历所有子视图的顺序是从subviews数组的末尾向前遍历(从界面最上方开始向下遍历)。
如果有subview的hitTest:withEvent:返回非空对象则A返回此对象,处理结束(注意这个过程,子视图也是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。并且这个过程中如果子视图的hidden=YES、userInteractionEnabled=NO或者alpha小于0.1都会并忽略);
如果所有subview遍历结束仍然没有返回非空对象,则A返回顶级视图;