响应链(Hit-Test 手势)

一、UIResponder和响应链的组成,如图一所示。

响应链的链接地址:
https://developer.apple.com/library/content/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html

1、如果initial View的上的事件没有响应,它会继续向它的superView传递响应时间,如果superView依旧没反应,依次向上寻找。

响应链 图一.png

2、事例,如图二所示。

发现btn.nextResponder.nextResponder.nextResponder 响应者为nil,原因是viewviewDidLoad中没有完全形成。

图二.png

view最终的形成在viewDidApear中,如图三所示,按钮的响应链如图三所示。
UIView->ViewController->UIWindow->UIApplication->AppDelegate

按钮的响应链 图三.png

二、Hit-Test找到view的流程

1、Hit-Test找View流程 如图四所示:

Hit-Test找View流程 图四.png

例子1:点击红色视图,查看hitTest流程。
代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self createViewHierachy];  
}
- (void)createViewHierachy {
    EOCLightGrayView *grayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
    EOCRedView *redView = [[EOCRedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
    EOCBlueView *blueView = [[EOCBlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
    EOCYellowView *yellowView = [[EOCYellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
    [self.view addSubview:grayView];
    [grayView addSubview:redView];
    [grayView addSubview:blueView];
    [self.view addSubview:yellowView];
}

重写颜色视图的方法

- (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 [super hitTest:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ touchBegan", self.EOCBgColorString);
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ touchesMoved", self.EOCBgColorString);
    [super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ touchesEnded", self.EOCBgColorString);
    [super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@ touchesCancelled", self.EOCBgColorString);
    [super touchesCancelled:touches withEvent:event];
}

界面如图五所示:


图五.png
结果如下:
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
redColorView touchBegan 0
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
redColorView touchesEnded 3
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]

分析:按照如图四的寻找结构,首先它会寻找黄色的视图,如果发现黄色的视图没有响应这个点击事件,会寻找同级下的灰色视图,如果发现触摸点在灰色视图范围内,则遍历它的子视图,以此类推。注:后添加的视图先寻找。
例子2:点击灰色视图,查看hitTest流程:

结果如下:
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
redColorView hitTest
redColorView pointInside
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]

如果发现灰色视图的pointInside返回为YES,它会继续寻找子视图。
例子3:点击黄色视图,查看hitTest流程:

结果如下
yellowColorView hitTest
yellowColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
yellowColorView touchBegan 
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
yellowColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]

如果黄色视图的pointInside返回为YES,它没有子视图,就停止寻找。

2、如果点击图五种的红色视图,让蓝色视图响应。该怎么做?

重写蓝色视图中的pointInside方法,返回YES,阻止他进行下一步的向兄弟视图的寻找,如图四所示查找视图的流程。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@ pointInside", self.EOCBgColorString);
    return YES;//[super pointInside:point withEvent:event];
}

打印结果如下所示:

yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
yellowColorView hitTest
yellowColorView pointInside
ligthGrayColorView hitTest
ligthGrayColorView pointInside
blueColorView hitTest
blueColorView pointInside
blueColorView touchBegan
ligthGrayColorView touchBegan
-[EOCTouchEventViewCtrl touchesBegan:withEvent:]
blueColorView touchesEnded
ligthGrayColorView touchesEnded
-[EOCTouchEventViewCtrl touchesEnded:withEvent:]

3、影响Hit-Test流程的因素:有alpha、hidden、userInteractionEnabled,当alpha的值小于等于0.01,Hit-Test就会受影响。

Hit-Test的真正逻辑:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@ hitTest", self.EOCBgColorString);
    if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
        return nil;
    }
    NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
    for (id view in subViews) {
        CGPoint convertPoint = [self convertPoint:point toView:view];
        if ([view pointInside:convertPoint withEvent:event]) {  
            return view;
        }
    }
    return self;  
}

三、使用Hit-Test的小案例

1、扩大按钮的响应范围。

如图六所示,1表示按钮,2表示按钮要扩大的范围。

扩大按钮范围 图六.png
- (void)viewDidLoad {
    [super viewDidLoad];
    CustomBtn * btn = [[CustomBtn alloc] initWithFrame:CGRectMake(100, 100, 10, 10)];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}
- (void)btnClicked{
    NSLog(@"%s",__func__);
}
@implementation CustomBtn
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGRect frame = [self getScaleFrame];
    //NSLog(@"frame = %@,point = %@",NSStringFromCGRect(frame),NSStringFromCGPoint(point));
    return CGRectContainsPoint(frame, point);
    //return YES;
}
- (CGRect)getScaleFrame{
    CGRect tmpFrame = CGRectMake(-15, -15, 40, 40);
    return tmpFrame;
}
@end

如果CustomBtn中的pointInside返回YES,那么点击视图的任何地方都会响应按钮的事件。

2、如果按钮添加到视图可见范围之外的地方,按钮超出父视图的部分无法响应点击事件。如图六所示:

- (void)viewDidLoad {
    [super viewDidLoad];
    RedView * redView = [[RedView alloc] initWithFrame:CGRectMake(200, 200, 100, 100)];
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];
    CustomBtn * btn = [[CustomBtn alloc] initWithFrame:CGRectMake(85, 85, 30, 30)];
    btn.backgroundColor = [UIColor blueColor];
    [btn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];
    [redView addSubview:btn];
}

蓝色按钮添加到红色视图上 图六.png

如何解决这个问题?
(一)重写红色视图的pointInside方法,返回YES,它会自动寻找子视图的响应事件。否则直接寻找兄弟视图的响应事件。
(二)重写hitTest方法。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%@ hitTest", self.EOCBgColorString);
    
    if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
        return nil;
    }
    NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
    for (id view in subViews) {
          //将point从self坐标系中的点转换成view坐标系中的点
          CGPoint convertPoint = [self convertPoint:point toView:view];
          if ([view pointInside:convertPoint withEvent:event]) {
               return view;
           }
        }
    return  self;
}

