聊聊响应者链条

作者: ZhengLi | 来源:发表于2016-08-19 21:48 被阅读275次

前言

今天面试滴滴的时候聊到了自己以前遇到的一个坑,其中涉及了一些响应者链条的事。本身这个bug就很有代表性(代表了自己对这块非常的不熟悉😃)。无独有偶,自己的好基友前几天腾讯电面一面时也遇到了这个问题。回来好好看了大神的博客。就顺手写个学习笔记自我总结下吧。

先聊聊响应链

UIResponder是所有可以响应事件的类的基类(从名字应该就可以看出来了),其中包括最常见的UIView和UIViewController甚至是UIApplication,所以我们的UIViewUIViewController都是作为响应事件的载体。

Apple🍎爸爸是这么说的:

The UIResponder class defines an interface for objects that respond to and handle events. It is the superclass of UIApplication, UIView and its subclasses (which include UIWindow). Instances of these classes are sometimes referred to as responder objects or, simply, responders.

先看看这个UIResponder的头文件:

#import <Foundation/Foundation.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIEvent.h>

NS_ASSUME_NONNULL_BEGIN

@class UIPress;
@class UIPressesEvent;

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject

- (nullable UIResponder*)nextResponder;

- (BOOL)canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;

- (BOOL)canResignFirstResponder;    // default is YES
- (BOOL)resignFirstResponder;

- (BOOL)isFirstResponder;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);

注:这里我删除了一些此文无关的定义

那么UIResponder和我们讲的相应者链条到底是什么关系呢?其实在iOS系统中,所谓响应者链条就是由这些UIResponder链接起来的。你可以想象成链表,链接他们的就是上面定义的属性nextResponder链接起来的。

响应者链条

Hit-Testing View

简单了解响应者链条是什么后,就要今天的主角登场了:Hit-Testing View

上节我们讲到了响应者链条,但是并没有说清楚它的工作流程。比如上图中的intial view是怎么寻找到的?系统正是通过一个叫做Hit-Test过程找到这个initial obje的。

Hit-Test的目的就是寻找目前手指点击到的那个最前的view,也可以理解为responder
这个过程对应的方法在UIView里:

- (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

当用户点击了手机屏幕时,UIApplication就会调用UIWindowhitTest: withEvent:方法。这个方法最终返回一个UiView。也就是我们要找到的那个最前的view。那这个方法具体流程是怎么样的呢?

我们拿下图说明下:


层次结构

UIWindow有一个MianVIew,MainView里面有三个subView:view A、view B、view C,他们各自有两个subView,他们层级关系是:view A在最下面,view B中间,view C最上(也就是addSubview的顺序,越晚add进去越在上面)。

现在假设手指点击在了绿色的View B上:
此时,UIApplication会调用UIWindowhitTest:WithEvent:方法,接着UIWindow调用上面的第二个方法:pointInside:withEvent:。现在手指点击的位置显然在MainViewbounds内,于是这个方法返回yes。紧接着,MainView开始遍历subviews。此处需要注意的是,这个方法会从index值较大的位置开始遍历,比如此处就会先找到VIew C上。因为它是最后一个被加入subviews数组里的。在调用了pointInside:withEvent:方法后,返回了NO。于是继续在MainViewsubview.index - 1的位置继续调用pointInside:withEvent:方法。如此循环,最终找到手指此时触摸的位置:View B.1

触摸发生时

完整流程:


完整流程

我们可以看到:判断当前这个view是不是hitView时,需要同时满足以下四个条件:

view.userInteractionEnable == YES
view.hidden == NO
view.alpha > 0.01f
[view pointInside:point withEvent:event] == YES

代码实现起来还是比较简单的😄:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (self.hidden || self.alpha <= 0.01f || [self pointInside:point withEvent:event] || !self.userInteractionEnabled) {
    return nil;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {//注意倒叙

    CGPoint newPoint = [subview convertPoint:point fromView:self];
    UIView *hitView = [subview hitTest:newPoint withEvent:event];

    if (hitView) {
        return hitView;
    }
}
return self;

}

Hit-Testing View应用

最常用的:扩大按钮热区

项目中经常有扩大某个按钮热区的需求,相信实际做过项目的童鞋都有过这样的经历。以前小弟的做法是把按钮宽高调大一点,但是这样也会导致按钮图片位移,还要改按钮的UIEdgeInsets等属性,很是蛋疼。有了Hit-Testing View,我们就可以这样写:😄


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect originRect = self.bounds;
    originRect.origin = CGPointMake(-15.0f, -10.0f);
    originRect.size = CGSizeMake(originRect.size.width + 30.0f, originRect.size.height + 10.0f + 10.0f);
    
    CGPoint touchPoint = [self convertPoint:point toView:self];
    if (CGRectContainsPoint(originRect, touchPoint)) {
        return self;
    }
    return [super hitTest:point withEvent:event];
}

