iOS Responder Chain 响应者链

一、事件分类

对于IOS设备用户来说,他们操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。对应的事件类型有以下三种:

1、触屏事件(Touch Event)

2、运动事件(Motion Event)

3、远端控制事件(Remote-Control Event)

今天以触屏事件(Touch Event)为例,来说明在Cocoa Touch框架中,事件的处理流程。首先不得不先介绍响应者链这个概念:

二、响应者链(Responder Chain)

先来说说响应者对象(Responder Object),有响应和处理事件能力的对象。

响应者链就是由一系列的响应者对象构成的一个层次结构。

UIResponder是所有响应对象的基类,在UIResponder类中定义 了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实 例都是可以构成响应者链的响应者对象。图一展示了响应者链的基本构成:

图一

从图一中可以看到,响应者链有以下特点:

1、响应者链通常是由视图(UIView)构成的;

2、一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(Super View);

3、视图控制器(如果有的话)的下一个响应者为其管理的视图的父视图;

4、单例的窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者

需要指出的是,Cocoa Touch应用不像Cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点;

5、单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

三、事件分发(Event Delivery)

第一响应者(First responder)指的是当前接受触摸的响应者对象(通常是一个UIView对象),即表示当前该对象正在与用户交互,它是响应者链的开端。整个响应者链和事件分发的使命都是找出第一响应者。

UIWindow对象以消息的形式将事件发送给第一响应者,使其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。

iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对 象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处 理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传 递给其处理的视图,这个过程称之为hit-test view。

UIWindow实例对象会首先在它的内容视图上调用 hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击 事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调 用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。
hitTest:withEvent:方法的处理流程如下:
首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
若返回NO,则hitTest:withEvent:返回nil;
若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

图二

加入用户点击了View E,下面结合图二介绍hit-test view的流程:

1、A是UIWindow的根视图,因此,UIWindwo对象会首相对A进行hit-test;

2、显然用户点击的范围是在A的范围内,因此,pointInside:withEvent:返回了YES,这时会继续检查A的子视图;

3、这时候会有两个分支,B和C:

点击的范围不再B内,因此B分支的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

点击的范围在C内,即C的pointInside:withEvent:返回YES;

4、这时候有D和E两个分支:

点击的范围不再D内,因此D的pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

点击的范围在E内,即E的pointInside:withEvent:返回
YES,由于E没有子视图(也可以理解成对E的子视图进行hit-test时返回了nil),因此,E的hitTest:withEvent:会将E返
回,再往回回溯,就是C的hitTest:withEvent:返回E--->>A的hitTest:withEvent:返回E。

至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。

不难看出,这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。

三、说明

1、如果最终hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;

