UIView的hitTest和pointInside方法

官方描述

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

Returns a Boolean value indicating whether the receiver contains the specified point.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

The view object that is the farthest descendent the current view and contains point. Returns nil if the point lies completely outside the receiver’s view hierarchy.

This method traverses the view hierarchy by calling the pointInside:withEvent: method of each subview to determine which subview should receive a touch event. If pointInside:withEvent: returns YES, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found. If a view does not contain the point, its branch of the view hierarchy is ignored. You rarely need to call this method yourself, but you might override it to hide touch events from subviews.

This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.

Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s clipsToBounds property is set to NO and the affected subview extends beyond the view’s bounds.

事件处理流程

1 当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中

2 UIApplication会从事件队列中取出最前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)

3 主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个最合适的UIView来处理触摸事件
(hitTest:withEvent:其实是UIView的一个方法,UIWindow继承自UIView,因此主窗口UIWindow也是属于视图的一种)

hitTest:withEvent:方法大致处理流程是这样的:

一 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内:

  • 若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil

  • 若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕:

  • 若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束
  • 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)

4 最终,这个触摸事件交给主窗口的hitTest:withEvent:方法返回的视图对象去处理

应用场景

1 扩大Button的点击区域(上下左右各增加20)

重写自定义Button的pointInside方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(CGRectInset(self.bounds, -20, -20), point)) {
        return YES;
    }
    return NO;
}
2 子view超出了父view的bounds响应事件

正常情况下,子View超出父View的bounds的那一部分是不会响应事件的
解决方法:重写父View的pointInside方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL flag = NO;
    for (UIView *view in self.subviews) {
        if (CGRectContainsPoint(view.frame, point)){
            flag = YES;
            break;
        }
    }
    return flag;
}
3 如果一个Button被一个View盖住了,在触摸View时,希望该Button能够响应事件

解决方法1
点击View及View的非交互子View(例如UIImageView),则该Button可以响应事件

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL next = YES;
    for (UIView *view in self.subviews) {
        if ([view isKindOfClass:[UIControl class]]) {
            if (CGRectContainsPoint(view.frame, point)){
                next = NO;
                break;
            }
        }
    }
    return !next;
}

解决方法2
点击View本身Button会响应该事件,点击View的任何一个子View,Button不会响应事件

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return nil;
    }
    return view;
}
4 特殊的UIScrollView

如图所示,该ScrollView可以显示上一页和下一页的部分界面,红色框是ScrollView的frame,绿色框部分是设置了clipsToBounds = NO的结果,但是正如情况2提到的,超出部分是不响应事件的。


UIView的hitTest和pointInside方法_第1张图片

简单的代码实现

    CGSize size = [UIScreen mainScreen].bounds.size;
    CGFloat width = size.width - 80;
    JCScrollView *scrollView = [[JCScrollView alloc] initWithFrame:CGRectMake(40, size.height - 150 - 30, width, 150)];
    scrollView.pagingEnabled = YES;
    scrollView.clipsToBounds = NO;
    scrollView.showsVerticalScrollIndicator = NO;
    scrollView.showsHorizontalScrollIndicator = NO;
    [scrollView setContentSize:CGSizeMake(width * 5, 150)];
    
    for (NSInteger i = 0; i < 5; i++) {
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(5 + width * i, 0, width - 10, 150)];
        view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(255) / 255.0f green:arc4random_uniform(255) / 255.0f blue:arc4random_uniform(255) / 255.0f alpha:1];
        [scrollView addSubview:view];
    }
    
    [self.view addSubview:scrollView];

如果需要绿色框响应ScrollView的滚动事件,则原理和情况1一样

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(CGRectInset(self.bounds, -40, 0), point)) {
        return YES;
    }
    return NO;
}

你可能感兴趣的:(UIView的hitTest和pointInside方法)