事件的传递

有了响应者链条,事件的传递也就水到渠成了。在UIApplication调用了UIWIndowhitTest:withEevent:方法并反悔了一个hitView后,就会通过sendEvent:这个方法将事件传递给当前的hitView。如果当前的hitView处理不了该事件,就会将事件交由自己的nextResponder处理,如此递归。若最后交由UIApplication仍然处理不了该事件,系统就会抛弃该事件。

摘抄一段大神的博客:

如果view重写了touch方法,我们一般会看到的效果是,这个view响应了事件之后,它nextResponder不会收到这个事件,即使重写了nextRespondertouch方法。这个时候如果想事件继续传递下去,可以调用[super touchesBegan:touches withEvent:event],不建议直接调用[self.nextRespondertouchesBegan:touches withEvent:event]

相关文章

  • 聊聊响应者链条

    前言 今天面试滴滴的时候聊到了自己以前遇到的一个坑,其中涉及了一些响应者链条的事。本身这个bug就很有代表性(代表...

  • 响应者链

    1> 什么是响应者链 响应者链条是由多个响应者对象连接起来的链条,其中响应者对象是能处理事件的对象,所有的View...

  • 什么是响应者链

    响应者链条是由多个响应者对象连接起来的链条,其中响应者对象是能处理事件的对象,所有的View和ViewContro...

  • 响应者链条

    1.什么是事件响应链2.touch 事件分发3.hit-test 底层实现4.应用 1.什么是事件响应链 大多数事...

  • 响应者链条

    什么是事件? iOS中事件分为3大类 : 触摸事件, 加速计事件和远程控制事件.当你的手指在手机屏幕上触摸时, 产...

  • 响应者链条

    响应者链条:是由多个响应者对象连接起来的链条 作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象...

  • 响应者链条

    ** 响应者链条** 在iOS中不是任何对象都能处理时间,只有继承了UIResponder的对象才能接收并处理事件...

  • 响应者链条

  • 响应者链条

    简单来说就是 :一级一级的找到响应的视图,如果没有就传给UIWindow实例和UIApplication实例,要是...

  • 响应者链条

    所有控件的多点触摸默认是关闭的。需要使用多点触摸时要手动打开。 UIView的属性:保存是否打开多点触摸的属性 @...

网友评论

  • 之城之城:MianVIew应该改为MainVIew
  • Orangels:- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 的实现方法中
    if 的判断条件 应该是 ![self pointInside:point withEvent:event] 吧?
  • 七里小晴天:CGRect newRect ={self.origin,self.width+=10,self.height+=10};
    if (CGRectContainsPoint(newRect, point)) {
    return nil;
    }
    哥们,你这个地方写错了吧? 应该return self吧?
    ZhengLi:@七里小晴天 是的,已修复。谢谢啦~

本文标题:聊聊响应者链条

本文链接:https://www.haomeiwen.com/subject/dqobsttx.html