2、hitTest:withEvent:方法将会忽略隐藏
(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于
0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds
属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的
pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写
pointInside:withEvent:方法来处理这种情况。

3、我们可以重写hitTest:withEvent:来达到某些特定的目的,下面的链接就是一个有趣的应用举例,当然实际应用中很少用到这些。

http://download.csdn.net/detail/wzzvictory_tjsd/5716299

我们常常利用UIView来写自己的UITouchEvent。例如在一个View/ViewController中直接实现以下3个方法:

-(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
{

}

我们用的非常多,但是大家知道这4个方法是谁的实例方法吗?如果你一下就说出是UIView的,那么为什么我们在UIViewController中也可以用呢,他们不是继承关系。

注意这4个实例方法来自UIView与UIViewController的共同父类:UIResponder。它是我们今天的主角。

基本上我们所能看到的所有图形界面都是继承自UIResponder的,So,它究竟为何方神圣?

UIResponder所谓很多视图的父类,他掌管着用户的操作事件分发大权。如果没有他,我们的电容屏如何将用户的操作传递给我们的视图令其做出反应呢?

我们先看看iOS中的响应者链的概念:

每一个应用有一个响应者链,我们的视图结构是一个N叉树(一个视图可以有多个子视 图,一个子视图同一时刻只有一个父视图),而每一个继承UIResponder的对象都可以在这个N叉树中扮演一个节点。当叶节点成为最高响应者的时候, 从这个叶节点开始往其父节点开始追朔出一条链,那么对于这一个叶节点来讲,这一条链就是当前的响应者链。响应者链将系统捕获到的UIEvent与 UITouch从叶节点开始层层向下分发,期间可以选择停止分发,也可以选择继续向下分发。

例子:

我用SingleView模板创建了一个新的工程,它的主Window上只有一个UIViewController,其View之上有一个Button。这个项目中所有UIResponder的子类所构成的N叉树为这样的结构:

那么他看起来并不像N叉树,但是不代表者不是一颗N叉树,当我们项目复杂之后,这个View可不可以有多个UIButton节点?所以他就是一棵树。

实际上我们要把这棵树写完整,应该还要算上UIButton的UILabel和UIImageView,因为他们也是UIReponder的子类。这里先不考虑了。

我们对UIButton来讲,他此时若是叶节点,那么这时我们针对他所在的响应链 来说,他在他之前的响应者就应该是我们controller的view(树中的叶节点比父节点永远更优先被分发事件,但是并不是说他就能在时间上先响应, 我们下面讲为什么)。所以我们尝试在任意地方打印这个Button的nextReponder对象。nextResponder对象是 UIReponder类的实例方法,它会返回任意对象在树中的上一个响应者实例:

NSLog(@"%@",_testButton.nextResponder);

控制台输出消息:

2013-09-21 03:40:25.989 响应链 [614:60b] <UIView: 0x16555e10; frame = (0 0; 320 568); autoresize = RM+BM; layer = <CALayer: 0x16555e70>>

我们可以根据这个UIView的尺寸来得知,他就是我们唯一的控制器中的那个UIView。

接下来我们再打印下这个UIView的下一个响应者是谁:

NSLog(@"%@",_testButton.nextResponder.nextResponder);

输出:

2013-09-21 03:45:03.914 响应链 [621:60b] <RSViewController: 0x15da0e30>

依次看,接着加一个nextResponder:

2013-09-21 03:50:49.428 响应链 [669:60b] (null)

为什么这里ViewController没有父亲呢?

注意这句代码我是写在ViewDidLoad中,而我们知道这个方法的生命周期比
较早,所以我们换个地方写或者延迟一段时间再打印,两种方法都可以得到结果(由此可以推理出我们响应者树的构造过程是在ViewDidLoad周期中来完
成的,这个函数会将当前实例的构成的响应者子树合并到我们整个根树中):

2013-09-21 03:53:47.304 响应链 [681:60b]
<UIWindow: 0x14e24200; frame = (0 0; 320 568); gestureRecognizers =
<NSArray: 0x14e242e0>; layer = <UIWindowLayer:
0x14e244a0>>

再继续往上追朔:

double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
         NSLog(@"%@",_testButton.nextResponder.nextResponder.nextResponder.nextResponder);
    });

2013-09-21 03:56:22.043 响应链 [690:60b] <UIApplication: 0x15659c00>

再加一个:

2013-09-21 03:56:51.186 响应链 [696:60b] <RSAppDelegate: 0x16663520>

那么我们的appDelegate还有没有父节点?

2013-09-21 03:57:22.588 响应链 [706:60b] (null)

没有了,注意,一个从叶节点开始分发的事件,最多也就只能分发到我们的AppDelegate了!

这个树形结构在我们的项目中尤为重要,举个栗子,如果我们想在一个view中重写UITouchEvent的4个方法,并且不影响他的父视图也响应这些事件,就要注意你重写的方式了,比如我们在ViewController中重写touchBegan如下:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"ViewController接收到触摸事件");
}

在appDelegate的中同样也写上这一段:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"appDelegate接收到触摸事件");
}

那么究竟是谁被触发呢?

2013-09-21 04:02:49.405 响应链 [743:60b] ViewController 接收到触摸事件

这个很好理解,我刚刚也说了,viewController明显是appDelegate的子节点,他有事件分发的优先权。如果我们想两个地方都触发呢?这里super一下就可以了:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"ViewController接收到触摸事件");
}

输出:

2013-09-21 04:07:26.206 响应链 [749:60b] appDelegate 接收到触摸事件

2013-09-21 04:07:26.208 响应链 [749:60b] ViewController 接收到触摸事件

注意看时间戳,appDelegate虽然优先级别不如ViewController,但是他响应的时间上面足足比ViewController早了0.002秒,我这里试了几次,都是相差0.002秒。

那么我们分析一下这里的响应者链是怎样工作的:

用户手指触摸到了UIView上,由于我们没有重写UIView的UITouchEvent,所以他里面和super执行的一样的,将该事件继续分发到UIViewController;

