iOS 触摸事件响应链

前言

本文讨论iOS事件中的触摸事件及其响应链,至于加速计事件和远程控制事件不在本文的讨论范畴。

本文讲解的问题:

  • 触摸事件进入app内部是如何传递的?(不包含系统响应阶段)
  • UIResponder响应者对象
  • 寻找最佳响应者

一、触摸事件进入app内部是如何传递的?

  1. 操作系统发来消息
  2. 唤醒app主线程runloop
  3. runloop的source1执行__IOHIDEventSystemClientQueueCallback()
  4. runloop的source0执行__UIApplicationHandleEventQueue()
  5. 将操作系统传来的IOHIDEvent封装为UIEvent
  6. source0触发UIApplication调用sendEvent:把UIEvent传递给UIWindow
  7. 接下来便是业务层查找最终响应者了(这一步后文会详细介绍)

一、UIResponder响应者对象

Apps receive and handle events using responder objects. A responder object is any instance of the UIResponder class, and common subclasses include UIView, UIViewController, and UIApplication.

正如官方文档说的,app内部通过UIResponder来接收和处理事件,包括UIResponder的派生类:UIView、UIViewController、UIApplication同样具有这种能力。

我们通过override它们的一系列方法可以实时的得到事件响应的回调:

触摸
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

现在我们大致了解了响应者扮演的角色。然而,在实际开发中,同时出现的响应者多不胜数,系统是如何找到那个最适合的响应者呢?

不急,在这之前,我们要做一些准备工作

首先,我们得了解UIResponder的nextResponder属性。顾名思义:下一个响应者。我翻译的官方说明:

  • UIView:若它是ViewController的跟view,那么它的nextResponder就是ViewController;否则,它的nextResponder就是它的superView
  • UIViewController:若它的view是某个window的跟视图,则其nextResponder为window;若它是从别的控制器present出来的,则其nextResponder为presenting view controller
  • UIWindow:它的nextResponder为UIApplication对象
  • UIApplication:若app delegate是一个UIResponder对象,且不是UIView、UIViewController、app本身,则UIApplication的nextResponder为app delegate

三、寻找最佳响应者

UIKit uses view-based hit-testing to determine where touch events occur. Specifically, UIKit compares the touch location to the bounds of view objects in the view hierarchy. The hitTest(_:with:) method of UIView walks the view hierarchy, looking for the deepest subview that contains the specified touch. That view becomes the first responder for the touch event.

简而言之,UIKit通过hit-testing机制确定最佳响应者,将从层级最深的子视图( the deepest subview)通过hitTest:withEvent:方法逐一查找,一旦有响应者满足触摸条件,该响应者将担此重任——处理touch event。

多看几遍,结合我的说明和官方说明,如果实在没理解到没关系,下面还会利用代码详细说明。

以下情况响应者拒绝响应事件, 1、anyobject.userInteractionEnabled = NO 2、anyobject.hidden = YES 3、anyobject.alpha < 0.01

注意:响应者拒绝响应事件,那么事件就不会向其lastResponder(对应nextResponder)传递

好了,接下来我们就来着重理解如何查找the deepest subview

我随便写了个demo,布局如下

层级关系:

     |——view1——view3
view0|
     |——view2

我在可视的view内部都重写了hitTest:withEvent:方法,以及touch的各个回调方法。提一句,在调用hitTest:withEvent:的时候,系统还会调用pointInside:withEvent判断是否包含。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"\n%@ 的lastResponder查找完毕->%@", NSStringFromClass([self class]), view?@"找到view":@"未找到view");
    return view;
}

看到了么,重写hitTest:withEvent:方法需要让super先执行一次,保证会按照系统默认的方式形成响应链,然后得到一个view,这个view就是符合条件的响应者。

哎,废话不多说,我们用实验证明,我们点击一下View3,打印如下(注意:由于系统问题该递归查找会走两次?这里删掉其中一次,以便理解): 打印信息——重点来了!!!!!

1、-[View0 hitTest:withEvent:]

2、-[View2 hitTest:withEvent:]

3、View2 的lastResponder查找完毕->未找到view

4、-[View1 hitTest:withEvent:]

5、-[View3 hitTest:withEvent:]

6、View3 的lastResponder查找完毕->找到view

7、View1 的lastResponder查找完毕->找到view

8、View0 的lastResponder查找完毕->找到view

9、-[View3 touchesBegan:withEvent:]

10、-[View3 touchesEnded:withEvent:]

解释:

  1. 点击事件传递到View0,开始走hitTest:withEvent:super调用hitTest:withEvent:继续查找
  2. View2和View1的nextRespondser都是View0,但是View2的层级比View1高(后添加),所以先走View2的hitTest:withEvent:(遵循the deepest subview原则)
  3. 由于点击point超出了View2的boundspointInside:WithEvent:方法返回了false(没打印出来,有兴趣自己试试),所以UIView *view = [super hitTest:point withEvent:event];这句方法返回了一个空。 注意:就算这里View2再来个子视图,同样不会走子视图的hitTest:withEvent:
  4. 传递到View1
  5. 传递到View3
  6. View3通过UIView *view = [super hitTest:point withEvent:event];找到了符合条件的View(这个view就是View3本身)
  7. 把找到的匹配响应者返回给View1
  8. 把找到的匹配响应者返回给View0
  9. 触摸事件触发
  10. 触摸事件结束

到这里,是不是对响应链有了一定的感知了呢?

