这篇文章只讨论触摸事件。对于触摸事件UIResponder内部提供了以下方法来处理事件:
触摸事件
- (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;
举个例子:
就像上图那样,点击了红色的View, 如果先加载蓝色View,后加载红色UIView 传递过程是这样的:
UIApplication对象——>UIWindow对象——>rootVC.view对象——>redview对象
如果先加载红色View,后加载蓝色UIView 传递过程是这样的:
UIApplication对象——>UIWindow对象——>rootVC.view对象——>blueview对象——>redview对象
//************华丽分割线 便于阅读***********
事件的传递其实就是在事件产生与分发之后如何寻找最优响应视图的一个过程。其中涉及到了UIView中的两个方法(可以重写),当hitTest返回YES才会调用这个View的 Touch事件,因为如果返回NO,则当前View被排除在相应链之外了。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击事件是否存在最优响应者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击是否在控件的Bounds之内
视图命中查找流程
1.调用hitTest方法进行最优响应视图查询
hidden = YES
userInteractionEnabled = NO
alpha < 0.01
以上三种情况会使该方法返回nil,即当前视图下无最优响应视图
2.hitTest方法内部会调用pointInside方法对点击点进行是否在当前视图bounds内进行判断,如果超出bounds,hitTest则返回nil。 未超出范围则进行步骤3
3.对当前视图下的subviews采取逆序上述1 2步骤查询最优响应视图。如果hitTest返回了对应视图则说明在当前视图层级下有最优响应视图,可能为self或者其subview,这个要看具体返回。
如何看到这一切呢? 我们可以重写view的-(UIView )hitTest:(CGPoint)point withEvent:(UIEvent)event方法来测试
#import "UIView+MYtes.h"
#import <objc/runtime.h>
@implementation UIView (MYtes)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL oriSEL = @selector(hitTest:withEvent:);
SEL swiSEL = @selector(wcq_hitTest:withEvent:);
Method oriMethod = class_getInstanceMethod(class, oriSEL);
Method swiMethod = class_getInstanceMethod(class, swiSEL);
BOOL didAddMethod = class_addMethod(class, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(class,
swiSEL,
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
}else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
- (UIView *)wcq_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ %s",[self class], __PRETTY_FUNCTION__);
return [self wcq_hitTest:point withEvent:event];
}
然后我们分别新建三个UIView的子类: AView、BView、CView并依次按顺序添加到ViewController上
然后我们依次点击A
、B
视图看下hitTes
调用顺序是否和预期一致
点击AView
点击BView
介绍响应者链之前先介绍下响应者对象
响应者对象:是可以响应事件并对其进行处理的对象。UIResponder是所有响应者对象的基类,它不仅为事件处理,而且也为常见的响应者行为定义编程接口。UIApplication、UIView、和所有从UIView派生出来的UIKit类(包括UIWindow)都直接或间接地继承自UIResponder类。 第一响应者是应用程序中当前负责接收触摸事件的响应者对象(通常是一个UIView对象)。
响应者链:由一系列“下一个响应者”组成
其顺序如下:
程序寻找能够处理事件的对象,事件就在响应者链中向上传递。
//******************* 华丽的分割线 ****************
系统先调用pointInSide: WithEvent:判断当前视图以及这些视图的子视图是否能接收这次点击事件,然后在调用hitTest: withEvent:依次获取处理这个事件的所有视图对象,在获取所有的可处理事件对象后,开始调用这些对象的touches回调方法
在自定义View中重写 touchesBegan方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UIResponder * next = [self nextResponder];
NSMutableString * prefix = @"".mutableCopy;
while (next != nil) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString: @"--"];
next = [next nextResponder];
}
}
通过这张图我们可以看到响应者链的组成。
需要注意的是:viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,想处理自定义View的触摸事件,必须在自定义UIView中重写touchBegan:withEvent:方法,两者不是一回事,但是都是继承自UIResponder 。
我们在使用UITextView和UITextField的时候,可以通过它们的inputAccessoryView属性给输入时呼出的键盘加一个附属视图,通常是UIToolBar,用于回收键盘。 但是当我们要操作的视图不是UITextView或UITextField的时候,inputAccessoryView就变成了readonly的。 这时我们如果还想再加inputAccessoryView,按API中的说法,就需要新建一个该视图的子类,并重新声明inputAccessoryView属性为readwrite的。比如我们要实现点击一个tableView的一行时,呼出一个UIPickerView,并且附加一个用于回收PickerView的toolbar。因此我们自建一个UITableViewCell类,并声明inputAccessoryView和inputView为readwrite的,并且重写它们的get方法,这样在某个tableviewcell变成第一响应者时,它就会自动呼出inputView和inputAccessoryView;
@interface MyTableViewCell : UITableViewCell<UIPickerViewDelegate,UIPickerViewDataSource>
{
UIToolbar *_inputAccessoryView;
UIPickerView *_inputView;
}
@property(strong,nonatomic,readwrite) UIToolbar *inputAccessoryView;
@property(strong,nonatomic,readwrite) UIPickerView *inputView;
@end
-(UIToolbar *)inputAccessoryView
{
if(!_inputAccessoryView)
{
UIToolbar *toolBar = [[UIToolbar alloc]initWithFrame:CGRectMake(0, 0, 320, 44)];
// UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonItem target:self action:@selector(dodo)];
UIBarButtonItem *right = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dodo)];
toolBar.items = [NSArray arrayWithObject:right];
return toolBar;
}
return _inputAccessoryView;
}
-(UIPickerView *)inputView
{
if(!_inputView)
{
UIPickerView * pickView = [[UIPickerView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
pickView.delegate =self;
pickView.dataSource = self;
pickView.showsSelectionIndicator = YES;
return pickView;
}
return _inputView;
}
手动把它变成第一响应者。(难道cell被选中时不是第一响应者?)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell becomeFirstResponder];
}
运行结果:
效果图
实现过程解析:
参考文章: iOS开发 - 事件传递响应链 iOS编程中的快递小哥-Responder Chain(响应链) IOS 应用事件的传递分析