事件传递及响应详解

一.UIResponder

   1.UIResponder简介

一个UIResponder类为那些需要响应并处理事件的对象定义了一组接口。

这些事件主要分为两类:触摸事件(touch events)和运动事件(motion events)。

UIResponder类为这两类事件都定义了一组接口,这个我们将在下面详细描述。

在UIKit中,UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类。另外SpriteKit中的SKNode也是继承自UIResponder类。因此UIKit中的视图、控件、视图控制器,以及我们自定义的视图及视图控制器都有响应事件的能力。这些对象通常被称为响应对象,或者是响应者(以下我们统一使用响应者)。



   2.管理响应链

UIResponder提供了几个方法来管理响应链,包括让响应对象成为第一响应者、放弃第一响应者、检测是否是第一响应者以及传递事件到下一响应者的方法,我们分别来介绍一下。

上面提到在响应链中负责传递事件的方法是nextResponder,其声明如下:

- (UIResponder *)nextResponder

UIResponder类并不自动保存或设置下一个响应者,该方法的默认实现是返回nil。子类的实现必须重写这个方法来设置下一响应者。UIView的实现是返回管理它的UIViewController对象(如果它有)或者其父视图。而UIViewController的实现是返回它的视图的父视图;UIWindow的实现是返回UIApplication对象;而UIApplication的实现是返回nil。所以,响应链是在构建视图层次结构时生成的。

事件顺着响应传递顺序:UIView控件 -> 父视图 ->  UIViewController  ->  UIWindow - >  UIApplication -> nil

响应过程


一个响应对象可以成为第一响应者,也可以放弃第一响应者。为此,UIResponder提供了一系列方法,我们分别来介绍一下。

如果想判定一个响应对象是否是第一响应者,则可以使用以下方法:

- (BOOL)isFirstResponder

如果我们希望将一个响应对象作为第一响应者,则可以使用以下方法:

- (BOOL)becomeFirstResponder

如果对象成为第一响应者,则返回YES;否则返回NO。默认实现是返回YES。子类可以重写这个方法来更新状态,或者来执行一些其它的行为。

上面提到一个响应对象成为第一响应者的一个前提是它可以成为第一响应者,我们可以使用canBecomeFirstResponder方法来检测,

- (BOOL)canBecomeFirstResponder

与上面两个方法相对应的是响应者放弃第一响应者的方法,其定义如下:

- (BOOL)resignFirstResponder

- (BOOL)canResignFirstResponder

resignFirstResponder默认也是返回YES。需要注意的是,如果子类要重写这个方法,则在我们的代码中必须调用super的实现。

canResignFirstResponder默认也是返回YES。不过有些情况下可能需要返回NO,如一个输入框在输入过程中可能需要让这个方法返回NO,以确保在编辑过程中能始终保证是第一响应者。


      

      3.管理输入视图

         所谓的输入视图,是指当对象为第一响应者时,显示另外一个视图用来处理当前对象的信息输入,如UITextView和UITextField两个对象,在其成为第一响应者是,会显示一个系统键盘,用来输入信息。这个系统键盘就是输入视图。输入视图有两种,一个是inputView,另一个是inputAccessoryView。这两者如图3所示:

与inputView相关的属性有如下两个,

@property(nonatomic, readonly, retain) UIView *inputView

@property(nonatomic, readonly, retain) UIInputViewController *inputViewController

这两个属性提供一个视图(或视图控制器)用于替代为UITextField和UITextView弹出的系统键盘。我们可以在子类中将这两个属性重新定义为读写属性来设置这个属性。如果我们需要自己写一个键盘的,如为输入框定义一个用于输入身份证的键盘(只包含0-9和X),则可以使用这两个属性来获取这个键盘。

与inputView类似,inputAccessoryView也有两个相关的属性:

@property(nonatomic, readonly, retain) UIView *inputAccessoryView

@property(nonatomic, readonly, retain) UIInputViewController *inputAccessoryViewController

设置方法与前面相同,都是在子类中重新定义为可读写属性,以设置这个属性。

另外,UIResponder还提供了以下方法,在对象是第一响应者时更新输入和访问视图,

- (void)reloadInputViews

调用这个方法时,视图会立即被替换,即不会有动画之类的过渡。如果当前对象不是第一响应者,则该方法是无效的。



       4.验证命令

在我们的应用中,经常会处理各种菜单命令,如文本输入框的”复制”、”粘贴”等。UIResponder为此提供了两个方法来支持此类操作。首先使用以下方法可以启动或禁用指定的命令:

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender

