gitHub地址 : 响应链Demo
文章有点长,如果只是想了解大概过程的,可以直接看后面的总结
一.触摸、事件、响应者
1. UITouch
源起触摸
-
一个手指一次触摸屏幕,就对应生成一个
UITouch
对象。多个手指同时触摸屏幕,生成多个UITouch
对象。 -
多个手指先后触摸,系统会根据
触摸的位置
判断是否更新同一个UITouch对象
。若两个手指一前一后触摸同一个位置(即双击)
,那么第一次触摸
时生成一个UITouch对象
,第二次触摸
会更新这个UITouch对象
,这是该UITouch对象
的Tap Count
属性值从1
变成2
,若两个手指一前一后触摸的位置不同
,将会生成两个UITouch
对象,两者之间没有联系。 -
每个
UITouch
对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
// 触摸的各个阶段状态
// 例如当手指移动时,会更新phase属性到UITouchPhaseMoved;
// 手指离屏后,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
- 手指离开屏幕一段时间后,确定该
UITouch
对象不会再被更新,就释放。
2.UIEvent
事件的真身
-
触摸
的目的是生成触摸事件
供响应者响应,一个触摸事件
对应一个UIEvent
对象,其中的type
属性标识了事件的类型
,事件有如下几种类型:
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};
这里我们所说的事件具体指的是触摸事件。
-
UIEvent
对象中包含了触发该对象的触摸对象集合,因为一个触摸事件
可能是由多个手指同时触摸产生的。触摸对象
集合通过allTouches
属性获取。
3.UIResponder
UIResponder
是iOS
中用于处理用户事件
的API
,可以处理触摸事件、按压事件(3D touch)、远程控制事件、硬件运动事件
。可以通过touchesBegan、pressesBegan、motionBegan、remoteControlReceivedWithEvent
等方法,获取到对应的回调消息。UIResponder
不只用来接收事件
,还可以处理和传递对应的事件,如果当前响应者不能处理,则转发给其他合适的响应者
处理。
应用程序
通过响应者来接收和处理事件,响应者
可以是继承自UIResponder
的任何子类,例如UIView、UIViewController、UIApplication
等。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者
。
第一响应者
未处理的事件,将会在响应者链
中进行传递,传递规则由UIResponder
的nextResponder
决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者
没有接收消息,则顺着响应者链
向后传递。
二.寻找事件的第一响应者
App
接收到触摸事件
后,会被放入当前应用程序的UIApplication
维护的事件队列中。
由于事件一次只有一个,但是能够响应的事件的响应者
众多,所以这就存在一个寻找第一响应者
的过程。
1. 事件自下而上传递
查找第一响应者时,有两个非常关键的AP
I,查找第一响应者
就是通过不断调用子视图的这两个API
完成的。
调用方法,获取到被点击的视图,也就是第一响应者
。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
hitTest:withEvent:
方法内部会通过调用pointInside:
这个方法,来判断点击区域是否在视图上,是则返回YES
,不是则返回NO
。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
具体流程:
-
应用程序接收到触摸事件后,将事件放入
UIApplication
的事件队列,等到处理该事件时,将该事件出队列,UIApplication
将事件传递给窗口对象(UIWindow)
,如果存在多个窗口,则优先询问后显示的窗口 -
如果
窗口UIWindow
不能响应事件,则将事件传递给其他窗口
;若窗口能响应事件,则从后往前
询问窗口的子视图。 -
以此类推,如果
视图
不能响应事件
,则将事件
传递给同级的上一个子视图
;如果能响应,就从后往前
遍历当前视图
的子视图
。 -
如果
当前视图
的子视图
都不能响应事件,则当前视图
就是最合适
的响应者
。
举个🌰 :
如图所示:
image.png视图层级如下(同一层级的视图越在下面,表示越后添加):
image.png现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow
,然后自下而上
开始在子视图中寻找第一响应者
。事件传递的顺序如下所示:
-
UIWindow
将事件传递给UIViewController
的视图UIView
,UIView
判断自身能响应事件,将事件传递给子视图A
-
A
判断自身能响应该事件,继续将事件传递给C
(因为视图C
比视图B
后添加,因此优先传给C
)。 -
C
判断自身能响应事件,继续将事件传递给F
(同理F
比E
后添加)。 -
F
判断自身不能响应事件,C
又将事件传递给E
。 -
E
判断自身能响应事件,同时E
已经没有子视图,因此最终E
就是第一响应者
。
2. hitTest函数本质
上面讲到了事件在响应者之间传递的规则,视图通过判断自身能否响应事件来决定是否继续想子视图传递。
这里涉及到两个问题:
-
视图判断自身能否响应事件的判断依据是什么?
-
如果能响应,视图是如何将事件传递给子视图的?
针对第一个问题:
首先我们要知道,以下几种状态
的视图是无法响应事件
的:
-
不允许交互:
userInteractionEnabled = NO
-
隐藏:
hidden = YES
如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件 -
透明度:
alpha < 0.01
如果设置一个视图的透明度<0.01
,会直接影响子视图的透明度。alpha:0.0~0.01
为透明。
其次,如果当前视图可以响应事件,还必须通过pointInside
函数判断,触摸点是否在当前视图的坐标范围内,如果不在当前视图的坐标范围内,则无法响应,如果在坐标范围内,并且该视图可以响应事件,就进入下一步事件的传递。
针对第二个问题:
hitTest:withEvent:
方法返回一个UIView
对象,作为当前视图
层次中的响应者。默认实现是:
-
若
当前视图
无法响应事件,则返回nil
-
若
当前视图
可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图
层次中的事件响应者 -
若
当前视图
可以响应事件,同时有子视图
可以响应,则从后往前
遍历子视图,返回子视图层次中的事件响应者 -
以此类推,直到找到的
当前视图
可以响应事件,并且当前视图
没有子视图,那么当前视图
就是第一响应者
。
依据以上的描述我们可以推测出hitTest:WithEvent:
的默认实现大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
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)
{
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
我们分别在上述示例的视图层次中的每个视图实现文件添加如下方法:
#pragma mark -------------------------- Override Methods
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
然后单点E视图,打印如下:
-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[FView hitTest:withEvent:]
-[FView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[FView hitTest:withEvent:]
-[FView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[EView touchesBegan:withEvent:]
-[CView touchesBegan:withEvent:]
-[AView touchesBegan:withEvent:]
-[EView touchesEnded:withEvent:]
-[CView touchesEnded:withEvent:]
-[AView touchesEnded:withEvent:]
从打印结果我们可以看到最终EView视图
先对事件进行了响应,同时将事件
沿着响应链
进行传递。
以上打印结果我们会发现单机E视图后,从[AView hitTest:withEvent:]
到 [EView pointInside:withEvent:]
的过程会执行两遍,这个问题我查找了一些资料,但都没有好的答案,苹果那边的回复是这样的:
Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.
具体详见:https://lists.apple.com/archives/cocoa-dev/2014/Feb/msg00118.html
意思就是说hitTest
是一个没有副作用
的纯函数
,进行多次调用也不会对外产生影响,因此系统可以多次调整调用之间
被测试的点。
这里并没有给出具体的调用两次
的原因,你也可以理解为系统为了精确触摸的点
,而进行了多次调用,但为什么是两次
,我也没找到相关答案。
3.事件拦截
实际开发中我们经常会遇到如下需求
事件拦截.gif在Tabbar
的Item
上面添加提示视图tipView
,当点击提示视图tipview
,对应的Item
也进行响应,并且提示视图tipView
消失。
很明显,这里的提示视图tipView
是添加在Tabbar
上面的,但是提示视图tipView
的位置又超出了Tabbar
的区域,这时我们点击提示视图tipView
,会发现提示视图tipView
得不到响应。
我们看一下调用的堆栈
:
从堆栈中我们得出如下分析:
-
生成的触摸事件首先传到了
UIWindow
,然后UIWindow
将事件传递给控制器的根视图UILayoutContainerView
, -
UILayoutContainerView
判断自己可以响应触摸事件,然后将事件传递给子视图Tabbar
-
子视图Tabbar
判断触摸点并不在自己的坐标范围内,因此返回nil
, -
这时
UILayoutContainerView
将事件传递其他子视图UINavigationTransitionView,UINavigationTransitionView
判断自己可以响应事件,就将事件时间传递给其子视图UIViewControllerWrapperView
-
UIViewControllerWrapperView
判断自己可以响应事件,就将事件传递给子视图FJFFirstViewController控制器
的View
-
FJFFirstViewController
控制器的View
判断自己可以响应事件,然后就将事件传递给子视图AView
,AView
判断点击位置不在自己的坐标范围,返回nil
,所以FJFFirstViewController
控制器的View
就是第一响应者。
从这边的分析我们可以看出事件没有传递到提示视图tipView
,在Tabbar
这里就直接返回了,因为Tabbar
判断点击位置不在自己的坐标范围内。
因此我们需要做的就是修改Tabbar
的hitTest:withEvent:
函数里面判断点击位置是否在Tabbar
坐标范围的的判断条件,也就是需要重写TabBard
的 pointInside:withEvent:
方法,判断如果当前触摸坐标
在子视图tipView
上面,就返回YES
,否则返回NO
;这样一来时间就会最终传递到tipView
上面,最终事件就会由tipView
来响应。
代码如下:
#import "FJFTabbar.h"
@implementation FJFTabbar
//TabBar
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//将触摸点坐标转换到在CircleButton上的坐标
CGPoint pointTemp = [self convertPoint:point toView:self.indicateView];
//若触摸点在CricleButton上则返回YES
if ([self.indicateView pointInside:pointTemp withEvent:event]) {
return YES;
}
//否则返回默认的操作
return [super pointInside:point withEvent:event];
}
@end
三.事件的响应及传递
经过Hit-Testing
的过程后,UIApplication
已经知道了第一响应者
是谁,接下来要做的事情就是:
- 将事件传递给第一响应者
- 将事件沿着响应链传递
A. 将事件传递给第一响应者:
由于第一响应者具有处理事件的最高优先级,因此UIApplication
会先将事件传递给它供其处理。首先,UIApplication
将事件通过 sendEvent:
传递给事件所属的window
,window
同样通过 sendEvent:
再将事件传递给hit-tested view
,即第一响应者。过程如下:
UIApplication ——> UIWindow ——> hit-tested view
以点击EView视图
为例,在EView
的 touchesBegan:withEvent:
上断点查看调用栈
就能看清这一过程:
从这调用堆栈
我们可以看出,UIApplication
对于将事件传递给那个UIWindow
是很明确的,UIWindow
对于将事件传递给哪个视图也是很明确的。因为这些信息都放在了UIEvent
的Touch
事件里面。
但是这些信息又是什么时候放入到UIEvent
内部的呢?
可想而知因为Hit-Testing
和SendEvent
两者中的UIEvent
是同一个UIEvent
,所以这应该是在Hit-Testing
寻找第一响应者
的过程中,填入UIEvent
内部的。
B.将事件沿着响应链传递:
因为每个响应者
必定都是UIResponder
对象,通过4
个响应触摸事件的方法来响应事件。每个
UIResponder对象默认都已经实现了这
4个方法
,但是默认不对触摸事件
做任何处理,单纯只是将事件沿着响应链
传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。
第一响应者
接收到触摸事件
后,就具有对触摸事件
的处理权,它可以选择自己处理这个事件,也可以将这个事件沿着响应链传递给下一个响应者
,这个由响应者
之间构成的视图链
就称之为响应链
。
需要注意的是,上一节所说的
事件传递
的目的是为寻找事件的最佳响应者
,是自下而上
的传递;这里的事件传递目的是响应者
做出对事件的响应,这个过程是自上而下
的。前者为“寻找”,后者为“响应”。
响应者对于事件的操作方式:
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链
往下传递。
响应者
对于接收到的事件有3种
操作:
-
不拦截,默认操作
事件会自动沿着默认的响应链往下传递 -
拦截,不再往下分发事件
重写touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
-
拦截,继续往下分发事件
重写touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
响应链中的事件传递规则:
每一个响应者对象(UIResponder对象)
都有一个nextResponder
方法,用于获取响应链
中当前对象的下一个响应者
。因此,一旦事件的第一响应者
确定了,这个事件所处的响应链
就确定了。
对于响应者对象,默认的 nextResponder
实现如下:
-
UIView
若视图是控制器
的根视图
,则其nextResponder
为控制器对象;否则,其nextResponder
为父视图。 -
UIViewController
若控制器的视图是window
的根视图,则其nextResponder
为窗口对象;若控制器是从别的控制器present
出来的,则其nextResponder
为presenting view controller
。 -
UIWindow
nextResponder
为UIApplication
对象。 -
UIApplication
若当前应用的app delegate
是一个UIResponder
对象,且不是UIView、UIViewController
或app
本身,则UIApplication
的nextResponder
为app delegate
。
举个🌰:
事件响应示例.png如上图所示,响应者链
如下:
-
如果点击
UITextField
后其会成为第一响应者
。 -
如果
textField
未处理事件,则会将事件传递给下一级响应者链
,也就是其父视图
。 -
父视图
未处理事件则继续向下传递,也就是UIViewController
的View
。 -
如果控制器的
View
未处理事件,则会交给控制器处理。 -
控制器未处理则会交给
UIWindow
。 -
然后会交给
UIApplication
。 -
最后交给
UIApplicationDelegate
,如果其未处理则丢弃事件。
UITextField ——> UIView ——> UIView ——> UIViewController
——> UIWindow ——> UIApplication ——> UIApplicationDelegation
图中虚线箭头是指若该UIView
是作为UIViewController根视图
存在的,则其nextResponder
为UIViewController
对象;若是直接add
在UIWindow
上的,则其nextResponder
为UIWindow
对象。
可以用以下方式打印一个响应链中的每一个响应对象,在第一响应者
的 touchBegin:withEvent:
方法中调用即可(别忘了调用父类的方法)
- (void)printResponderChain {
UIResponder *responder = self;
printf("%s",[NSStringFromClass([responder class]) UTF8String]);
while (responder.nextResponder) {
responder = responder.nextResponder;
printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
}
}
以点击EView
为例,重写EView
的touch Begin:WithEvent
:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
[self printResponderChain];
[super touchesBegan:touches withEvent:event];
}
响应链如下:
EView --> CView --> AView --> UIView --> FJFFirstViewController -->
UIViewControllerWrapperView --> UINavigationTransitionView -->
UILayoutContainerView --> UINavigationController -->
UIViewControllerWrapperView --> UITransitionView -->
UILayoutContainerView --> FJFTabBarViewController --> FJFWindow -->
FJFApplication --> AppDelegate
另外如果有需要,完全可以重写响应者的 nextResponder
方法来自定义响应链。
四.UIGestureRecognizer、UIControl
上面我们讲述了UIResponder
响应触摸事件的过程,但除了UIResponder
之外,UIGestureRecognizer
、UIControl
同样具备对事件的处理能力。
以下将通过结合具体的示例来讲解UIGestureRecognizer
和UIControl
是如何处理触摸事件的。
举个例子:
image.png代码:
#pragma mark -------------------------- Life Circle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"分类";
// view tap
FJFTapView *tmpContainerView = [[FJFTapView alloc] initWithFrame:CGRectMake(50, 80, 260, 300)];
tmpContainerView.backgroundColor = [UIColor redColor];
FJFTapGestureRecognizer *tapGesture = [[FJFTapGestureRecognizer alloc] initWithTarget:self action:@selector(viewTap:)];
[tmpContainerView addGestureRecognizer:tapGesture];
[self.view addSubview:tmpContainerView];
// view longPress
FJFLongPressView *tmpLongPressView = [[FJFLongPressView alloc] initWithFrame:CGRectMake(50, 400, 260, 200)];
tmpLongPressView.backgroundColor = [UIColor grayColor];
FJFLongPressGestureRecognizer *longPressGesture = [[FJFLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(viewlongPress:)];
[tmpLongPressView addGestureRecognizer:longPressGesture];
[self.view addSubview:tmpLongPressView];
// button
FJFButton *tmpButton = [[FJFButton alloc] initWithFrame:CGRectMake(100, 50, 120, 80)];
tmpButton.backgroundColor = [UIColor greenColor];
[tmpButton setTitle:@"UIButton" forState:UIControlStateNormal];
[tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:tmpButton];
// imageControl
FJFImageControl *imageControl = [[FJFImageControl alloc] initWithFrame:CGRectMake(100, 150, 120, 80) title:@"imageControl" iconImageName:@"ic_red_box.png"];
imageControl.backgroundColor = [UIColor blueColor];
[imageControl addTarget:self action:@selector(imageControlTouch:) forControlEvents:UIControlEventTouchUpInside];
[tmpContainerView addSubview:imageControl];
}
#pragma mark -------------------------- Response Event
// tap
- (void)viewTap:(UITapGestureRecognizer *)tap {
NSLog(@"%s", __FUNCTION__);
}
// longPress
- (void)viewlongPress:(UILongPressGestureRecognizer *)longPress {
NSLog(@"%s", __FUNCTION__);
}
// buttonClicked
- (void)tmpButtonClicked:(UIButton *)sender {
NSLog(@"%s", __FUNCTION__);
}
// controlTouch
- (void)imageControlTouch:(FJFImageControl *)imageControl {
NSLog(@"%s", __FUNCTION__);
}
如代码所示:
-
FJFTapView
添加了继承自UITapGestureRecognizer
的FJFTapGestureRecognizer
单击手势 -
FJFLongPressView
添加了继承自UILongPressGestureRecognizer
的FJFLongPressGestureRecognizer
长按手势 -
UIButton
添加 点击事件 -
FJFImageControl
继承自UIControl
,也添加了点击事件,且UIButton
和FJFImageControl
都是FJFTapView
的子视图
。
观察各种情况的日志:
1.点击FJFTapView
:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
2.长按FJFLongPressView
:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
3.点击UIButton
:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
4.点击FJFImageControl
:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
接下来我们一一解释这些现象:
1. UIGestureRecognizer:
手势
分为离散型手势(discrete gestures
)和持续型手势(continuous gesture
)。系统提供的离散型手势
包括点按手势([UITapGestureRecognizer](apple-reference-documentation://hcmEtJ0eLp))
和轻扫手势([
UISwipeGestureRecognizer](apple-reference-documentation://hcKMJKvz5T))
,其余均为持续型手势
。
两者主要区别在于状态变化
过程:
- 离散型:
识别成功:Possible —> Recognized
识别失败:Possible —> Failed
- 持续型:
完整识别:Possible —> Began —> [Changed] —> Ended
不完整识别:Possible —> Began —> [Changed] —> Cancel
A. 离散型手势
从点击FJFTapView
的日志可以分析:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesCancelled:withEvent:]
-
UIWindow
在将事件传递给第一响应者FJFTapView
之前,先将事件传递给相关的手势识别器FJFTapGestureRecognizer
, -
若手势成功识别事件,就会取消
第一响应者FJFTapView
对事件的响
应; -
若手势没能识别事件,
第一响应者FJFTapView
就会接手事件的处理。
这里我们可以得出:
UIGestureRecognizer
比UIResponder
具有更高的事件响应的优先级
这个结论我们也可以从官方文档中得出:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties.
还有一点需要注意的是:
UIGestureRecognizer
对事件的响应也是通过touch
相关的4个方法来实现的,而这4个方法声明在UIGestureRecognizerSubclass.h
中。
而这里UIWindow
之所以知道要把事件传递给哪些手势识别器,主要还是通过UIEvent
里面的gestureRecognizers数组
来获取的,而数组
里面的手势识别器
是在Hit-Test View
寻找第一响应者
过程中填充的。
这里UIWindow
会取出UIEvent
里面的gestureRecognizers数组
的手势识别器,将事件传递给各个手势识别器
,如果有一个手势识别器
识别了事件,其他的手势识别器
就不会响应该事件
。
注意:这里取出
gestureRecognizers数组
的手势识别器
,没有按照特定的顺序,比如说从前往
后或是从后往前
,可以通过hook
掉UIGestureRecognizer
的touch
相关方法,去追踪得出。
因此我们可以分析日志:
-
UIWindow
先将事件传递给gestureRecognizers数组
里的手势识别器
,然后再传递给第一响应者FJFTapView
. -
因为
手势识别器
识别事件,需要一定时间,因此FJFTapView
先调用了touchesBegan
,这是因为FJFTapGestureRecognizer
成功识别了事件,UIApplication
就会取消FJFTapView
对事件的响应。
B. 持续型手势
从点击FJFLongPressView
日志分析:
[FJFLongPressGestureRecognizer touchesBegan:withEvent:]
[FJFLongPressView touchesBegan:withEvent:]
[FJFThreeViewController viewlongPress:]
[FJFLongPressView touchesCancelled:withEvent:]
[FJFLongPressGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewlongPress:]
从日志我们可以看出长按手势
回调了两次
,我们通过分析两次
调用的堆栈:
第一次调用堆栈:
第二次调用堆栈
我们可以看出第一次调用是在runloop
中通知监听的手势识别器的观察者,来通知长按手势识别器
对长按事件进行响应,此时手势识别器
的state
为UIGestureRecognizerStateBegan
。
第二次调用是UIWindow
先将事件传递给UIEvent
的gestureRecognizers
数组里的手势识别器
,然后长按手势识别器FJFLongPressGestureRecognizer
识别成功进行回调,此时手势识别器
的state
为UIGestureRecognizerStateEnded
。
这里的调用逻辑其实跟单击手势识别器FJFTapGestureRecognizer
相似,主要区别在于长按手势识别器FJFLongPressGestureRecognizer
调用了两次
。
C. 总结
当触摸发生
或者触摸的状态
发生变化时,UIWindow
都会传递事件寻求响应。
-UIWindow
先将触摸事件
传递给响应链上绑定的手势识别器
,再发送给触摸对象对应的第一响应者
。
-
手势识别器
识别手势期间,若触摸对象
的触摸状态发生变化,事件都是先发送给手势识别器
,再发送给第一响应者
。 -
手势识别器
如果成功识别手势,则通知UIApplication
取消第一响应者
对于事件的响应,并停止向第一响应者
发送事件。 -
如果
手势识别器
未能识别手势,而此时触摸并未结束,则停止向手势识别器
发送事件,仅向第一响应者
发送事件。 -
如果
手势识别器
未能识别手势,且此时触摸已经结束,则向第一响应者
发送end
状态的touch
事件,以停止对事件的响应。
D. 拓展
手势识别器的3
个属性:
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
a. cancelsTouchesInView:
默认为YES
。表示当手势识别器
成功识别了手势之后,会通知Application
取消响应链对事件的响应,并不再传递事件给第一响应者
。若设置成NO
,表示手势识别成功后不取消
响应链对事件的响应,事件依旧会传递给第一响应者
。
以点击FJFTapView
为例,将tapGesture.cancelsTouchesInView = NO;
输出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapView touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFTapView touchesEnded:withEvent:]
从日志我们可以看出,即便FJFTapGestureRecognizer
识别了点击手势,UIApplication
也依旧将事件发送给FJFTapView
.
b. delaysTouchesBegan:
默认为NO
。默认情况下手势识别器
在识别手势期间,当触摸状态发生改变时,Application
都会将事件传递给手势识别器
和第一响应者
;若设置成YES
,则表示手势识别器
在识别手势期间,截断事件,即不会将事件发送给第一响应者
。
以点击FJFTapView
为例,将tapGesture.delaysTouchesBegan = YES;
输出日志如下:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
从日志可以看出,手势识别器识别手势期间,事件不会传递给FJFTapView
,因此FJFTapView
的touchesBegan:withEvent:
不会被调用;而手势识别器
成功识别手势后,独吞了事件,不会再传递给FJFTapView
,因此只打印手势识别器
识别成功后手势的绑定函数
。
c. delaysTouchesEnded:
默认为YES
。当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)
再调用响应者的touchesEnded:withEvent:
;若设置成NO
,则在手势识别失败时会立即通知Application
发送状态为end
的touch
事件给第一响应者
以调用 touchesEnded:withEvent:
结束事件响应。
2.UIControl
UIControl
是系统提供的能够以target-action
模式处理触摸事件
的控件,iOS
中UIButton、UISegmentedControl、UISwitch
等控件都是UIControl
的子类。
值得注意的是,UIConotrol
是UIView
的子类,因此本身也具备UIResponder
应有的身份。
UIControl
作为控件类的基类,它是一个抽象基类
,我们不能直接使用UIControl
类来实例化控件,它只是为控件子类定义一些通用的接口,并提供一些基础实现,以在事件发生时,预处理这些消息并将它们发送到指定目标对象上。
关于UIControl
,此处介绍两点:
-
target-action
机制 - 触摸事件优先级
Target-Action机制
Target-action
是一种设计模式,直译过来就是”目标-行为”
。当我们通过代码为一个按钮添加一个点击事件时,通常是如下处理:
[button addTarget:self action:@selector(tapButton:) forControlEvents:UIControlEventTouchUpInside];
image.png
注:图片来源于官方文档Cocoa Application Competencies for iOS – Target Action
即当事件发生时,事件会被发送到控件对象中,然后再由这个控件对象去触发target对象
上的action行为
,来最终处理事件。因此,Target-Action机制
由两部分组成:即目标对象Target
和行为Selector
。目标对象
指定最终处理事件的对象,而行为Selector
则是处理事件的方法。
UIControl
作为能够响应事件的控件,必然也需要待事件交互符合条件时才去响应,因此也会跟踪事件发生的过程。不同于UIResponder
以及UIGestureRecognizer
通过touches
系列方法跟踪,UIControl
有其独特的跟踪方式:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
return YES;
}
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event {
NSLog(@"%s",__func__);
}
这4
个方法和UIResponder
的那4
个方法几乎吻合,只不过UIControl
只能接收单点触控
,因此接收的参数是单个UITouch
对象。这几个方法的职能也和UIResponder
一致,用来跟踪触摸的开始、滑动、结束、取消
。不过,UIControl
本身也是UIResponder
,因此同样有touches
系列的4
个方法。事实上,UIControl
的 Tracking
系列方法是在touch
系列方法内部调用的。比如 beginTrackingWithTouch
是在 touchesBegan
方法内部调用的, 因此它虽然也是UIResponder
,但touches
系列方法的默认实现和UIResponder
本类还是有区别的。
我们来分析下FJFButton
的日志输出以及调用堆栈
:
日志输出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFButton touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFButton touchesEnded:withEvent:]
[FJFThreeViewController tmpButtonClicked:]
调用堆栈:
从以上信息,我们可以分析:
-
UIWindow
首先将事件传递给响应链上绑定的手势识别器FJFTapGestureRecognizer
,再传递给第一响应者FJFButton
-
手势识别器FJFTapGestureRecognizer
和第一响应者FJFButton
分别调用touch相关方法对事件进行识别, -
最终
第一响应者FJFButton
对事件进行响应调用sendAction:to:forEvent:
将target、action
以及event
对象发送给UIApplication
,UIApplication
对象再通过sendAction:to:from:forEvent:
向target
发送action
。
通过这个结果,我们会疑问:UIControl比其父视图上的手势识别器具有更高的事件响应优先级?
接下来我们看下继承自UIControl
的FJFImageControl
的日志和调用堆栈:
日志输出:
[FJFTapGestureRecognizer touchesBegan:withEvent:]
[FJFImageControl touchesBegan:withEvent:]
[FJFTapGestureRecognizer touchesEnded:withEvent:]
[FJFThreeViewController viewTap:]
[FJFImageControl touchesCancelled:withEvent:]
调用堆栈:
image.png从以上信息,我们又可以得出::UIControl比其父视图上的手势识别器的优先级来的低?
经验证系统提供的有默认action
操作的UIControl
,例如UIbutton
、UISwitch
等的单击,UIControl
的响应优先级比手势识别器高,而对于自定义的UIControl
,响应的优先级比手势低。
至于为什么会这样,没找到具体原因,但测试的结果,推测系统应该是依据UITouch
的touchIdentifier
来进行区别处理。
Target-Action的管理:
UIControl
通过addTarget
方法和removeTarget
方法来添加和删除Target-Action
的操作。
// 添加
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
// 删除
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
如果想获取控件对象所有相关的target
对象,则可以调用allTargets
方法,该方法返回一个集合。集合中可能包含NSNull
对象,表示至少有一个nil
目标对象。
而如果想获取某个target
对象及事件相关的所有action
,则可以调用actionsForTarget:forControlEvent:
方法。
不过,这些都是UIControl
开放出来的接口。我们还是想要探究一下,UIControl
是如何去管理Target-Action
的呢?
实际上,我们在程序某个合适的位置打个断点来观察UIControl
的内部结构,可以看到这样的结果:
从图中我们可以看出,
UIControl
内部实际上是有一个可变数组(_targetActions
)来保存Target-Action
,数组中的每个元素是一个UIControlTargetAction
对象。UIControlTargetAction
类是一个私有类,内部维护
@interface UIControlTargetAction : NSObject {
SEL _action;
BOOL _cancelled;
unsigned int _eventMask;// 事件类型,比如:UIControlEventTouchUpInside
id _target;
}
这四个变量,UIControl
正是依据UIControlTargetAction
来对事件进行处理。
五.事件完整响应链
-
系统通过
IOKit.framework
来处理硬件操作,其中屏幕处理也通过IOKit
完成(IOKit
可能是注册监听了屏幕输出的端口)
当用户操作屏幕,IOKit
收到屏幕操作,会将这次操作封装为IOHIDEvent
对象。通过mach port(IPC进程间通信)
将事件转发给SpringBoard
来处理。 -
SpringBoard
是iOS
系统的桌面程序。SpringBoard
收到mach port
发过来的事件,唤醒main runloop
来处理。 -
main runloop
将事件交给source1
处理,source1
会调用__IOHIDEventSystemClientQueueCallback()
函数。
函数内部会判断,是否有程序在前台显示,如果有则通过mach port
将IOHIDEvent
事件转发给这个程序。
如果前台没有程序在显示,则表明SpringBoard
的桌面程序在前台显示,也就是用户在桌面进行了操作。
__IOHIDEventSystemClientQueueCallback()
函数会将事件交给source0
处理,source0
会调用__UIApplicationHandleEventQueue()
函数,函数内部会做具体的处理操作。 -
例如用户点击了某个应用程序的
icon
,会将这个程序启动。
应用程序接收到SpringBoard
传来的消息,会唤醒main runloop
并将这个消息交给source1
处理,source1
调用__IOHIDEventSystemClientQueueCallback()
函数,在函数内部会将事件交给source0
处理,并调用source0
的__UIApplicationHandleEventQueue()
函数。
在__UIApplicationHandleEventQueue()
函数中,会将传递过来的IOHIDEvent
转换为UIEvent
对象。 -
在函数内部,将事件放入
UIApplication
的事件队列,等到处理该事件
时,将该事件
出队列,UIApplication
将事件传递给窗口对象(UIWindow)
,如果存在多个窗口
,则从后往前
询问最上层显示的窗口 -
窗口UIWindow
通过hitTest
和pointInside
操作,判断是否可以响应事件,如果窗口UIWindow
不能响应事件,则将事件传递给其他窗口;若窗口
能响应事件,则从后往前询问窗口的子视图。 -
以此类推,如果
当前视图
不能响应事件,则将事件传递给同级
的上一个子视图
;如果能响应,就从后往前
遍历当前视图
的子视图
。 -
如果
当前视图
的子视图
都不能响应事件,则当前视图
就是第一响应者
。 -
找到
第一响应者
,事件的传递的响应链也就确定的。 -
如果
第一响应者
非UIControl
子类且响应链上也没有绑定手势识别器UIGestureRecognizer
; -
那么由于
第一响应者
具有处理事件的最高优先级,因此UIApplication
会先将事件传递给它供其处理。首先,UIApplication
将事件通过sendEvent:
传递给事件所属的window
,window
同样通过sendEvent:
再将事件传递给hit-tested view
,即第一响应者
,第一响应者
具有对事件的完全处理权,默认对事件不进行处理,传递给下一个响应者(nextResponder)
;如果响应链上的对象一直没有处理该事件,则最后会交给UIApplication
,如果UIApplication
实现代理,会交给UIApplicationDelegate
,如果UIApplicationDelegate
没处理,则该事件会被丢弃。 -
如果
第一响应者
非UIControl
子类但响应链上也绑定了手势识别器UIGestureRecognizer
; -
UIWindow
会将事件先发送给响应链上绑定的手势识别器UIGestureRecognizer
,再发送给第一响应者
,如果手势识别器
能成功识别事件,UIApplication
默认会向第一响应者
发送cancel响应事件
的命令;如果手势识别器
未能识别手势,而此时触摸
并未结束
,则停止向手势识别器
发送事件,仅向第一响应者
发送事件。如果手势识别器
未能识别手势,且此时触摸已经结束
,则向第一响应者
发送end
状态的touch
事件,以停止对事件的响应。 -
如果
第一响应者
是自定义的UIControl
的子类同时响应链上也绑定了
手势识别器UIGestureRecognizer;这种情况跟
第一响应者非
UIControl子类但响应链上也绑定了
手势识别器UIGestureRecognizer`处理逻辑一样;
-
如果
第一响应者
是UIControl
的子类且是系统类(UIButton、UISwitch)
同时响应链上也绑定了手势识别器UIGestureRecognizer
; -
UIWindow
会将事件先发送给响应链上绑定的手势识别器UIGestureRecognizer
,再发送给第一响应者
,如果第一响应者
能响应事件,UIControl
调用调用sendAction:to:forEvent:
将target、action
以及event
对象发送给UIApplication
,UIApplication
对象再通过sendAction:to:from:forEvent:
向target
发送action
。
网友评论