「iOS」————响应者链与事件传递链
iOS学习
- 响应者链和事件传递链
- 传递链:
- hitTest:withEvent
- **pointInside:withEvent**
- 响应链
- 第一响应者和最佳响应者
- 触摸事件(UITouch)
- UIGestureRecognizer(手势识别器)
响应者链和事件传递链
iOS事件的主要由:响应连和提交链构成。一般事件先通过提交链,提交下去。响应链,如果上层不能响应,那么一层通过响应链找到能响应的UIResponse
。
-
响应连:由最基础的
view
向系统提交,first view
->super view
-> … ->view controller
->window
->Application
->AppDelegate
-
交付链:有系统向最上层
view
交付,Application
->window
->root view
-> … ->first view
iOS中只有继承了UIResponse
的对象才能够接受处理事件。UIResponse
是响应对象的基类,定义了处理上述各种事件的接口。常见的子类有:UIView
,UIViewController
,UIApplication
和UIApplicationDelegate
。
穿透控件:
如果我们不想让某个视图响应事件,只需要重载 PointInside:withEvent:方法,让此方法返回NO就行了.
若是view上有view1,view1上有view2,点击view2,view2自己响应,点击view1,view1不响应,只有view响应,也就是隔层传递
与加速器,陀螺仪和磁力计相关的运动事件不遵循响应者连,Core Motion
将这些事件直接传递给你指定的对象
传递链:
事件传递流程:
- 发生触摸事件后,系统会将该事件封装成
UIEvent
对象加入到一个由UIApplication管理的事件队列
UIApplication
会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)
。- 主窗口会调用
hitTest:withEvent:
方法沿着视图层次结构从上到下进行传递最后在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。 - 找到合适的视图控件后,就会调用视图控件的touches方法(touchesBegan、touchesMoved、touchedEnded)来作具体的事件处理。
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
触摸事件的传递是从父控件传递到子控件,如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
上文中说到:要找到最合适的控件来处理事件,那么,如何找到呢?
找到最合适的控件:
- 自己是否能接受触摸事件?
- 触摸点是否在自己身上
- 通过
pointInside:withEvent
方法判断触摸点是否在自己身上。返回NO则不在自己身上,那就不再遍历子控件,返回YES,代表在自己身上,那就继续遍历子控件,从后往前遍历子控件,重复前面两个步骤如果没有符合条件的子控件,那么自己就是最适合处理的控件,找到最适合接受的控件后,调用控件touchesBegan,touchesMoved,touchedEnded的方法。
- 通过
- 从后往前遍历子控件,重复前面的两个步骤欧
- 如果没有符合条件的子控件,那么就自己最适合处理。
UIView不接收触摸事件的三种情况:
- userInteractionEnabled = NO隐藏
- hidden = YES;
- 透明:alpha = 0.0 ~ 0.01;
具体的寻找,使用了两个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:
方法
为了寻找并返回最合适的view(能够响应时间的那个最合适的view)
以下是这个方法的实现逻辑
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {// 1.判断窗口能否接收事件if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;// 2.判断点在不在窗口上// 不在窗口上if ([self pointInside:point withEvent:event] == NO) return nil;// 3.从后往前遍历子控件数组int count = (int)self.subviews.count;for (int i = count - 1; i >= 0; i--) {// 获取子控件UIView *childView = self.subviews[i];// 坐标系的转换,把窗口上的点转换为子控件上的点// 把自己控件上的点转换成子控件上的点CGPoint childP = [self convertPoint:point toView:childView];UIView *fitView = [childView hitTest:childP withEvent:event];if (fitView) {// 如果能找到最合适的viewreturn fitView;}}// 4.没有找到更合适的view,也就是没有比自己更合适的viewreturn self;}
- 首先判断该控件是否能接受事件
- 在调用当前视图的
pointInside:withEvent:
方法判断触摸点是否在当前视图内 - 返回NO,则表示不在该视图内,直接返回nil
- 若返回YES,则向当前视图的所有子视图发送
hitTest:withEvent:
消息,所有子视图的遍历顺序是从最顶视图一直到最低层视图,即从subviews
数组的末尾向前遍历,直到有子视图返回非空对象,或者全部子视图遍历完毕。 - 若第一次有子视图返回非空对象,则
hitTest:withEvent:
返回此对象,处理结束。 - 若所有子视图都返回空,则
hitTest:withEvent:
返回自身
不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用
hitTest:withEvent:
方法如果
hitTest:withEvent:
方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
其实该方法的逻辑很简单,就是先判断自己能不能响应,是不是在自己身上,不是则直接返回。是的话继续往下递归判断子事件,直到返回nil,nil之前的父控件则是最合适的控件。
pointInside:withEvent
该方法就是判断点是不是在当前view上的。返回YES,代表点在方法调用者的坐标系上;返回NO,代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
解决以下问题,点击红色区域时,按钮并不会接受这个事件
//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{//将触摸点坐标转换到在CircleButton上的坐标CGPoint pointTemp = [self convertPoint:point toView:_CircleButton];//若触摸点在CricleButton上则返回YESif ([_CircleButton pointInside:pointTemp withEvent:event]) {return YES;}//否则返回默认的操作return [super pointInside:point withEvent:event];
}
响应链
事件响应流程:
- 如果找到最合适的控件来处理调用最合适的控件的touches…(touchesBegan、touchesMoved、touchedEnded)方法。
- 如果调用了[super touch…],就会将事件顺着响应者链往上传递,传给上一个响应者,接着上一个响应者就会调用touches…方法。
- 如果没有找到合适的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给 UIApplication。如果 UIApplication 不能处理事件,则将其丢弃。
响应者
在iOS中,不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件,称之为“响应者对象”。
UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。
UIResponder提供了我们平时最常用的touchesBegan/touchesMoved/touchesEnded方法。此外还有如下几个属性比较重要:
- isFirstResponder:判断该View是否为第一响应者。
- canBecomeFirstResponder:判断该View是否可以成为第一响应者。
- becomeFirstResponder:使该View成为第一响应者。
- resignFirstResponder:取消View的第一响应者。
这里我们需要区分一下第一响应者和最佳响应者
第一响应者和最佳响应者
-
第一响应者 (First Responder):
- 第一响应者是指当前能够响应某个事件的第一个对象。
- 通常情况下,当某个事件发生时,该事件首先被传递到第一响应者。
- 第一响应者通常是用户当前正在交互的视图,比如用户正在编辑的
UITextField
或者点击的UIButton
。
-
最佳响应者(Best Responder):
- 最佳响应者是指在响应者链上最适合处理某个事件的对象。
- 当第一响应者无法完全处理某个事件时,该事件会沿着响应者链向上传递,直到找到最佳响应者。
- 最佳响应者通常是能够最完整地处理该事件的对象,比如包含第一响应者的视图控制器。
一般来说,事件传递的目的就是为了让我们找到第一响应者。那么我们该如何判断是否为第一响应者呢?
- 能够响应触摸事件
- 触摸点在自己身上
- 没有任何子视图,或是所有子视图都不在触摸点上
补充知识:
对于UIWindow,若存在多个窗口,则优先询问后显示的窗口,这和控件的逻辑是一致的,优先询问子视图和后出现的控件
什么是上一个响应者?
如果当前这个view是控制器的view,那么控制器就是上一个响应者;
如果当前这个view不是控制器的view,那么父控件就是上一个响应者。
我们知道,响应者是从下往上传的
响应者对于事件的操作方式:
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者链条是什么
它是一种事件处理机制,由多个响应者对象连接起来的层次结构,使得事件可以沿着这些对象进行传递。利用响应者链条我们可以通过调用touches的super 方法,让多个响应者同时响应该事件。
如何做到多个对象处理同一个事件
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的
iOS中的各种事件
- 触摸事件
- 加速计事件
- 远程控制事件
与加速器,陀螺仪和磁力计相关的运动事件不遵循响应者连,Core Motion
将这些事件直接传递给你指定的对象
触摸事件(UITouch)
保存着跟手指相关的信息,比如触摸的位置、时间、阶段。 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。 当手指离开屏幕时,系统会销毁相应的UITouch对象。
UITouch的常用属性和方法
@property(nonatomic,readonly,retain) UIWindow *window;
//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIView *view;
//触摸产生时所处的视图
@property(nonatomic,readonly) NSUInteger tapCount;
//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSTimeInterval timestamp;
//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase;
//当前触摸事件所处的状态- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置,这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0));调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置。
- (CGPoint)previousLocationInView:(UIView *)view;
//该方法记录了前一个触摸点的位置。
UIEvent
UIEvent:称为事件对象,记录事件产生的时刻和类型。 每产生一个事件,就会产生一个UIEvent对象。 UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)。event绑定了touch对象
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件类型
@property(nonatomic,readonly) NSTimeInterval timestamp;
//事件产生的时间
触摸过程:
一次完整的触摸过程,会经历一下三个状态:
- 触摸开始:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- 触摸移动:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- 触摸结束:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
如果取消了这次触摸,还会有一个触摸取消方法:
- 触摸取消(可能会经历):
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
这四个触摸事件处理方法中,都有NSSet *touches
和UIEvent *event
两个参数。
用户用手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。一根手指对应一个UITouch对象。
-
一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数。
-
如果两根手指同时触摸一个view,那么view只会调用一次
touchesBegan:withEvent:
方法,touches参数中装着2个UITouch对象。 -
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次
touchesBegan:withEvent:
方法,并且每次调用时的touches参数中只包含一个UITouch对象。 -
根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸。
touches中存放的是UITouch对象,touch对象保存了触摸所属的window属性和view属性。同时event也绑定了touch对象。
UIGestureRecognizer(手势识别器)
手势识别器比UIResponder具有更高的事件响应优先级!!
UIGestureRecognizer能识别用户在某个view上面做的一些常见手势。该类是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势。
事实上,手势分为离散型手势(discrete gestures)
和持续型手势(continuous gesture)
。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)
和轻扫手势(UISwipeGestureRecognizer)
,其余均为持续型手势
。
UITapGestureRecognizer
(敲击) UIPinchGestureRecognizer
(捏合,用于缩放) UIPanGestureRecognizer
(拖拽)UISwipeGestureRecognizer
(轻扫) UIRotationGestureRecognizer
(旋转) UILongPressGestureRecognizer
(长按)
以下是UIGestureRecognizer定义
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {// 没有触摸事件发生,所有手势识别的默认状态UIGestureRecognizerStatePossible,// 一个手势已经开始但尚未改变或者完成时UIGestureRecognizerStateBegan,// 手势状态改变UIGestureRecognizerStateChanged,// 手势完成UIGestureRecognizerStateEnded,// 手势取消,恢复至Possible状态UIGestureRecognizerStateCancelled, // 手势失败,恢复至Possible状态UIGestureRecognizerStateFailed,// 识别到手势识别UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
手势识别器会抢占UITableView的cell的点击事件。
UITapGestureRecognizer 默认 cancelsTouchesInView = YES:识别成功后会调用 touchesCancelled,中断 TableView 内部的点击流程,didSelectRowAtIndexPath可能不触发或只高亮不选择。
总结
- 触摸发生时,系统内核生成触摸事件,先由IOKit处理封装成IOHIDEvent对象,通过IPC传递给系统进程SpringBoard,而后再传递给前台APP处理。
- 事件传递到APP内部时被封装成开发者可见的UIEvent对象,先经过hit-testing寻找第一响应者,而后由Window对象将事件传递给hit-tested view,并开始在响应链上的传递。
- UIRespnder、UIGestureRecognizer、UIControl,笼统地讲,事件响应优先级依次递增。