一、什么是响应链?
大多数事件的分发都是依赖响应链的。
响应链
是由一系列链接在一起的响应者
(UIResponse子类:UIApplication
、UIViewController
、UIView
)组成的。一般情况下,一条响应链开始于第一响应者,结束于application对象。如果一个响应者不能处理事件,则会将事件沿着响应链传到下一响应者。
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;复制代码
二、事件的传递与响应
2.1、事件的传递:寻找事件的第一响应者(Hit-Testing)
事件被苹果分为3种大类型: 触摸事件,加速计事件以及远程遥控事件
当一个事件发生后,事件会从父控件传给子控件,也就是说由
硬件
->系统
->UIApplication
->UIWindow
->SuperView
->SubView
以上就是事件的传递,也就是寻找
第一响应者
的过程。 符合第一响应者
的条件包括:
- touch事件的位置在响应者区域内
pointInside:withEvent: == YES
- 响应者
self.hidden != NO
- 响应者
self.alpha > 0.01
- 响应者
self.userInteractionEnabled = YES
- 遍历 subview 时,是从上往下顺序遍历的,即 view.subviews 的 lastObject 到 firstObject 的顺序,找到合适的响应者view,即停止遍历.
第一响应者
对于接收到的事件有3种操作:
- 不拦截,默认操作。事件会自动沿着默认的响应链往下传递
- 拦截,不再往下分发事件。重写
touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
- 拦截,继续往下分发事件。重写
touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
下图展示了Hit-Testing的逻辑
2.2、事件的响应:一旦事件的第一响应者确定了,这个事件所处的响应链就确定了
案例一:下图是官网对于响应链的示例展示
- 图中虚线箭头是指若该
UIView
是作为UIViewController
根视图存在的,则其nextResponder
为UIViewController
对象; - 若是直接add在
UIWindow
上的,则其nextResponder
为UIWindow对象。
// 若触摸发生在UITextField上,则事件的传递顺序是:UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation复制代码
案例二:参考下图
- 1、 首先由 view 来尝试处理事件,如果他处理不了,事件将被传递到他的父视图
superview
- 2、
superview
也尝试来处理事件,如果他处理不了,继续传递他的父视图UIViewcontroller.view
- 3、
UIViewController.view
尝试来处理该事件,如果处理不了,将把该事件传递给UIViewController
- 4、
UIViewController
尝试处理该事件,如果处理不了,将把该事件传递给主窗口Window
- 5、主窗口
Window
尝试来处理该事件,如果处理不了,将传递给应用单例Application
- 6、如果
Application
也处理不了,则该事件将会被丢弃
事件的传递和响应的区别?
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
如何判断上一个响应者?
如果当前这个view是控制器的view,那么控制器就是上一个响应者 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链条的事件传递过程?
如果view
的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window
对象进行处理 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication
对象 如果 UIApplication
也不能处理该事件或消息,则将其丢弃(销毁)
如何做到一个事件多个对象处理?
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // 1.自己先处理事件... NSLog(@"do somthing..."); // 2.再调用系统的默认做法,再把事件交给上一个响应者处理 [super touchesBegan:touches withEvent:event]; }复制代码
事件的生命周期
1、系统响应阶段
2、APP响应阶段
- 1、
重写子view的point:inside`` →
扩大Button的点击区域(上下左右各增加20) - 2、
重写父view的point:insde`` →
子view超出了父view的bounds响应事件 - 3、 如果一个Button被一个View盖住了,在触摸View时,希望该Button能够响应事件
- 4、 特殊的UIScrollView
- 5、
总结
- 1、如果父控件不能接收触摸事件,则子控件也无法接收触摸事件
- 2、如果想让控件不处理触摸事件,可以设置
userInteractionEnabled = NO
,结果是包括父控件在内的所有子控件都不能处理触摸事件(虽然设置透明度和hidden=YES
也可以,但是那样就看不见了注意:如果父控件的透明度设置为0或者hidden=YES,那么子控件也是不可见的。) - 3、遍历一个控件的子控件的顺序是从上到下的(最后添加的view最先被遍历)。
- 4、指定某一个子控件响应事件,只需要在父控件的hitTest中返回指定的子控件就可以。
- 5、如果一个控件的
isUserInteractionEnabled=false
,想让它继续继续处理触摸事件,可以在它的父控件的hitTest方法中直接返回它。 - 6、hitTest查找第一响应者的时候,即便父控件是第一响应者,还是要调用子控件的hitTest方法,否则怎么知道是不是还有其他最合适的响应者
- 7、
-
- → 1、先调用父控件的
point:inside:
方法
- → 1、先调用父控件的
-
- → 2、调用最上面子控件的
point:inside:
方法
- → 2、调用最上面子控件的
-
- → 3、如果最上面子控件的
point:inside:
方法返回false,则对应的hitTest返回nil
- → 3、如果最上面子控件的
-
- → 4、如果最上面子控件的
point:inside:
方法返回true,则调用对应的hitTest方法重复上面的操作返回子控件的最合适子控件
- → 4、如果最上面子控件的
疑问?
UIGestureRecongnizer
、UIContorl
都可以处理触摸事件-
UIGestureRecognizer
:使用addGestureRecognizer
方法处理事件
-
UIControl
:使用addTarget
方法处理事件
-
UIResponder
:使用touches
等一系列方法处理事件
UIButton继承自UIControl
,UIControl继承自UIView
,如果给UIButton
添加了手势,并实现了自己的处理事件的>>方法,当点击UIButton
的时候发现touches
方法走了,手势方法(addGestureRecognizer
)也走了,自己的方法(addTarget
)没有走。由此可以得出一个结论:
UIGestureRecognizer的优先级
> >UIContol的优先级
,当一个UIButton即实现了自己的方法(addTarget
),又添加了手势方法(addGestureRecognizer
)的话,自己的方法(addTarget
)会被屏蔽掉,不管是否添加了手势,touches
方法都会处理。