iOS 事件传递流程

本文详细解析了iOS的事件处理机制,包括事件的传递过程、hitTest和pointInside方法的作用及其实现细节,并通过实例展示了如何实现事件透传以及如何阻止特定视图接收触摸事件。

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

参考文章
Event Handling Guide for iOS

iOS 的事件传递和 Android 的事件传递都是 “U”型传递。即分为两部分:
- 首先在视图的层次结构里找到能响应消息的那个视图,这个过程是由父视图到子视图遍历
- 然后在找到的视图里处理消息。这个流程由处理这个事件的子视图向父视图传递,但一般子视图处理完成后不再向父视图传递

这里写图片描述

处理原理如下

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

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

• 主窗口会调用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)

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

拿到这个UIView后,就调用该UIView的touches系列方法。

1.2、消息处理过程,在找到的那个视图里处理,处理完后根据需要,利用响应链nextResponder可将消息往下一个响应者传递。

UIAppliactionDelegate <- UIWindow <- UIViewController <- UIView <- UIView

【关键】:要理解的有三点:1、iOS判断哪个界面能接受消息是从View层级结构的父View向子View传递,即树状结构的根节点向叶子节点递归传递。2、hitTest和pointInside成对,且hitTest会调用pointInside。3、iOS的消息处理是,当消息被人处理后默认不再向父层传递。

在处理事件过程中,如果调用了 super 方法,则事件会向父视图传递,否则事件被当前视图处理后不再传递。这时需要注意:在touches 方法中如果有一个方法没有调用 super 的方法,其他 touces 方法都不应该再调用 super 方法,否则会造成事件处理流程紊乱。

在触摸事件传递过程中最好使用 super 的方式传递给父类,不推荐使用 self.nextResoponder 的方式传递

应用

事件透传

【需求】是:界面如下,

Window

  -ViewA

    -ButtonA

    -ViewB

      -ButtonB

层次结构:ViewB完全盖住了ButtonA,ButtonB在ViewB上,现在需要实现1)ButtonA和ButtonB都能响应消息 2)ViewA也能收到ViewB所收到的touches消息 3)不让ViewB(ButtonB)收到消息。

(首先解析下,默认情况下,点击了ButtonB的区域,iOS消息处理过程。

-ViewA

  -ButtonA

  -ViewB

    -ButtonB

当点击ButtonB区域后,处理过程:从ViewA开始依次调用hitTest

pointInside的值依次为:

ViewA:NO;

ViewB:YES;

ButtonB:YES;

ButtonB的subViews:NO;

所以ButtonB的subViews的hitTest都返回nil,于是返回的处理对象是ButtonB自己。接下去开始处理touches系列方法,这里是调用ButtonB绑定的方法。处理完后消息就停止,整个过程结束。)

【分析】:

实现的方式多种,这里将两个需求拆解开来实现,因为实现2就可以满足1。

2.1、需求1的实现,ViewB盖住了ButtonA,所以默认情况下ButtonA收不到消息,但是在消息机制里寻找消息响应是从父View开始,所以我们可以在ViewA的hitTest方法里做判断,若touch point是在ButtonA上,则将ButtonA作为消息处理对象返回。

代码如下:

#pragma mark - hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 当touch point是在_btn上,则hitTest返回_btn
    CGPoint btnPointInA = [_btn convertPoint:point fromView:self];
    if ([_btn pointInside:btnPointInA withEvent:event]) {
        return _btn;
    }

    // 否则,返回默认处理
    return [super hitTest:point withEvent:event];

}

这样,当触碰点是在ButtonA上时,则touch消息就被拦截在ViewA上,ViewB就收不到了。然后ButtonA就收到touch消息,会触发onClick方法。

2.2、需求2的实现,上面说到响应链,ViewB只要override掉touches系列的方法,然后在自己处理完后,将消息传递给下一个响应者(即父View即ViewA)。

代码如下:在ViewB代码里

#pragma mark - touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"B - touchesBeagan..");

    // 把事件传递下去给父View或包含他的ViewController
    [self.nextResponder touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"B - touchesCancelled..");
    // 把事件传递下去给父View或包含他的ViewController
    [self.nextResponder touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"B - touchesEnded..");
    // 把事件传递下去给父View或包含他的ViewController
    [self.nextResponder touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"B - touchesMoved..");
    // 把事件传递下去给父View或包含他的ViewController
    [self.nextResponder touchesBegan:touches withEvent:event];

}

然后,在ViewA上就可以接收到touches消息,在ViewA上写:

#pragma mark - touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A - touchesBeagan..");
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A - touchesCancelled..");
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A - touchesEnded..");
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"A - touchesMoved..");

}

这样就实现了向父View透传消息。

2.3 、不让ViewB收到消息,可以设置ViewB.UserInteractionEnable=NO;除了这样还可以override掉ViewB的ponitInside,原理参考上面。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // 本View不响应用户事件
    return NO;

}

视图停止响应触摸事件

如果要一个视图不响应事件有三种方法:
* userInteractionEnabled 设为 NO
* hidden 设为 YES
* alpha 设为 0 或 0 - 0.01

这三种方法各有弊端。首先 userInteractionEnabled 设为 NO 之后,此视图及其子视图都将不响应触摸事件;hidden 设为 YES
或者 alpha 设为 0(或 0 - 0.01)后此视图及其子视图都将不可见。

除了上面三个方法还可以通过重写父视图 hitTest 方法来实现或者重写这个视图的 pointInside 方法

3 判断是否点中某个 CALayer

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

{

    //get touch position

    CGPoint point = [[touches anyObject] locationInView:self.view];

    //get touched layer

    CALayer *layer = [self.layerView.layer hitTest:point];

    //get layer using hitTest

    if (layer == self.blueLayer) {

        [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"

                                    message:nil

                                   delegate:nil

                          cancelButtonTitle:@"OK"

                          otherButtonTitles:nil] show];

    } else if (layer == self.layerView.layer) {

        [[[UIAlertView alloc] initWithTitle:@"Inside White Layer"

                                    message:nil

                                   delegate:nil

                          cancelButtonTitle:@"OK"

                          otherButtonTitles:nil] show];

    }

}
寻找 View 的 Controller

ViewController 管理有一个 View,通过这个 View 可以方便的遍历其子视图,但是没有一个直接的方法来寻找一个 View 所在的 ViewController,下面就用事件传递的特点来寻找这个 ViewController

-(UIViewController*)parentController{
    UIResponder *responder = [self nextResponder];
    while (responder) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController*)responder;
        }
        responder = [responder nextResponder];
    }
    return nil;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值