一、事件起因
最近在开发一版本的需求中,遇到一个问题,需要在一个ViewController的顶部,UINavgationBar的下面放置一个View,这个View需要能够正常收到事件
将我们的View放到这个位置之后,发现底部的View、按钮等无法接受到响应
二、解决思路
1)第一次想到的是事件转发,如果控制了事件分发是不是就可以将事件想发给谁就发给谁了呢?
我们先回忆一下,事件分发的过程:
1、首先用户触摸屏幕产生触摸事件(此外还有Motion Event,Remote Event),手机系统驱动收到这个触摸事件
2、系统产生中断,进入响应触摸事件的终端程序,此时肯定是系统将事件封装为UIEvent,并发送给最前台的进程
3、进程收到这个事件之后,将其发给UIApplication进行处理
4、UIApplication 收到这个UIEvent事件之后,会进行一个事件的分发,决定将这个事件发给最终的响应者进行处理
这里涉及到两部分,建立事件响应链、沿着事件响应链处理事件
建立事件响应链,其实是寻找最合适的事件响应者(responder)的过程,步骤大概如下
首先UIApplication对象将事件发给UIWindow,UIWindow会调用从UIView 继承的 hitTest方法返回第一响应者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
hitTest 是一个递归当用的方法,找到触摸事件的第一响应者,流程是,对Window下面的所有子View调用hitTest方法,
如果触摸点 point Inside子view中,那么相当于找到下一级,再次进行递归寻找,直到找到最终的响应者。
以上是事件寻找响应者的过程、如果第一响应者不能处理这个事件,会将这个事件沿着nextResponder逐步向上传递,进行处理
5、当找到第一响应者之后,会有响应的回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
如果这个响应者上绑定有手势识别器,那么手势识别器会对这个事件进行延迟、等识别
这就是UIScrollView上面按钮响应慢的原因,因为第一个touchBegan来了之后,手势识别器需要后面的事件一起判断是滑动还是点击
2)如何实现UINavgationBar事件穿透功能
我们可以通过修改寻找响应者的逻辑,来将事件抛回上层,再次递归到底层的View来实现,具体如下
首先我们继承一个UINavgationBar,修改hitTest的流程
//穿透点击事件 @objc class UICustomTouchNavigationBar: UINavigationBar { @objc var passThroughEvent:Bool = false override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let v = super.hitTest(point, with: event) if (passThroughEvent && v?.ignoreEvent ?? false) { //启用事件穿透的时候 return nil } return v; } } var UIViewIgnoreEventKey = "UIViewIgnoreEventKey" @objc extension UIView { public var ignoreEvent:Bool { get{ guard let value = objc_getAssociatedObject(self, &UIViewIgnoreEventKey) as? Bool else { return false } return value } set{ objc_setAssociatedObject(self, &UIViewIgnoreEventKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN) } } }
我们要做的是,在UINavgationBar上面的按钮等收到事件的时候,我们不忽略;在空白的地方收到事件的时候,我们直接返回nil进行忽略
在初始化导航的时候,传入我们自定义的NavBar
- (instancetype)initWithNavigationBarClass:(nullable Class)navigationBarClass toolbarClass:(nullable Class)toolbarClass NS_AVAILABLE_IOS(5_0);
因为UINavgationBar是属于每个ViewController,也就是独立的,所以我们只需要在ViewDidLoad中启用事件穿透,并且把UINavgationBar的子View中的时间忽略标志打开,那么事件将自动传递到底层中。
三、附言
1)图片来自
https://www.jianshu.com/p/74a2f44840fa
https://www.jianshu.com/p/4155c9ffe1a8