值得注意的是,由于我们这里重写了View3的touch回调方法,以至于View1没有回调touch的对应方法。如果我们想让touch方法继续向nextRespondser传递,一是View3不去override对应的touch回调,但是若我们想要View3和View1都响应,那就让super调用一次就会继续传递了,比如触摸开始回调:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"\n%s", __func__);
    [super touchesBegan:touches withEvent:event];
}

四、关于Gestures、Responder、UIControl的响应优先级问题

(一)Gestures

我们直接给View3添加一个点击手势,添加手势响应的方法如下:

- (void)respondsToTapG {
    NSLog(@"\n%s", __func__);
}

然后我们点击View3,打印如下:

-[View0 hitTest:withEvent:]

-[View2 hitTest:withEvent:]

View2 的lastResponder查找完毕->未找到view

-[View1 hitTest:withEvent:]

-[View3 hitTest:withEvent:]

View3 的lastResponder查找完毕->找到view

View1 的lastResponder查找完毕->找到view

View0 的lastResponder查找完毕->找到view

-[View3 touchesBegan:withEvent:]

-[View1 touchesBegan:withEvent:]

-[View3 respondsToTapG]

-[View3 touchesCancelled:withEvent:]

我们注意到最后一行,当View3手势识别器成功识别的时候,View3走了touchesCancelled:withEvent:方法。由此可见,系统默认Gestures比Responder拥有更高的响应优先级。我们看到UIGestureRecognizer并非是UIResponder的子类,官方文档有两句话:

Gesture recognizers use the target-action design pattern to send notifications. Gesture recognizers are the simplest way to handle touch or press events in your views.

Gesture采用的是target-action设计模式,由此可以推断apple考虑到使用touch回调判断交互的各种不便,所以做了一个Gestures,让我们以很简单的方式来实现对复杂触摸事件的监听。

问题1:如果我们需要手势识别器的同时,又想要Respondser能够继续响应touch回调方法呢? 只需要修改UIGestureRecognizer的一个属性:

gestureObject.cancelsTouchesInView = NO

问题2:我们给View3的父视图View1同样添加一个手势,我们会发现就算让View3的手势识别器的cancelsTouchesInView = NO,并且让Respondser继续向nextRespondser传递,View1的手势识别器仍然不能工作,这是为何? 哈哈,聪明的你如果看懂了之前的东西就不难发现,Responder和Gesture根本就是两套响应机制,Responder是否能传递不关Gesture的事。如果要让手势向下传递,只需要实现手势识别器如下代理方法:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

额外说明:手势穿透的用途蛮多,UIScrollview实现了UIGestureRecognizerDelegate代理,所以我们想要重叠的scrollview同时响应拖动手势,只需要override上述方法即可。详细案例见我的另外一篇文章:

iOS解决方案:多个scrollview联动 http://www.jianshu.com/p/42479a0e8ac6

(二)UIControl

我们知道UIControl同样是UIRespondser的派生类,它同样使用了target-action设计模式,这里不对UIControl做具体介绍,就说两点(感兴趣自行实验):

  • 由于UIControlUIRespondser派生类,可想而知如果针对于同一个view,同时存在UIControlGesture的时候,UIControl优先级会低于Gesture
  • UIControl会阻止父视图上的手势识别器行为,而UIRespondser不会阻止父视图上的手势识别器行为

尾声

本文重点讲解响应链相关问题,没有对其他技术点做过多介绍,如不理解多翻翻资料,见谅。

好久没有写长文了,各位大佬如果发现纰漏或者错误欢迎指出,如果有什么不理解的地方也欢迎留言探讨O(∩_∩)O哈哈!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏应兆康的专栏

Python使用本地shadowsocks代理

2141
来自专栏我爱编程

猫眼Top100

1624
来自专栏wOw的Android小站

[Android][Framework] 无障碍快捷方式相关代码

问题:无障碍快捷方式(Accessibility Shortcut)打开不生效。

1911
来自专栏程序员宝库

为什么浏览器的用户代理字符串以 Mozilla 开头?

为什么浏览器的用户代理字符串(user-agent string)以 Mozilla 开头? ? 最早流行的浏览器是 NCSA Mosaic,它称自己为 NCS...

3448
来自专栏有趣的Python

一键评教,查询成绩,批量免验证码选课,退课,-云大urp教务系统大作战(3)

这一小节没有什么好讲的,如果你学会了上一小节的中心思想: 从真实世界看程序世界 那么我们可以触类旁通的继续通过f12控制台对于真实的查询成绩,加课,退课,教...

2474
来自专栏技术碎碎念

数据的分页处理

当页面中要显示的内容过多需要分多页显示、或是数据量过大内存吃不消时,需要分页处理。 原理:每次从数据库中取出一定量的数据,通过jsp页面显示 实现: ①写一个类...

2645
来自专栏施炯的IoT开发专栏

Windows Mobile 6.5.3 Developer Tool Kit

    目前,微软正在花大量的人力物力财力做Windows Phone 7,而且据说以后会给出硬件平台配置需求,目前的Windows Mobile 6.x系列机...

1925
来自专栏SeanCheney的专栏

Python模拟登陆 —— 征服验证码 10 知乎(倒立文字验证码)

知乎的倒立文字验证码 # 登录知乎,通过保存验证图片方式 import urllib.request import urllib.parse import ti...

34611
来自专栏iOSer成长记录

iOS-关于Cell上Button点击效果

1543
来自专栏雪胖纸的玩蛇日常

django 分页功能

1815

扫码关注云+社区