前言(铺垫一下)
当发生事件时必须知道由谁来响应事件。
所有事件响应的类都是UIResponder的子类,
响应链是一个由不同对象组成的层次结构,
其中的每个对象将依次获得响应事件消息的机会。
当发生事件时 事件首先被发送给第一响应者,
第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。
事件将沿着响应者链一直向下传递,直到被接受并做出处理。
一般来说第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理事件就会被传递给它的视图控制器对象viewcontroller(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推直到顶层视图。接下来会沿着顶层视图(top view)到窗口(UIWindow对象)再到程序(UIApplication对象)。
如果整个过程都没有响应这个事件,该事件就被丢弃。
在响应者链中只要由对象处理事件,事件就停止传递。
典型的 事件响应链 路线图:
由离用户最近的view向系统传递。
First Responser -- >View -- >SuperView...-- >The Window -- >The Application -- > App Delegate
响应者链流程经常被委托(delegation)打断,
一个对象(通常是视图)可能将响应工作委托给另一个对象来完成(通常是视图控制器ViewController),所以 做事件响应时在ViewController中必须实现相应协议来实现事件委托。
iOS中的UIResponder类,定义了响应者对象的所有方法。UIApplication、UIView等类都继承了UIResponder类,UIWindow和UIKit中的控件因为继承了UIView,所以也间接继承了UIResponder类,这些类的实例都可以当作响应者。
下图网络出处:https://www.cnblogs.com/Julday/archive/2019/12/30/12119536.html
事件的(传递,响应).pngiOS事件的类型
iOS用户操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。
对应的事件类型如下:
1、触屏事件 (Touch Event)
2、运动事件 (Motion Event)
3、远端控制事件(Remote-Control Event)
惯例我们就以触屏事件Touch Event为例说明在Cocoa Touch框架中事件的处理流程。
响应链(Responder Chain)
响应者对象(Responder Object)指的是有响应和处理事件能力的对象。
响应链 就是由一系列的 响应者对象 构成的一个层次结构。
UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。
响应链的基本构成.png
上图可见 响应者链的一些特点:
1、响应者链通常是由视图(UIView)构成的;
2、视图的下一个响应者是它de视图控制器UIViewController(当然如果有它的话),然后再转给它的父视图Super View;
3、视图控制器 的下一个响应者shi其管理的视图的父视图;
4、单例的窗口UIWindow的内容视图将指向窗口本身作为它的下一个响应者;
5、单例的应用UIApplication是一个响应者链的终点,它的下一个响应者指向nil,结束整个循环。
Cocoa应用可以有多个UIWindow对象。
Cocoa Touch应用只有一个UIWindow对象,整个响应者链比较简单。
事件分发(Event Delivery)
第一响应者First responder指的是当前接受触摸的响应者对象(通常是一个UIView对象),即当前该对象正在与用户交互,它是响应者链的开端。
事件传递的流程图.png事件传递的最终目的:找出一个能处理并响应事件的对象(第一响应者First responder)
如何寻找第一响应者(事件传递的过程)
事件传递链:
由系统向离用户最近的view传递。
UIKit –> active app's event queue –> window –> root view –> …… –> lowest view
1、当iOS程序发生触摸事件后,系统会利用Runloop将事件加入到UIApplication的任务队列中,具体过程可以参考深入理解RunLoop
2、UIApplication分发触摸事件到UIWindow,然后UIWindow依次向下分发给UIView
3、UIView调用hitTest:withEvent:
方法看看自己能否处理事件,以及触摸点是否在自己上面。
4、如果满足条件,就遍历UIView上的子控件。重复上面的动作。
5、直到找到最顶层的一个满足条件(既能处理触摸事件,触摸点又在上面)的子控件,此子控件就是我们需要找到的第一响应者。
hitTest:withEvent:的处理流程
(上面的查找其实就是由该方法递归调用实现的)
1、首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
2、若返回NO,则hitTest:withEvent:返回nil;
3、若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
4、若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
5、如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
hitTest:withEvent:方法的伪代码大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.userInteractionEnabled || !self.hidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
CGPoint subPoint = [subView convertPoint:point fromView:self];
UIView *bestView = [subView hitTest:subPoint withEvent:event];
if (bestView) {
return bestView;
}
}
return self;
}
return nil;
}
事件的响应流程
通过上面的 hitTest:withEvent: 寻找到第一响应者后,需要逆着寻找第一响应者的方向(从第一响应者->UIApplication)来响应事件。
流程如下
1.首先通过 hitTest:withEvent: 确定第一响应者,以及相应的响应链
2.判断第一响应者能否响应事件,如果第一响应者能进行响应则事件在响应链中的传递终止。如果第一响应者不能响应则将事件传递给 nextResponder也就是通常的superview进行事件响应
3.如果事件继续上报至UIWindow并且无法响应,它将会把事件继续上报给UIApplication
4.如果事件继续上报至UIApplication并且也无法响应,它将会将事件上报给其Delegate
5.如果最终事件依旧未被响应则会被系统抛弃
哪些视图不响应呢?
hidden = YES 视图被隐藏
userInteractionEnabled = NO 不接受响应事件
alpha <= 0.01,透明视图不接收响应事件
子视图超出父视图范围
需响应视图被其他视图盖住
是否重写了其父视图以及自身的hitTest方法
是否重写了其父视图以及自身的pointInside方法
应用场景
方形按钮点击四角无效,点击中间的圆形区域有效。
核心思路:在pointInside: withEvent:方法中修改对应的区域。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 如果控件不允许与用用户交互,那么返回nil
if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
return nil;
}
//判断当前视图是否在点击范围内
if ([self pointInside:point withEvent:event]) {
//遍历当前对象的子视图(倒序)
__block UIView *hit = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//坐标转换,把当前坐标系上的点转换成子控件坐标系上的点
CGPoint convertPoint = [self convertPoint:point toView:obj];
//调用子视图的hitTest方法,判断自己的子控件是不是最适合的View
hit = [obj hitTest:convertPoint withEvent:event];
//如果找到了就停止遍历
if (hit) *stop = YES;
}];
//返回当前的视图对象
return hit?hit:self;
}else {
return nil;
}
}
// 该方法判断触摸点是否在控件身上,是则返回YES,否则返回NO,point参数必须是方法调用者的坐标系
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;
//判断是否在圆形区域内
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}
网友评论