iOS 探讨之 事件与响应者

本文深入探讨了iOS中用户与设备交互的触控事件处理机制,包括事件传递、响应者及响应者链条的概念,并详细解释了hitTest:withEvent:与pointInside:withEvent:方法的工作原理。

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

题外话
     苹果忙着发布新产品,我则忙着处理那些讨厌的数据...为了坚持最初的信念,在10月最后一天把自己以前总结的一篇文章拿出来给大家分享一下吧。


概述
用户与设备交互的话题再怎么探讨也不为过,这几天寻了几篇文章,写的都很不错,大致梳理一下,留个念想。

在iOS中,用户与设备的交互事件可以分为3类:
+ 触控事件
+ 传感器事件
+ 远程控制事件

本次将主要探讨触控事件中的事件传递、响应者、响应者链条。

图 1-1 用户与设备交互方式


探讨
1.  基础信息
     响应者 (Responsder Object) : 能够响应并且处理事件的对象,且继承UIResponder的对象称之为响应者对象,能够处理touchesBegan等触摸事件。
     第一响应者 (First Responder) : 第一个接收事件的View对象。
     响应者链条: 有很多响应者链接在一起组合起来的一个链条称之为响应者链条。

2.  事件传递
     鄙人愚见,事件传递可以认为是App寻找最佳执行此事件响应者的过程。

图 1-2 用户与设备交互事件传递


     用户与 iOS设备产生交互后,当前App的UIApplication管理事件队列会加入此事件来等待处理。待App准备处理此事件时,会从准备队列中移除这个事件,App会寻找最佳执行此事件的Target,故会将事件传递给KeyWindow(UIWindow),如果此时该ViewController没有加入GestureRecognizer 那么紧接着会传递给View(视图层次(父->子)),以寻找最佳执行Target。


图 1-3 触摸事件实例图


触摸事件举例:
点击橘色的View: UIApplication事件队列 -> UIWindow -> 蓝色 -> 橘色
点击绿色的View: UIApplication事件队列 -> UIWindow -> 蓝色 -> 白色 -> 绿色
点击红色的View: UIApplication事件队列 -> UIWindow -> 蓝色 -> 白色 -> 红色

传递详解:
KeyWindow会在它的内容视图上调用 hitTest:withEvent: (该方法返回的就是处理此触摸事件的最合适view)来完成这个找寻过程。
hitTest:withEvent: 在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。
然后再调用pintInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内)。
如果pointInside:withEvent:返回NO,那么hitTest:withEvent:也直接返回nil。
如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完成;若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;若所有子视图都返回nil,则hitTest:withEvent:方法返回该视图自身。

3. 探讨 hitTest:withEvent: 方法的底层实现
不接收触摸事件的三种情况
+ 不接收用户交互 userInterationEnabled = NO
+ 隐藏 hidden = YES
+ 透明 alpha = 0.0~0.01

底层实现
// point是该视图的坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判断自己能否接收触摸事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01return nil;
    // 2.判断触摸点在不在自己范围内
    if (![self pointInside:point withEvent:event]) return nil;
   // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
    NSInteger count = self.subviews.count;
   for (NSIntegeri = count - 1; i >= 0; i--) {
       UIView *childView = self.subviews[i];
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 没有找到比自己更合适的view
    return self;
}

4. 响应者链条
     每个能执行hitTest:withEvent:方法的View都属于事件传递的一部分,但是只有pointInside:withEvent:返回为YES的View才属于响应者链条。

处理原则
     响应者链条其实还包括 视图控制器(ViewController)、UIWindow、UIApplication。如下图:



图 1-4 响应链


     通过事件传递找到最合适的处理触摸事件的View后(就是最后一个pintInside:withEvent:返回YES的View,它是第一响应者),如果该view是控制器view,那么上一个响应者就是控制器。如果它不是控制器View,那么上一个响应者就是前面一个pointInside:withEvent:返回YES的view(父控件)。最后这些所有pointInside:withEvent:返回YES的view加上它们的控制器、UIWindow、UIApplication共同构成响应者链条。
     响应者链条是自上而下的(顶层->UIApplication),前面的事件传递是自下而上的(UIApplication->顶层)。

5. 响应者链条应用
可以让一个触摸事件让多个响应者同时处理该事件。如在上图中多个View中打印touchBegan:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchBegan---%@", [self class]);
    [super touchesBegan:touches withEvent:event];
}


总结
1. 知识扩展
     如果将某个view的pointInside:withEvent:方法直接返回NO(无论子控件的pointInside:withEvent:返回什么结果),影响的是子控件区域和自身区域的点击事件处理,这些区域不再响应事件。其余区域响应点击事件不发生变化。
     如果将某个view的pointInside:withEvent:方法直接返回YES,自身区域响应点击事件不变。其它改变:
     首先,父控件所有区域点击事件交给该view处理。
     然后,再看该view处于父控件的子控件数组中的位置。数组前面的兄弟控件的点击事件交给该view处理,数组后面的兄弟控件的点击事件由其兄弟控件处理。
     最后,该view的子控件原来能够自己处理点击的区域继续由子控件处理,子控件原先不能够自己处理点击的(超出view范围)区域可以由子控件处理了。
     所以,想要屏蔽掉某个view响应点击事件,如果其没有子控件或者子控件响应事件也想屏蔽掉,直接将该View的pointInside:withEvent:返回为NO就行了。而在一般情况下,不建议将view的pointInside:withEvent:返回YES.

2. 什么时候重写hitTest:withEvent:,什么时候重写pointInside:withEvent:,在哪个view内重写它们?
     很多情况下hitTest:withEvent:和pointInside:withEvent:方法任选其一都可以实现某个功能,比如在屏蔽中,pointInside:withEvent:返回NO就可以实现的话,都可以用hitTest:withEvent:返回nil代替。
     但是,hitTest:withEvent:更强大。一般pointInside:withEvent:在一般情况下起内部顶多只能根据情况判断怎么返回NO,屏蔽掉自己和子控件的事件响应。所以只要是想要保留子控件对触摸事件响应,屏蔽其父控件的响应,单独重写pointInside:withEvent:无法办到,必须重写hitTest:withEvent:方法。
     触摸事件原来该由某个view响应,现在你不想让它处理而让别的控件处理,那么就应该在该view内重写hitTest:withEvent:或pointInside:withEvent:方法。


参考文献
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值