UIViewController的TouchBegan被我们重写了,如果我们 不super,那么我们在这里写响应代码。事件到这里就不继续分发了。可想而知,UIViewController祖先节 点:UIWindow,UIApplication,AppDelegate都无权被分发此事件。

如果我们super了TouchBegan,那么此次触摸事件由

ViewController分发给UIWindow,

UIWindow继而分发给UIApplication,

UIApplication再分发给AppDelegate,

于是我们在ViewController和appDelegate的touchBegan方法中都捕获到了这次事件。

到这里大家应该对这个响应者树有一个很好的理解了吧?

接下来我们再谈谈第一响应者,和UIButton上的事件分发。

时间: 2024-11-08 22:06:43

iOS Responder Chain 响应者链的相关文章

iOS - Responder Chain

 在iOS中,当发生事件响应时,必须知道由谁来响应事件.这就是由响应者链来对事件进行响应,所有事件响应的类都是UIResponder的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会.当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方.事件将沿着响应者链一直向下传递,直到被接受并作出处理. 一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件交由他处理,如果他不处理,事件就会被传递给它的视图控制器

IOS开发中响应者链

在IOS开发中,有时候会遇到如下情况:在页面1上有一个RedView,在RedView上有一个GreenView,在GreenView上有一个button,这些view的创建代码如下: 1.AppDelegate.m 1 // 2 // AppDelegate.m 3 // 响应者链 4 // 5 // Created by mac on 16/5/10. 6 // Copyright © 2016年 mzw. All rights reserved. 7 // 8 9 #import "App

IOS中的响应者链

响应者链就是当子视图不响应,父视图有响应事件,父视图响应 #import "RootViewController.h" #import "ResponderView.h" @interface RootViewController () @end @implementation RootViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after

iOS 的事件处理 响应者链

ios的事件处理时从硬件开始,由驱动传递给系统层面,再传递给应用程序本身(UIApplication),然后会根据响应链找到所谓的firstResponsder,如果它不 进行处理,然后就传递给响应链下一级响应者,直到回到UIApplication(如果响应链上没有响应),由UIApplication进行默认处理. 在代码可控区域内,ios的屏幕点击事件是从上到下(firstResponsder沿着响应联到window再到app本身)的,所以如果点击所在的点不在某个view的区域内,这个view

Responder一点也不神秘————iOS用户响应者链完全剖析

http://blog.csdn.net/mobanchengshuang/article/details/11858217?utm_source=tuicool&utm_medium=referral 这篇文章想跟大家分享的主旨是iOS捕获用户事件的各种情况,以及内部封装的一些特殊事件. 我们先从UIButton谈起,UIButton大家使用的太多了,他特殊的地方就在于其内置的普通Default/高亮Highlighted/选择Selected/可用Enable的几个状态(UIControlS

【IOS笔记】Event Delivery: The Responder Chain

Event Delivery: The Responder Chain  事件分发--响应链 When you design your app, it’s likely that you want to respond to events dynamically. For example, a touch can occur in many different objects onscreen, and you have to decide which object you want to re

View Controller Programming Guide for iOS---(八)---Using View Controllers in the Responder Chain

Using View Controllers in the Responder Chain 响应链中使用视图控制器 View controllers are descendants of the UIResponder class and are therefore capable of handling all sorts of events. When a view does not respond to a given event, it passes that event to its

ios中事件的响应链(Responder chain)和传递链

事件的响应链涉及到的一些概念 UIResponder类,是UIKIT中一个用于处理事件响应的基类.窗又上的所有事件触发,都由该类响应(即事件处理入又).所以,窗又上的View及控制器都是 派生于该类的,例如UIView.UIViewController等. 调用UIResponder类提供的方法或属性,我们就可以捕捉到窗又上的所有响应 事件,并进行处理. 响应者链条是由多个响应者对象连接起来的链条,其中响应者对象是能处理事 件的对象,所有的View和ViewController都是响应者对象,利

iOS开发——响应链(Responder Chain)的深入理解和代码示例

我在之前一篇博客<iOS响应者链Responder Chain浅析>中对iOS开发中遇到的响应者链概念有了基本的了解.但是仅仅停留在理解概念的基础上还是远远不够的.该博客我们会通过代码案例来深入理解响应链.该博客的示例上传至 https://github.com/chenyufeng1991/ResponderChain  . (1)首先来说说第一响应者(First Responder).响应事件的传递过程就是为了找到第一响应者.以下几个方法: isFirstResponder:判断该View