四、手势识别

1、手势的状态

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
    
    UIGestureRecognizerStateBegan,      // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
    UIGestureRecognizerStateChanged,    // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
    UIGestureRecognizerStateEnded,      // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
    UIGestureRecognizerStateCancelled,  // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
    
    UIGestureRecognizerStateFailed,     // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
    
    // Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};

手势的是从哪里看是识别的呢?

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    RedView * view = [[RedView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    view.backgroundColor = [UIColor redColor];
    view.userInteractionEnabled = YES;
    RedViewPanGesture * pan = [[RedViewPanGesture alloc] initWithTarget:self action:@selector(panEvent:)];
    [view addGestureRecognizer:pan];
    [self.view addSubview:view];
}
- (void)panEvent:(UIPanGestureRecognizer *)sender{
    NSLog(@"%s",__func__);
}
@end
@implementation RedViewPanGesture
- (instancetype)initWithTarget:(id)target action:(SEL)action
{
    if (self = [super initWithTarget:target action:action]) {
        self.delegate = self;
    }
    return self;
}
#pragma mark - GestureDelegate method
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    NSLog(@"%s",__func__);
    return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    NSLog(@"%s",__func__);
    return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    NSLog(@"%s",__func__);
    return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    //我被另外一个手势变成Fail
    NSLog(@"%s",__func__);
    return YES;
    
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    NSLog(@"%s",__func__);
    return YES;
}
@end

打印结果:

 -[RedViewPanGesture gestureRecognizer:shouldReceiveTouch:]
 -[RedViewPanGesture gestureRecognizer:shouldRequireFailureOfGestureRecognizer:]
 -[RedViewPanGesture gestureRecognizer:shouldRequireFailureOfGestureRecognizer:]
 -[RedViewPanGesture gestureRecognizerShouldBegin:]
 -[ViewController panEvent:]

结论:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
在手势开始的时候,就会调用手势的事件方法。

2、手势影响View的方法

2.1、delaysTouchesBegan

这是手势的一个属性,默认状态下这个属性为NO,表示当手势未被识别时,会调用视图的触摸方法(hit-test、touchMove、pointInside)的方法。如果这个手势的属性为YES,表示,手势识别,不会调用视图的触摸方法(hit-test、touchMove、pointInside)的方法,如果手势识别失败,则会调用视图的触摸方法。

2.2、cancelsTouchesInView

这个是手势的一个属性,默认状态为YES,当手势被识别的时候,会阻止视图的触摸事件。当为NO时,手势被识别时,不会阻止视图的触摸事件。
例:给按钮添加点击手势,当cancelsTouchesInView的方法为YES时,只会执行点击手势响应的方法,不会执行按钮的点击事件。当设置了该属性的方法为NO,按钮视图执行了按钮的点击事件,也执行了添加到按钮上的点击方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton * btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [btn setTitle:@"按钮" forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
    btn.backgroundColor = [UIColor redColor];
    UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapEvent)];
    tap.cancelsTouchesInView = NO;
    [btn addGestureRecognizer:tap];
    [self.view addSubview:btn];
}
- (void)tapEvent{
    NSLog(@"%s",__func__);
}
- (void)btnClicked{
    NSLog(@"%s",__func__);
}

2.3、delaysTouchesEnded

这是手势的一个属性,默认状态下这个属性为YES,当手势识别成功后,会将touchesCancelled消息发送给视图发送hit-test消息,手势识别失败后,会延迟0.15ms,期间没有收到别的touch才会发送touchesEnded,如果设置成NO,手势识别失败后会立马发送touchesEnded已借宿当前的触摸。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容