一.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代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
3.事件的响应
1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件;
2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理。
注:这个图是从其他地方截取的,我个人认为有不合理的地方,[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];
}