关于hit-testing机制

本文详细介绍了iOS系统中触控事件的处理流程,包括UITouch和UIEvent对象的创建、事件传递过程、hitTest方法的工作原理及不同视图对触摸事件的响应方式。此外还提供了一些实用案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当我们点击屏幕上某个点的时候, IOS会检查到手指触摸操作(Touch),并生产一个UITouch对象,将其打包成一个UIEvent对象。然后将其放入当前活动的Application的 事件对列 , UIApplication会从 事件对列 中按照对列的顺序,通常先分发给应用程序主窗口,主窗口会调用hitTest:withEvent:方法(假设称为方法A,注意这是UIView的方法),查找合适的事件触发视图(这里通常称为“hit-test view”)。
看下面的图就一目了然
hit-testing

关于方法- hitTest withEvent:

1.以下三种情况,当前视图就不再接收消息了,直接返回 nil。
self.userInteractionEnabled = NO
self.hidden = YES
self.alpha <= 0.01

2.判断触摸点是否在该类范围内,若pointInside返回 YES就倒序遍历 subview,subView 调用自己的 hit 方法,一层subview一层subview 的找下去,若最终找到一个目标 view,该类的 hit 方法就返回这个 目标view,若没有这个 view,则返回 self。若pointInside返回 NO 返回 nil。

上代码:

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

首先MainView 上的子view包括 viewA,ViewB(ViewB1,ViewB2),ViewC,添加顺序也是依次添加。

由图和代码我们可以看出:遍历当前视图的所有子视图的顺序是从当前视图的subviews数组的末尾向前遍历(倒序),即先判断最后添加到当前视图上的子视图

下面是整个触摸的传递过程,传递到不同的 View 调用- hitTest withEvent:判断触摸点是否在当前视图范围内
1.主 window:-pointInside: withEvent:–>return YES,以此为根节点向下进行倒序遍历搜索
2.MainView :-pointInside: withEvent:–>return YES,以此为根节点向下进行倒序遍历搜索
3.ViewC:-pointInside: withEvent:–>return NO,不再向下进行搜索
4.ViewB :-pointInside: withEvent:–> YES,以此为根节点向下进行搜索
5.ViewB2 :–> NO,不再向下进行搜索
6.ViewB1 :–> YES,好了到这 ViewB2没有子视图(或者子视图的 hitView 都返回了nil),直接返回 self

至此,我们已经找到了 hitView,接下来把 hitView 向上传递给 UIApplication,UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给hit-testview,hitView会调用touchesBegan方法响应事件

事件的响应

UIResponder 的 touchesBegan 默认实现如下,将事件抛给nextResponder

//UIResponder
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [[self nextResponder] touchesBegan:touches withEvent:event];
}

UIView没有重写该方法,会传递给nextResponder;
UIControl重写了touchesBegan,未传递给nextResponder,所以会打断事件响应链,不再外下传递

//UIControl
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    _touchInside = YES;
    _tracking = [self beginTrackingWithTouch:touch withEvent:event];

    self.highlighted = YES;

    if (_tracking) {
        UIControlEvents currentEvents = UIControlEventTouchDown;

        if (touch.tapCount > 1) {
            currentEvents |= UIControlEventTouchDownRepeat;
        }

        [self _sendActionsForControlEvents:currentEvents withEvent:event];
    }
}

下面来看几种情况:

1.当前view,包括子 view 都不接收事件,将事件传递给兄弟viewA:当前view 重写- hitTest withEvent:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return nil;
}

2.当前view不接收事件,但是,子 view 要接收,将事件传递给兄弟viewA(即:点击当前 view 空白的地方,让和他重叠的兄弟 view 接收事件):当前view 重写- hitTest withEvent:

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

3.设置 scrollView 的分页宽度。现在有这么一个需求:有一个 VC,上面有一个parentView,parentView 上面有个 scrollView,scrollView 上显示三张图片,滑动一下,就会滚出来一张图片,看清楚一张,就是说 scrollView 的分页是1/3VC.View。
思路:
设置scrollView的宽度为1/3VC.View。//这样分页宽度就是自己的宽度。
设置scrollView的 center 横向居中。
设置 scrollView.contentoffset = 1/3VC.View。//这样就显示了3张图片
开启分页模式:scrollView.pagingEnabled = YES。
设置scrollView.clipsToBounds = NO。//这样超出范围的视图也会显示。
然后重写scrollView所在的parentView的hitTest事件,让其返回值是UIScrollView对象。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

UIResponder.m
UIControl.m
iOS事件处理,看我就够了~
iOS触摸事件全家桶

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值