QiShare文章汇总iOS UI开发iOS面试知识点

UIView 中的hitTest方法

2019-05-09  本文已影响20人  QiShare

级别: ★★☆☆☆
标签:「iOS」「hitTest」「pointInside」
作者: dac_1033
审校: QiShare团队


1. 事件响应的过程

在iOS中的view之间逐层叠加,当点击了屏幕上的某个view时,这个点击动作会由硬件层传导到操作系统并生成一个事件(Event),这个事件顺着view的层级由下往上传导,直至找到包含有这个点击点、层级最高、且可与用户交互的view来响应这个事件。事件的传递过程官网有图解:


iOS中的事件响应链

2. 响应链中涉及的方法

// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   
 // default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  

- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
  1. 点击事件会在hitTest、pointInside两个方法配合的情形下,向下传递;
  2. hitTest:withEvent:在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。然后再调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内)。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil;
  3. 如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回该视图自身。
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (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:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

// 这几个方法比较常用,在此不再敖述;
// 当然,UIResponder中不止这三个响应事件的方法,本文仅以touches的这三个方法为例。
测试hitTest方法的执行过程

以下代码是在A视图中都重写我们需要观察的几个父类方法,BCDE中需要重写的代码以此类推:

/*
* 例如:A中重写父类方法的代码,
*/

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    NSLog(@"AView ---->> hitTest:withEvent: ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"AView <<--- hitTest:withEvent: --- /n hitTestView:%@", view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    
    NSLog(@"AView --->> pointInside:withEvent: ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"AView <<--- pointInside:withEvent: --- isInside:%d", isInside);
    return isInside;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"AView touchesBegan");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    
    NSLog(@"AView touchesMoved");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    
    NSLog(@"AView touchesEnded");
}

当点击了一下B视图所在区域时,Xcode输出log如下:

点击B视图,Xcode输出的log

在示例中可以发现响应链中所涉及方法的执行过程,有以下特点

  1. 当UIView中的isUserInteractionEnabled = NO、isHidden = YES、alpha <= 0.01时,hitTest方法不会被调用;
  2. UIResponder 中的touches三个方法都是发生在找到最终的响应事件的view之后;
  3. 二是寻找hit-test view的事件链传导了两遍,具体原因不明;

3. hitTest方法的应用

#import "BeyondBoundsOfView.h"

@interface BeyondBoundsOfView ()

@property (nonatomic, strong) UIButton *button;

@end

@implementation BeyondBoundsOfView

- (instancetype)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    if (self) {
        _button = [UIButton buttonWithType:UIButtonTypeSystem];
        [_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_button setTitle:@"UIButton" forState:UIControlStateNormal];
        [_button setBackgroundColor:[UIColor lightGrayColor]];
        _button.frame = CGRectMake(0, 0, 80, 80);
        [self addSubview:_button];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    CGSize size = self.frame.size;
    _button.center = CGPointMake(size.width / 2, 0);
}


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    
    for (UIView *subview in self.subviews) {
        CGPoint convertedPoint = [subview convertPoint:point fromView:self];
        UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
        if (hitTestView) {
            return hitTestView;
        }
    }
    return nil;
}

@end

上面代码中关键的一行:
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
获取到convertedPoint对我们循环调用子view的hitTest很关键。

工程源码GitHub地址


小编微信:可加并拉入《QiShare技术交流群》。

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
iOS 关于tabBar的几处笔记
算法小专栏:谈谈大O表示法
iOS UIWebView、WKWebView注入Cookie
Cookie简介
iOS 图标&启动图生成器(一)
算法小专栏:“D&C思想”与“快速排序”
奇舞周刊

上一篇下一篇

猜你喜欢

热点阅读