iOS 事件传递机制
iOS的事件分为以下几种
Touch Events
触摸事件-
Shake-motion events
运动事件,比如重力感应 -
Remote-control events
远程控制事件,穿戴设备控制手机 Press events
按压事件-
Editing menu messages
编辑菜单信息事件? 应该叫啥?
下面主要讲解触摸事件: 分为两个过程:
- 传递: 触摸屏幕的时候,寻找合适的
View
;- 响应: 找到最合适的
View
之后,继续寻找能响应此事件的View
;
事件过程1:事件的传递
1. 什么是响应者对象?
答:有响应和处理事件能力的对象。
所有的响应者都是继承于 UIResponder
,也就是说UIResponder
是所有响应事件类的基类。UIView
, UIViewController
, UIApplication
都是响应事件的载体。
open class UIResponder : NSObject, UIResponderStandardEditActions {
open var next: UIResponder? { get }
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
}
2. 什么是响应链?
由一系列的响应者对象构成的一个层级结构。
3. 什么是next( oc中叫nextResponder)
?
答:用于获取响应链中当前对象的下一个响应者。
-
UIView
若view是控制器的根视图,则其nextResponder
是控制器对象;否则nextResponder
为Controller
(父视图)。 - UIViewController
若控制器的视图是window
的根视图,则nextResponder
为窗口对象,否则nextResponder
为该控制器的view
的父视图。 -
UIWindow
nextResponder为UIApplication对象。 -
UIApplication
若当前的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate
3. 响应链如何构建的?
- 当一个
view
被add
到superView
上,它的nextResponder
就被指向了它的superView
; - 当
controller
被初始化的时候,self.view
的nextResponder
就被指向了所在的controller
,controller
的nextResponder
会指向self.view
的superView
,这样整个app就被串成了一条响应链条;
4. 寻找响应者(Hit-Test View)
通过Hit-Test可以找到手指点击点处于屏幕最前面的那个UIView。
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
只要事件一传递给一个控件,这个控件就会调用自己的该方法寻找并返回能响应事件的合适view。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
执行了hitTest
方法,系统底层会调用该方法,判断触摸点是否在当前的view上,如果不在当前view上,就不能处理事件。
iOS 系统检测到手指触摸操作时,通过递归方法向界面的根节点UIWindow发送hitTest:withEvent:
消息。
当前的window调用pointInside:withEvent:
,判断触摸点是否在范围内,如果在,则 倒叙遍历 window的subviews,然后依次对subview发送hitTest:withEvent:
消息。如果当前subview调用pointInside:withEvent:
判断触摸点是否在自己范围内,如果不在,这个view的subviews也就不会被遍历了。
直到遍历到触摸点在view里面,并且view没有subview,那么它就是我们要找的hit-test view了,找到之后就会一路返回直到根节点,而该view之后的view就不会被遍历了。
- 这是一个由底到上的过程:由window开始遍历,直到找到合适的view。
- 倒叙遍历子视图: 因为视图的层级结构会出现挡住的情况。
- 若
hitTest:withEvent:
返回了此对象,不再遍历其他view,处理结束。
当事件遍历到了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范围内。如果这些条件都满足了,那么遍历就可以继续往下走了
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.hidden ||
self.alpha < 0.01 ||
!self.userInteractionEnabled ||
![self pointInside:point withEvent:event] ) {
return nil;
} else {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
}
注意: 此时将事件交给找到的Hit-Test View
,但是并不一定是Hit-Test View
来响应事件。
## 事件过程2:事件的响应
当我们知道最合适的 View 后,事件会 由上向下【子view -> 父view,控制器view -> 控制器】来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了如下的方法:
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
如果有实现那么就由此 View 响应,如果没有实现,那么就会传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器】。一旦找到最合适响应的View就结束, 在执行响应的绑定的事件,如果没有就抛弃此事件。