官方文档Understanding Event Handling, Responders, and the Responder Chain中,有这样的叙述:
app使用响应者对象接收和处理事件。响应者对象是UIResponder
类的实例,常见的子类包括UIView
,UIViewController
和UIApplication
。响应者接收原始事件数据,并且必须处理该事件或将其转发给另一个响应者对象。当你的app收到一个事件时,UIKit
会自动将该事件指向最合适的响应者对象,称为第一响应者(first responder)。
未被处理的事件会从响应者传递到活动的响应者链(active responder chain)中的响应者,响应者链是应用程序的响应者对象的动态配置(Unhandled events are passed from responder to responder in the active responder chain, which is a dynamic configuration of your app’s responder objects)
在你的app中没有单个响应者链。 UIKit
为对象如何从一个响应者传递到另一个响应者定义了默认的规则,但是你可以通过覆盖响应者对象中相应的属性来改变这些规则。
下图显示了一个应用程序中默认的响应者链,其接口包含label,text field,一个button和两个背景 view。如果text field不处理事件,则UIKit将该事件发送到文本字段的父级UIView
对象,然后是该window的根view。在根view中,响应者链在将事件引导到window之前转移到拥有该view的view controller。如果窗口不处理事件,则UIKit将事件传递给UIApplication
对象,如果该对象是UIResponder
的实例并且已经不是响应者链的一部分,则可能传递给app delegate。
决定事件的第一响应者
对于每种类型的事件,UIKit
都会指定一个第一响应者,并首先将该事件发送给该对象。 第一响应者根据事件的类型而变化。
注意
加速计、陀螺仪、磁力计相关的Motion事件,并不遵循响应者链。Core Motion会将直接分发这些事件到指定的对象。可参考Core Motion Framework
Controls使用action消息直接与其关联的目标对象进行通信。 当用户与控件进行交互时,控件调用其目标对象的动作方法 - 换句话说,它向其目标对象发送一个动作消息。 Action消息不是事件,但它们仍然可以利用响应者链。 当控件的目标对象为nil
时,UIKit从目标对象开始,遍历响应者链,直到找到实现相应操作方法的对象。 例如,UIKit编辑菜单使用此行为来搜索响应者对象,这些对象实现了名称为cut(_ :)
,copy(_ :)
或paste(_ :)
的方法
如果view附加有的gesture recognizer,则gesture recognizer在view接收之前接收touch和press事件。 如果view的所有的gesture recognizer都无法识别其手势,则将事件传递给view进行处理。 如果view不处理touches,则UIKit将事件传递给响应者链。 有关使用手势识别器处理事件的更多信息,请参阅处理UIKit手势。
确定哪个响应者包含触摸事件
UIKit
使用基于视图的hit-testing来确定触摸事件发生的位置。 具体来说,UIKit
将触摸位置与视图层次结构中视图对象的边界进行比较。 UIView
的hitTest(_:with :)
方法遍历视图层次结构,查找包含指定touch的最深的子视图。 该view成为触摸事件的第一响应者。
注意
如果触摸位置超出view边界,则hitTest(_:with :)
方法会忽略该view及其所有子view。 因此,当一个视图的clipsToBounds
属性为false
时,即使这些子视图碰巧包含触摸,也不会返回该view边界外的子视图。
UIKit
将每个触摸永久分配给包含它的视图。 UIKit
在触摸第一次出现时创建每个UITouch
对象,并且只有在触摸结束后才释放该触摸对象。 随着触摸位置或其他参数的更改,UIKit
会使用新信息更新UITouch
对象。 唯一不变的属性就是view(即使touch的位置移动到了原view的外面,touch的view 属性值也不会发生变更)。当touch结束时,UIKit
会释放掉这个UITouch
对象
改变响应者链
通过重写响应者对象的next属性,你可以修改响应者链。这样做后,下一个响应者就是你返回的对象
许多UIKit对象,已经重写了这个属性,返回了指定的对象,包括:
UIView
对象。如果view是view controller的root view,则下一个响应者就是view controller;否则,下一个响应者就是view的superviewUIWindow
对象。window的下一个响应者对象就是 UIApplication
对象UIApplication
对象。下一个响应者对象就是app代理,但只有app代理是UIResponder
的一个实例,而不是一个view、 view controller或app自身时,才是这样的iOS中事件的分类:
触摸事件调用层次如下:
查找第一响应者主要涉及以下两个方法:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
hitTest(_:with:)返回包含指定点receiver的视图层级(包括它本身)中的最深的后代。(Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.)
此方法通过遍历视图层级,调用每个子视图的point(inside:with:)
方法来确定哪个子视图应该接收触摸事件。如果point(inside:with:)
返回true
,那么类似的遍历子视图的视图层次,直到找到包含指定点的最前面的view。如果视图不包含该点,则该视图层级的分支将被忽略。
此方法忽略隐藏的视图对象、禁用了用户交互,或者alpha级别小于0.01的view
Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s clipsToBounds property is set to NO and the affected subview extends beyond the view’s bounds.
大意是说,即使clipsToBounds设置为NO,但如果subview超出了view的边际,它也不会响应hit
参考Hit-Testing in iOS,hitTest(_:with:)
内部实现可能是这样的
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
如下的view层级:
grayView
和yellowView
是控制器view的子view,而redView
和blueView
是grayView
的子view,如下:
重写每个自定义的view中的hitTest:withEvent:
和pointInside:withEvent:
方法,以及相关的touchesBegan:
等方法,如下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ pointInside", self.EOCBgColorString);
return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ hitTest", self.EOCBgColorString);
return [self hitTest:point event:event];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
EOCLog(@"%@ touchBegan", self.EOCBgColorString);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
EOCLog(@"%@ touchesMoved", self.EOCBgColorString);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
EOCLog(@"%@ touchesEnded", self.EOCBgColorString);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
EOCLog(@"%@ touchesCancelled", self.EOCBgColorString);
[super touchesCancelled:touches withEvent:event];
}
现在如果在redView
进行点击,方法的输出顺序如下:
可以看到系统输出了2次,顺序为:
yellowColorView->ligthGrayColorView->blueColorView->redColorView
可见,总是先找view层级中,最前端的子view,依次递归遍历,而且调用hitTest:withEvent:
在前,pointInside:withEvent:
在后
可以理解为:hitTest:withEvent:
会调用自身的pointInside:withEvent:
方法,如果pointInside:withEvent:
返回为YES
,则会继续遍历子view,如果pointInside:withEvent:
返回为NO
,则返回nil
。与上面介绍的hitTest(_:with:)
可能的内部实现是一致的
可以利用上面的调用过程,做出一些非常有用的功能,例如:
1.扩大Button的点击区域
自定义Button,重写- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
CGRect frame = [self getScaleFrame]; //扩大点击区域
return CGRectContainsPoint(frame, point);
}
另外,通过上面的输出可以看到touchesBegan:withEvent:
调用顺序为:
redColorView touchBegan
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
redColorView首先响应,然后再是它的父view,然后再是父view的superview,即控制器EOCTouchEventViewCtrl,与上面的hitTest的顺序就一样了,这其实就跟响应链相关了
官方问答对UIResponder的介绍如下:
Responder objects—that is, instances of UIResponder—constitute the event-handling backbone of a UIKit app. Many key objects are also responders, including the UIApplication object, UIViewController objects, and all UIView objects (which includes UIWindow). As events occur, UIKit dispatches them to your app’s responder objects for handling.
许多关键的对象都是responder,包括UIApplication、UIViewController和UIView
当event发生时,UIKit把它派发给app的响应对象来处理
There are several kinds of events, including touch events, motion events, remote-control events, and press events. To handle a specific type of event, a responder must override the corresponding methods. For example, to handle touch events, a responder implements the touchesBegan(:with:), touchesMoved(:with:), touchesEnded(:with:), and touchesCancelled(:with:) methods. In the case of touches, the responder uses the event information provided by UIKit to track changes to those touches and to update the app’s interface appropriately.
In addition to handling events, UIKit responders also manage the forwarding of unhandled events to other parts of your app. If a given responder does not handle an event, it forwards that event to the next event in the responder chain. UIKit manages the responder chain dynamically, using predefined rules to determine which object should be next to receive an event. For example, a view forwards events to its superview, and the root view of a hierarchy forwards events to its view controller.
可通过nextResponder
属性来返回下一个响应值对象,如下:
所以如果在redView
中,重写的touchesBegan:withEvent:
方法,不加上[super touchesBegan:touches withEvent:event];
,则事件不会向上面传递,相当于中断了响应者链
1.手势与hitTest的关系
如下的例子:
自定义一个拖动手势RedColorTapGesture
,重写touch相关方法
@interface RedColorTapGesture : UIPanGestureRecognizer<UIGestureRecognizerDelegate>
@end
@implementation RedColorTapGesture
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchBegan ");
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesMoved");
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesEnded");
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesCancelled");
[super touchesCancelled:touches withEvent:event];
}
@end
在redView上添加一个RedColorTapGesture
手势,拖动,观察控制台输出
2018-11-30 15:15:04.618614+0800 事件层次分析[42539:2949533] redColorView hitTest
2018-11-30 15:15:04.618790+0800 事件层次分析[42539:2949533] redColorView pointInside
2018-11-30 15:15:04.618989+0800 事件层次分析[42539:2949533] redColorView hitTest
2018-11-30 15:15:04.619083+0800 事件层次分析[42539:2949533] redColorView pointInside
2018-11-30 15:15:04.619487+0800 事件层次分析[42539:2949533] RedColorTapGesture touchBegan
2018-11-30 15:15:04.619863+0800 事件层次分析[42539:2949533] redColorView touchBegan
2018-11-30 15:15:04.741574+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:04.741806+0800 事件层次分析[42539:2949533] redColorView touchesMoved
2018-11-30 15:15:04.765998+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:04.766234+0800 事件层次分析[42539:2949533] redColorView touchesMoved
......
2018-11-30 15:15:04.922074+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:04.922265+0800 事件层次分析[42539:2949533] redColorView touchesCancelled
2018-11-30 15:15:04.943999+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:04.944203+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:04.944327+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:04.966473+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:04.966673+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:04.989167+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:04.989408+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:05.011762+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:05.012001+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:05.034189+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesMoved
2018-11-30 15:15:05.034418+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
2018-11-30 15:15:05.259231+0800 事件层次分析[42539:2949533] RedColorTapGesture touchesEnded
2018-11-30 15:15:05.259419+0800 事件层次分析[42539:2949533] -[EOCGestureEventViewCtrl panAction]
如果重写redView的hitTest:withEvent:
方法,直接返回nil
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@ hitTest", self.EOCBgColorString);
return nil;
}
此时拖动时,只有redView
2018-11-30 15:18:15.156214+0800 事件层次分析[42570:2963426] redColorView hitTest
2018-11-30 15:18:15.156555+0800 事件层次分析[42570:2963426] redColorView hitTest
2018-11-30 15:18:18.154568+0800 事件层次分析[42570:2963426] redColorView hitTest
2018-11-30 15:18:18.154864+0800 事件层次分析[42570:2963426] redColorView hitTest
所以:手势和pointInside以及hitTest:必须得先找到view,然后才能触发手势
如果,子view没有手势,而父view上添加了手势,那么子View可以响应父View上的手势
如下的例子,button是有颜色的view的子view,但是button在view的边界之外,此时点击button,并不会响应button的事件
可以这样重写-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法,来使button响应action事件:
重写有颜色view的-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view) {
return view;
}
for (UIView *subView in self.subviews) {
CGPoint cPoint = [self convertPoint:point toView:subView];
if ([subView hitTest:cPoint withEvent:event]) {
return subView;
}
}
return nil;
}
其它应用包括:
参考:
1.扩大响应的区域
2.将触摸事件传递给下面的视图
3.将触摸事件传递给子视图