响应链 事件响应流程: 触摸屏幕 【系统响应阶段】 IOKit.framework 处理 通过mach port 转发 SpringBoard (IPC 进程间通信) SpringBoard.app 处理 【事件被封装成 IOHIEvent 对象】 通过mach port 转发给app 的主线程 (IPC 进程间通信) 【进入app 阶段】 Event事件传到app,主线程的runloop被唤醒 触发source 1 回调 source 1 回调,转换并触发 source 0 回调,触摸事件被封装为 UIEvent 对象。 source 0 回调事件,将Event对象 事件 添加到 UIAplication 到事件队列里,等 待处理 事件出队列(先进先出 FIFO) UIAplication 开始为该事件 【寻找 最佳响应者】 如何寻找最佳响应者下面细谈 这个过程是从 底层的UIAplication 一直递归到 屏幕顶的view 找到响应者后,接着在响应链中 【传递响应 】了。 有响应者view 依据其 nextRespoonder 往父视图 传递 并非都会传递响应链,如果响应者自己完全处理事件并拦截的话,就不会传递响 事件被响应者消耗,被手势识别器或是target-action模式捕捉并消耗掉。 优先级:UIControl > UIGestureRecognizer > UIResponder runloop 如果没有其它事件就会进入休眠中 UITouch 单击产生一个UITouch 对象 双击就是一个UITouch 对象,更新了两次 (tap count 1 变2) 双指同时触摸 产生 两个 UITouch 对象 每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的 视图、窗口(UIWindow)等信息 UIEvent 事件的对象 包含触摸事件类型,触摸对象UITouch 的集合 allTouchs 等 UIResponder 触摸事件绝大部分最后都是 被 继承 UIResponder 都对象处理 常见响应者: - UIView - UIViewController - UIApplication - AppDelegate UIResponder 及子类都实现如下方法: //手指触碰屏幕,触摸开始 - (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; 寻找事件的最佳响应者 (Hit-Testing) 简单理解就是,处理事件之前,要先找到响应此事件的对象。 通过subviews 获取子view 事件传递顺序如下: UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图 如果多个Window 怎么知道是哪个? touch对象保存了触摸所属的window及view hitTest:withEvent: 每个view都有此方法,通过此反法返回结果判断是否支持触摸事件响应 以下几种状态的视图无法响应事件: - 不允许交互:userInteractionEnabled = NO - 隐藏:hidden = YES 如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无 法接收事件 - 透明度:alpha < 0.01 如果设置一个视图的透明度<0.01,会直接影响子视图 的透明度。alpha:0.0~0.01为透明。 默认hitTest: 递归逻辑是: - 若当前视图无法响应事件,则返回nil - 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视 图层次中的事件响应者 - 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事 件响应者 我们可以重写 hitTest 方法,添加判断逻辑来完成自定义 响应者 pointInside: withEvent: 光支持 hitTest:withEvent:还不够,还需要支持pointInside: withEvent: ,确保 点击位置在该对象区域内才行 以上两个方法是响应者都必须同时满足的情况 事件的传递 (响应链) 前面 我们通过subviews 和 递归,和 hitTest ,pointInside 方法,最终找到 最 佳事件响应者 响应者对于事件的拦截以及传递都是通过 `touchesBegan:withEvent:` 方法控 制的,该方法的默认实现是将事件沿着默认的响应链往下传递。 最佳响应者 此时已经拿到了 处理事件的控制权, 那么接着该怎么做呢? 响应者对于接收到的事件有3种操作: - 不拦截,默认操作 事件会自动沿着默认的响应链往下传递 - 拦截,不再往下分发事件 重写 `touchesBegan:withEvent:` 进行事件处理,不调用父类的 `touchesBegan:withEvent:` - 拦截,继续往下分发事件 重写 `touchesBegan:withEvent:` 进行事件处理,同时调用父类的 `touchesBegan:withEvent:` 将事件往下传递 注意: 也就是说,如果响应者只自己处理,那么就不会走 响应链,整个事件也就到此结 束了 响应链是如何走的? 若触摸发生在UITextField上,则事件的传递顺序是(假设简单加个 TextField): - UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation 一个按钮view 实际响应链流程是: 完整响应链如下: CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate UIGestureRecognizer 手势识别器 疑问: 如果在寻找响应者的时候,同时发现该对象还支持 UIGestureRecognizer 代理 事件,那么会如何呢? hitTest逻辑被拦截并取消了,也就是UIResponder 的那些代理方法不会处理了 UIGestureRecognizer 优先级 > UIResponder Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识 别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit- tested view对事件的响应 若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权 疑问? 递归过程中,如何知道这个对象支持 UIGestureRecognizer ? event所绑定的touch对象上存在 对应的UIGestureRecognizer 列表(不单是一 个) 持续滑动场景下,会导致什么问题? 手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事 件,仅向hit-test view发送事件。 UIControl UIControl是系统提供的能够以target-action模式处理触摸事件的控件 iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子 类。 UIConotrol是UIView的子类,因此本身也具备UIResponder应有的身份 有时候UIView 的touch事件没响应,但是使用UIButton却可以响应 响应优先级: 默认优先级:UIControl > UIGestureRecognizer > UIResponder 只有系统提供的有默认action操作的UIControl,例如UIbutton、UISwitch等的 单击,优先级:UIControl > UIGestureRecognizer 。 而对于自定义的UIControl,经验证,响应优先级比手势识别器低 UIGestureRecognizer > UIControl > UIResponder 响应方法: - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event; - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent: (nullable UIEvent *)event; - (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent: (nullable UIEvent *)event; - (void)cancelTrackingWithEvent:(nullable UIEvent *)event; UIControl只能接收单点触控,因此接收的参数是单个UITouch对象。 添加方式: UIControl控件通过 `addTarget:action:forControlEvents:` 添加事件处理的target和action target-action - target:处理交互事件的对象 - action:处理交互事件的方式 可以通过重写UIControl的 `sendAction:to:forEvent:` 或 `sendAction:to:from:forEvent:` 自定义事件执行的target及action