作为入门级的知识点,其实还有很多人搞不清楚怎么回事,举个例子:在一个父view加三个重叠的子视图(
UIView
),给每一个view添加一个tap事件,点击重叠区域,响应者毫无疑问是最上层的view,下面我们试两种情况:
1 设置最上层的view的userInteractionEnabled属性为NO,你会发现第二层的view响应了事件。
2 设置最上层的view的userInteractionEnabled属性为YES,去掉添加的tap事件,你会发现父view响应了事件。
通过这个现象我们切入今天的主题:响应者链的传递规则以及我们可以实施的阴谋。
引言
王老汉有两个儿子,他的大儿子是个光棍,二儿子又两个儿子,大致的关系图如下:
有一天,隔壁伍丽娟送来了一个肉包子,王老汉不舍得吃,于是问小王:“小王,你想吃吗?你不想吃我问问你哥。”,小王说:“我吃”,其实小王不是想自己吃,而是想给王大大和小明吃,于是问小明:“想吃吗?”,小明天生是个植物人,根本不鸟他爹,也从来都没鸟过,于是问王大大,王大大说:“我吃!”,突然王大大发现包子里有个小纸条,他彻底懵逼了,于是把这个有纸条的包子给了他爹也就是小王,小王也搞不定上面的暗语啊,于是又交给了他爹老王,老王微微一笑,出门去解决了这个包子的问题。
是不是觉得一派胡言,那就对了。
响应者链的模式
首先,来了解两个方法:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
这个方法调用- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
并返回响应事件的view。
比如如图格局,点击viewD,响应事件:
- UIWindow知道有点击事件之后,会首先调用自己的
hitTest:
方法,hitTest:
这个方法会调用会调用自身的pointInside:
方法,结果发现pointInside:
方法返回YES,说明点击区域在UIWindow内,然后UIWindow遍历他的子视图调用hitTest:
方法。- self.view调用自身的
hitTest:
方法,hitTest:
这个方法会调用会调用自身的pointInside:
方法,结果发现pointInside:
方法返回YES,从而确定点击在self.view的范围内,然后self.view遍历他的子视图调用hitTest:
方法。- ViewC调用自身的
hitTest:
方法,hitTest:
这个方法会调用会调用自身的pointInside:
方法,结果发现pointInside:
方法返回YES,从而确定点击在ViewC的范围内,然后ViewC遍历他的子视图调用hitTest:
方法。- ViewE调用自身的
hitTest:
方法,hitTest:
这个方法会调用会调用自身的pointInside:
方法,结果发现pointInside:
方法返回NO,从而确定点击不在ViewE的范围内,然后ViewE会在自己的hitTest:
方法中返回nil;下一步轮到ViewD调用自身的hitTest:
方法,hitTest:
这个方法会调用会调用自身的pointInside:
方法,结果发现pointInside:
方法返回YES,从而确定点击在ViewD的范围内,然后ViewD遍历他的子视图调用hitTest:
方法。- viewD无子视图所有遍历终止,在方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
中返回viewD,view一层层向superView传递,最终确定返回viewD,也就是viewD响应事件;
注意:如果某一层view的userInteractionEnabled设置为NO,那么方法- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
会直接返回nil,所以事件到这里也就终止了。
实现过程可以这么理解:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (self.userInteractionEnabled) {
if([self pointInside:point withEvent:event]){
for (UIView *view in self.subviews) {
UIView *hitTestView = [view hitTest:point withEvent:event];
if(!hitTestView){
return view;
}
}
}
}
return nil;
}
既然确定了点击的对象,那么下一步就是响应事件,也就是利用刚才的反向顺序,如果viewD不能响应这个事件,那么便向上找,直到nextResponder响应这个点击事件,如果都不响应,这个点击事件便流失了。
其实一句话,响应者链就是一个从下到上的定位过程以及从上到下的寻找过程,定位的是点击的view,寻找的是能够响应这个点击的view。
这样一个概念相信大家都有了,那么它有什么作用呢。要不然一堆理论没有任何卵用。
- 这里既然提到了nextResponder,其实刚才说的view是一个具象化的概念,因为UIViewController也继承自UIResponder,那么说一个通过view找到当前属于的VC的方法:
#import "UIView+Responder.h"
@implementation UIView (Responder)
-(UIViewController*)viewOnCurrentVC{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end
- 既然我们可以知道事件的传递过程,那么我们就能够截获这个事件,让我们看好的view去响应这个事件。比如:新手指导,镂空对应区域并响应镂空区域点击事件,点击新手指导非镂空区域提供事件接口。
//
// YSModalView.h
// LibrarysDemo
//
// Created by ys on 2017/4/26.
// Copyright © 2017年 ys. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface YSModalView : UIView
/*
*strokeColor 边框颜色
*canRespond 是否可以响应事件
*modalViewsArray 需要模态的视图数组
*modalRectsArray 需要模态的rect数组
*/
@property(nonatomic,strong)UIColor *strokeColor;
@property(nonatomic,assign)BOOL canRespond;
@property(nonatomic,copy)void(^tapModalBlock)();
@property(nonatomic,strong)NSArray *modalViewsArray;
@property(nonatomic,strong)NSArray<NSValue*> *modalRectsArray;
/** 创建模态 */
+(instancetype)YSModalViewOnSuperView:(UIView*)superView;
/** 修改属性后刷新模态 */
-(void)updateDisplay;
@end
//******************************************************
//******************************************************
//******************************************************
//
// YSModalView.m
// LibrarysDemo
//
// Created by ys on 2017/4/26.
// Copyright © 2017年 ys. All rights reserved.
//
#import "YSModalView.h"
@implementation YSModalView
/** 创建模态 */
+(instancetype)YSModalViewOnSuperView:(UIView*)superView{
if (!superView) {
return nil;
}
YSModalView *modalView = [[YSModalView alloc] initWithFrame:superView.bounds];
modalView.userInteractionEnabled = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:modalView action:@selector(tapModalView:)];
[modalView addGestureRecognizer:tap];
modalView.backgroundColor = [UIColor clearColor];
[superView addSubview:modalView];
return modalView;
}
-(void)tapModalView:(UITapGestureRecognizer*)tap{
if (self.tapModalBlock) self.tapModalBlock();
}
/** 修改属性后刷新模态 */
-(void)updateDisplay{
[self setNeedsDisplay];
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
if (self.canRespond) {
for (NSValue *viewValue in [self allRectArray]) {
CGRect viewRect = viewValue.CGRectValue;
viewRect = [self convertRect:viewRect toView:self];
if (CGRectContainsPoint(viewRect, point)) {
return NO;
}
}
}
return YES;
}
-(void)drawRect:(CGRect)rect{
UIBezierPath *backBezierPath = [UIBezierPath bezierPathWithRect:self.bounds];
backBezierPath.usesEvenOddFillRule = YES;
backBezierPath.lineWidth = 0;
//
UIBezierPath *strokePath = [UIBezierPath bezierPath];
strokePath.lineWidth = 2;
for (NSValue *viewValue in [self allRectArray]) {
//按钮镂空位置
CGRect viewRect = viewValue.CGRectValue;
viewRect = [self convertRect:viewRect toView:self];
UIBezierPath *viewPath = [UIBezierPath bezierPathWithRoundedRect:viewRect cornerRadius:5];
[backBezierPath appendPath:viewPath];
//虚线
UIBezierPath *oneStrokePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(viewRect.origin.x - 2, viewRect.origin.y - 2, viewRect.size.width + 4, viewRect.size.height + 4) cornerRadius:5];
[strokePath appendPath:oneStrokePath];
}
[[[UIColor blackColor] colorWithAlphaComponent:0.5] setFill];
[backBezierPath fill];
//
CGFloat dash[] = {5,3};
[strokePath setLineDash:dash count:2 phase:0];
[self.strokeColor setStroke];
[strokePath stroke];
}
//
-(NSArray<NSValue*>*)allRectArray{
NSMutableArray* allRectArray = [NSMutableArray arrayWithArray:self.modalRectsArray];
for (UIView* view in self.modalViewsArray) {
[allRectArray addObject:[NSValue valueWithCGRect:view.frame]];
}
return allRectArray;
}
@end
其实诸如此类的应用时机还有很多,基础的的东西会也许会给你意想不到的惊喜。
网友评论
if (self.userInteractionEnabled) {
if([self pointInside:point withEvent:event]){
for (UIView *view in self.subviews) {
UIView *hitTestView = [view hitTest:point withEvent:event];
if(!hitTestView){
return view;
}
}
}
}
return nil;
}
`if(!hitTestView)`
这个判断是不是写反了