响应链
事件响应流程:
触摸屏幕
【系统响应阶段】
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