iOS首页投稿(暂停使用,暂停投稿)iOS Developer

聊聊响应者链条

2016-08-19  本文已影响275人  ZhengLi

前言

今天面试滴滴的时候聊到了自己以前遇到的一个坑,其中涉及了一些响应者链条的事。本身这个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]

上一篇 下一篇

猜你喜欢

热点阅读