原理
iOS 把用户触摸事件打包成一个 UIEvent 对象,作为事件传递的消息载体,放入当前活跃的 APP 的消息队列中,然后通过 Hit-Test 机制 来找到响应者,响应者通过响应链(Responder Chain)的传递做出响应,这就是 iOS 事件分发机制的实现原理
UIEvent 有哪些
UIEvent 包含最常见的三种事件:Touch Events(触摸事件)、Motion Events(运动事件,比如重力感应和摇一摇等)、Remote Events(远程事件,比如用耳机控制手机)。这里我们只讨论触摸事件
Hit-Test 机制
如图,我点击了 E,Hit-Test 机制是如何找到这个 View 呢?
1 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event |
其中 UIView 的 pointInside:withEvent:
方法的作用是,判断当前的点是否在当前 View 的 bounds 中
1 | - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event |
注意,以下情况,Hit-Test 函数返回 nil
- view.isHidden = YES
- view.alpha <= 0.01
- view.userInterfaceEnable=NO
- control.enable = NO(如果是 UIControl)
其次注意,子视图的遍历是逆序的,为了保证相同层级下的子视图,离用户越近的优先得到响应
Responder Chain(响应链)
在 UIKit 中,UIApplication、UIView、UIViewController 这几个类都是直接继承自 UIResponder 类;而响应链是由 UIResponder 组合而成的数组,起始于 FirstResponder,结束于 UIApplication
用户触摸屏幕后,系统通过 Hit-Test 机制找到响应的 UIView,即 FirstResponder;如果该 UIResponder 不处理该事件,则会交给它 的下一个 UIResponder,如果该 UIResponder 处理则停止,否则继续递归直到响应链结束
- UIView 的 nextResponder 属性,如果有管理此 view 的 UIViewController 对象,则为此 UIViewController 对象;否则 nextResponder 即为其 superview
- UIViewController 的 nextResponder 属性为其管理 view 的 superview
- UIWindow 的 nextResponder 属性为 UIApplication 对象
- UIApplication 的 nextResponder 属性为 nil。
应用
寻找 UIView 所在的 Controller
1 | @implementation UIView (Controller) |
扩大按钮点击区域
重写以下方法即可
1 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event |
子 view 超出了父 view 的 bounds 响应事件
正常情况下,子 View 超出父 View 的 bounds 的那一部分是不会响应事件的
解决方法1:重写父 View 的 pointInside 方法
这种方法会导致如果点击在父 View (而不是其子 View)上时,不会再响应任何事件,父 View 就像变透明了一样
1 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event |
解决方法2:重写父 View 的 hitTest 方法(推荐)
1 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event |
实现一个透明的 View,点击子 View 有效,点击自身无效
1 | // 播放器中用到的 QNBPlayerIntellectView |