事件的产生和传递
iOS系统检测到手指触摸(Touch)操作时会将其放入到一个由UIApplication管理的事件队列中。UIApplication会从事件队列中取出最前面的触摸事件并传递给key window(当前接收用户事件的窗口)处理。
key window会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件。
hitTest:withEvent:方法的处理流程如下:
- 首先调用当前视图的
pointInside:withEvent:方法判断触摸点是否在当前视图内。 - 若返回NO,说明触摸点不在当前视图内,则当前视图的
hitTest:withEvent:返回nil。 - 若返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的
hitTest:withEvent:方法重复前面的步骤,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。 - 若第一次有子视图返回非空对象,则
hitTest:withEvent:方法返回此对象,处理结束。 - 如所有子视图都返回非,则
hitTest:withEvent:方法返回自身self。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理:
touchesBegan…
touchesMoved…
touchedEnded…
- 这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理。
注意:如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件
如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?
- 触摸点是否在自己身上?
- 从后往前遍历子控件,重复前面的两个步骤
- 如果没有符合条件的子控件,那么就自己最适合处理
事件传递的完整过程
- 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。
- 调用最合适控件的touches….方法。
- 如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者。
- 接着就会调用上一个响应者的touches….方法。
如何判断上一个响应者?
- 如果当前这个view是控制器的view,那么控制器就是上一个响应者。
- 如果当前这个view不是控制器的view,那么父控件就是上一个响应者。
响应者链的事件传递过程
对于左边的app,事件传递按下面的路径:
1. Initial view 尝试着去处理事件或者消息。如果不能处理事件,它就递交事件给superview,因为这个initial view并不是视图控制器层级中得顶级view.
2. 这个superview尝试去处理该事件,如果superview不能处理该事件,它就递交事件给它的父view,因为它也不是view层级的顶级view。
3. 视图控制器的顶级view尝试着去处理该事件,如果连顶级view都不能处理该事件,它就递交事件给它的controller。
4. 这个viewcontroller尝试着去处理该事件,并且如果它不能处理该事件,它就会递交事件给window。
5. 如果window不能处理该事件,它就递交事件给singlegon app object(既UIApplication)
6. 如果连application都不能处理该事件,那么毫无疑问该事件将会被丢弃。
右边的应用传递按照稍微不同的路径,但是所有的事件传递都遵循相同的传递规则:
1. 一个view controller层级中得view向上递交事件知道它到达顶级view。
2. 顶级view递交事件给它的controller。
3. Viewcontroller递交事件给它的顶级view的superview,步骤1-3重复直到它到达rootcontroller。
4. 这个rootviewcontroller递交事件给window对象。
5. window对象递交事件给application对象。
UIView不接收触摸事件的三种情况
hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。
提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的。