该方法默认返回YES,我们的类可以通过某种途径处理这个命令,包括类本身或者其下一个响应者。子类可以重写这个方法来开启菜单命令。例如,如果我们希望菜单支持”Copy”而不支持”Paser”,则在我们的子类中实现该方法。需要注意的是,即使在子类中禁用某个命令,在响应链上的其它响应者也可能会处理这些命令。

另外,我们可以使用以下方法来获取可以响应某一行为的接收者:

- (id)targetForAction:(SEL)action withSender:(id)sender

在对象需要调用一个action操作时调用该方法。默认的实现是调用canPerformAction:withSender:方法来确定对象是否可以调用action操作。如果可以,则返回对象本身,否则将请求传递到响应链上。如果我们想要重写目标的选择方式,则应该重写这个方法。下面这段代码演示了一个文本输入域禁用拷贝/粘贴操作:

- (id)targetForAction:(SEL)action withSender:(id)sender{   

               UIMenuController *menuController = [UIMenuController sharedMenuController];

                if (action == @selector(selectAll:) || action == @selector(paste:) ||action == @selector(copy:) || action==@selector(cut:)){

                      if (menuController){

                               [UIMenuController sharedMenuController].menuVisible = NO;

                     }

                   return nil;

               }

            return [super targetForAction:action withSender:sender];

}



二.iOS中的事件的产生和传递过程


1.事件的产生

        发生触摸事件(以触摸事件为例)后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?因为队列的特定是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。

主窗口会在视图层次结构中找到一个最合适的视图(什么样的视图才算最合适的视图???)来处理触摸事件,这也是整个事件处理过程的第一步。

找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。


2.事件的传递

事件传递过程: 触摸事件的传递是从父控件传递到子控件,也就是: 产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件


前面一直在说最合适的视图来处理触摸事件,那么什么样的视图才算最合适的视图???按照如下步骤找到的就是最合适的视图:

1.首先判断主窗口(keyWindow)自己是否能接受触摸事件(系统底层如何谈判?),如果能,那么在判断触摸点在不在窗口自己身上;

2.判断触摸点是否在自己身上(系统底层如何判断?),在自己身上继续第三步;

3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤);

4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。

5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view

UIView不能接收触摸事件的三种情况:

       1.不允许交互:userInteractionEnabled = NO

       2.隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件

       3.透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

       注 意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES。


2.1 寻找最合适的view底层剖析

UIView基类提供的2个方法

1. - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法,方法可以返回最合适的view

注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法; 如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。


2. - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  // default returns YES if point is in bounds

作用:判断下传入过来的点在不在方法调用者的坐标系上,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法


3.事件的响应

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件;

2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理。

touches默认做法。


         注:这个图是从其他地方截取的,我个人认为有不合理的地方,[super touchesBegan:touches withEvent:event]这段代码应该改为[[self nextResponder] touchesBegan:touches withEvent:event] 或者 [[self superview] touchesBegan:touches withEvent:event];因为响应链是往上找 下一个响应者 或者 父控件 而不是去父类里面找touch方法,然后调用。带着这个疑惑我自己写了个代码发现按这两种方式写都没有问题。为何会这样呢???

事件处理的整个流程总结:

1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,事件传递已完成)

4.最合适的view会调用自己的touches方法处理事件

5.touches默认做法是把事件顺着响应者链条向上抛。


3.1 4 实际项目中的应用

情景1: 点击子控件,让父控件响应事件;

实现方式一 : 因为hitTest:withEvent:方法的作用就是控件接收到事件后,判断自己是否能处理事件,判断点在不在自己的坐标系上,然后返回最合适的view。所以,我们可以在hitTest:withEvent:方法里面强制返回父控件为最合适的view

#import "GreenView2.h"

@implementation GreenView2

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{

          return [self superview]; // return nil;

          // 此处返回nil也可以。返回nil就相当于当前的view不是最合适的view

}

@end


实现方式二: 让谁响应,就直接重写谁的touchesBegan: withEvent:方法

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

             NSLog(@"do somthing...");

}



情景2:点击子控件,父控件和子控件都响应事件

实现方式事件的响应是顺着响应者链条向上传递的,即从子控件传递给父控件,touch方法默认不处理事件,而是把事件顺着响应者链条传递给上一个响应者。这样我们就可以依托这个原理,让一个事件多个控件响应
#import "GreenView2.h"

@implementation GreenView2

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

           NSLog(@"-- touchGreen");

        if ([self nextResponder]) {

             [[self nextResponder] touchesBegan:touches withEvent:event];

       }

        //或者使用父控件

        // [[self superview] touchesBegan:touches withEvent:event];

}



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容