只有继承了UIResponder的类才能响应touch事件,
响应顺序,优先是最上层的view响应事件,
如果该view有视图控制器的话会是下一个响应者,否者就是该
view的父视图,这样至上而下传递事件。直到单例UIWindow
对象,最后是单例UIApplication对象以终止,UIApplication
的下一个响应者是nil,已结束整个响应循环。
事件在传递过程中视图可以决定是否需要对该事件进行响应。
1. 事件的产生
发生事件后,系统会将该事件加入到一个由UIApplication
管理的事件队列中。
UIApplication会从事件队列中取出最前面的事件,
并将该事件分发下去处理。通常,先发送事件给应用程序
的主窗口(keywindow)。
keywindow会在视图层次结构中找到一个最合适的视图来处理事件。
事件分发(Event Delivery)
第一响应者(First responder)指的是当前接受触摸的响应者
对象(通常是一个UIView对象),即表示当前该对象正在与用户
交互,它是响应者链的开端。整个响应者链和事件分发的使命都是
找出第一响应者。
UIWindow对象以消息的形式将事件发送给第一响应者,
使其有机会首先处理事件。如果第一响应者没有进行处理,
系统就将事件(通过消息)传递给响应者链中的下一个响应者
,看看它是否可以进行处理。
事件的传递先从父控件传递到子控件(UIApplication
->window->寻找处理事件最合适的view)。
如果父view不能接受触摸事件,那么子view也不能接收到
触摸事件。
如何找到最合适的view来处理事件
iOS系统检测到手指触摸(Touch)操作时会将其打包成一
个UIEvent对象,并放入当前活动Application的事件队列,
单例的UIApplication会从事件队列中取出触摸事件并传递给单
例的UIWindow来处理,UIWindow对象首先会使用
hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图
(View),即需要将触摸事件传递给其处理的视图,这个过程称之为
hit-test view。
UIWindow实例对象会首先在它的内容视图上调用
hitTest:withEvent:,此方法会在其视图层级结构中的每个视图
上调用pointInside:withEvent:(该方法用来判断点击事件发
生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视
图),如果pointInside:withEvent:返回YES,则继续逐级调
用,直到找到touch操作发生的位置,这个视图也就是要找的hit-
test view。
hitTest:withEvent:方法的处理流程如下:
view会调用hitTest:withEvent:方法,
hitTest:withEvent:方法底层会调用
pointInside:withEvent:方法判断触摸点是不是在这个view的
坐标系上。如果在坐标系上,会分发事件给这个view的子view。然
后每个字view重复以上步骤,直至最底层的一个合适的view。
如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
事件的响应
事件响应会先从底层最合适的view开始,然后随着上一步找到的链
一层一层响应touch事件。默认touch事件会传递给上一层。如果到
了viewcontroller的view,就会传递给viewcontroller。如
果viewcontroller不能处理,就会传递给UIWindow。如果
UIWindow无法处理,就会传递给UIApplication。如果
UIApplication无法处理,就会传递给
UIApplicationDelegate。如果UIApplicationDelegate不
能处理,则会丢弃该事件。
当我去点击View-C的时候,hit-Testing实际上是这样检测的
1.首先,视图会先从View-A开始检查,发现触摸点在View-A,
所以检查View-A的子视图View-B。
2.发现触摸点在View-B内,好棒!看看View-B内的子
视图View-C。
3.发现触摸点在View-C内,但View-C没有子视图了,
所以View-C是此次触摸事件的hit-TestView了。
那么UIView中其实提供了两个方法来确定hit-TestView
1.- (UIView *)hitTest:(CGPoint)point
withEvent:(UIEvent *)event;
2.- (BOOL)pointInside:(CGPoint)point
withEvent:(UIEvent *)event;
//这个就是我们上面重写的方法
注意其实在每次递归去调用hitTest:(CGPoint)point
withEvent:(UIEvent *)event之前,都会调用
pointInside:withEvent:来确定该触摸点是否在该View内。
所以当我们重写pointInside:(CGPoint)point
withEvent:(UIEvent *)event后,其实我们的点击后调
用hitTest来递归的找hit-TestView的区域从这样:
去点手机屏幕,runloop都干了什么,怎么去通知到响应事件的(runloop的唤醒和休眠,调用NSNotification底层的东西)
Core Foundation和Foundation为Mach端口提供了高级API。
在内核基础上封装的CFMachPort / NSMachPort可以用做
runloop源
而这个源,正是我们经常在调用栈里看到的source0与source1
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统
事件,其回调函数为
__IOHIDEventSystemClientQueueCallback()
当我们触发了事件(触摸/锁屏/摇晃等)后
由IOKit.framework生成一个 IOHIDEvent事件
而IOKit是苹果的硬件驱动框架
由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件
它专门处理用户交互设备,由IOHIDServices和IOHIDDisplays
两部分组成
其中IOHIDServices是专门处理用户交互的,它会将事件封装成
IOHIDEvents对象,详细请看这里
然后这些事件又由SpringBoard接收,它只接收收按键(锁屏/静音
等),触摸,加速,接近传感器等几种 Event
接着用mach port转发给需要的App进程
随后苹果注册的那个 Source1 就会触发回调,并调用
_UIApplicationHandleEventQueue()进行应用内部的分发
_UIApplicationHandleEventQueue()把IOHIDEvent处理包
装成UIEvent进行处理分发,我们平时的UIGesture/处理屏幕旋
转/发送给 UIWindow/UIButton 点击、touchesBegin/
Move/End/Cancel这些事件,都是在这个回调中完成