Objective-C中的触摸事件传递与响应链机制
触摸事件基础
在iOS开发中,触摸事件是用户与应用交互的重要方式。当用户在屏幕上进行触摸操作时,系统会将这些触摸操作封装成事件,并在应用程序的界面中进行传递和处理。Objective-C作为iOS开发的主要编程语言之一,提供了一套完善的触摸事件传递与响应链机制。
触摸事件类型
- UITouch:代表用户在屏幕上的一次触摸操作。一个UITouch对象对应一次触摸,从触摸开始到结束的整个过程,该对象会包含触摸的位置、时间、阶段等信息。例如,用户按下屏幕时,系统会创建一个UITouch对象;手指移动过程中,该对象的位置信息会更新;当手指离开屏幕时,这个UITouch对象的生命周期结束。
- UIEvent:是事件的抽象基类,触摸事件是其具体的子类。UIEvent包含了在特定时间内发生的所有触摸事件(UITouch对象集合)。在实际应用中,一个UIEvent可能包含多个UITouch对象,比如用户进行多指触摸操作时。
- 触摸事件阶段:UITouch对象有不同的阶段,通过
phase
属性来表示。- UITouchPhaseBegan:触摸开始,当用户手指首次接触屏幕时进入此阶段。
- UITouchPhaseMoved:触摸移动,手指在屏幕上移动时处于此阶段。
- UITouchPhaseStationary:触摸静止,手指停留在屏幕上且没有移动时进入此阶段。
- UITouchPhaseEnded:触摸结束,手指离开屏幕时进入此阶段。
- UITouchPhaseCancelled:触摸取消,由于某些系统原因(如来电、系统弹窗等)导致当前触摸操作被取消时进入此阶段。
触摸事件处理方法
在UIResponder类(UIView和UIViewController的父类)中定义了一系列处理触摸事件的方法,UIView和UIViewController可以通过重写这些方法来处理触摸事件。
- 触摸开始:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
// 获取触摸点在当前视图坐标系中的位置
CGPoint point = [touch locationInView:self];
NSLog(@"Touch began at point: %@", NSStringFromCGPoint(point));
}
}
在这个方法中,touches
参数是当前事件中涉及的所有UITouch对象的集合,event
参数则是包含这些触摸事件的UIEvent对象。通过遍历touches
集合,可以获取每个触摸点在当前视图坐标系中的位置。
- 触摸移动:
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
// 获取触摸点在当前视图坐标系中的位置
CGPoint point = [touch locationInView:self];
NSLog(@"Touch moved to point: %@", NSStringFromCGPoint(point));
}
}
当手指在屏幕上移动时,系统会调用这个方法。同样通过遍历touches
集合获取触摸点的新位置。
- 触摸结束:
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
// 获取触摸点在当前视图坐标系中的位置
CGPoint point = [touch locationInView:self];
NSLog(@"Touch ended at point: %@", NSStringFromCGPoint(point));
}
}
当手指离开屏幕时,系统会调用此方法,在这个方法中可以进行一些触摸结束后的操作,比如完成一个绘制手势后的处理。
- 触摸取消:
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
// 获取触摸点在当前视图坐标系中的位置
CGPoint point = [touch locationInView:self];
NSLog(@"Touch cancelled at point: %@", NSStringFromCGPoint(point));
}
}
当触摸操作被取消时,系统会调用这个方法,通常可以在这里进行一些清理工作。
触摸事件传递
当用户触摸屏幕时,触摸事件需要在应用的视图层次结构中进行传递,以找到最合适的视图来处理该事件。这个过程涉及到一系列复杂的规则和机制。
事件传递的起点
- UIApplication:触摸事件首先由UIApplication对象接收。UIApplication是应用程序的核心对象,负责管理应用程序的生命周期、事件循环等。当系统检测到触摸事件时,会将事件传递给UIApplication。
- UIWindow:UIApplication接收到事件后,会将事件传递给应用程序的主窗口(UIWindow)。UIWindow是视图层次结构的根容器,所有的视图都添加在UIWindow上。一个应用程序通常有一个主窗口,但也可以有多个窗口(比如在分屏应用中)。
寻找最佳响应者视图
- hitTest:withEvent: 方法:UIWindow接收到事件后,会调用
hitTest:withEvent:
方法来寻找能够处理该事件的最佳响应者视图。hitTest:withEvent:
方法的作用是从当前视图开始,递归地调用子视图的hitTest:withEvent:
方法,寻找最合适的视图。该方法的返回值是最适合处理触摸事件的视图,如果没有找到合适的视图,则返回nil。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 首先判断当前视图是否隐藏、不响应用户交互或者alpha值小于0.01
if (self.hidden || self.userInteractionEnabled == NO || self.alpha <= 0.01) {
return nil;
}
// 判断触摸点是否在当前视图范围内
if (!CGRectContainsPoint(self.bounds, point)) {
return nil;
}
// 从后往前遍历子视图,因为后添加的子视图在前面
NSArray *subviews = self.subviews;
for (NSInteger i = subviews.count - 1; i >= 0; i--) {
UIView *subview = subviews[i];
// 将触摸点转换到子视图的坐标系中
CGPoint subPoint = [self convertPoint:point toView:subview];
UIView *hitView = [subview hitTest:subPoint withEvent:event];
if (hitView) {
return hitView;
}
}
// 如果没有子视图处理该事件,则返回当前视图
return self;
}
在上述代码中,首先检查当前视图是否满足接受触摸事件的条件(不隐藏、可交互且alpha值足够大),然后判断触摸点是否在当前视图的范围内。接着从后往前遍历子视图,将触摸点转换到子视图的坐标系中,递归调用子视图的hitTest:withEvent:
方法。如果某个子视图返回了非nil的视图,则表示找到了合适的响应者视图,直接返回该视图;如果所有子视图都没有找到合适的响应者视图,则返回当前视图。
- pointInside:withEvent: 方法:在
hitTest:withEvent:
方法中,会调用pointInside:withEvent:
方法来判断触摸点是否在当前视图的范围内。pointInside:withEvent:
方法主要用于判断触摸点是否在视图的矩形区域内,默认实现是根据视图的bounds
来判断。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return CGRectContainsPoint(self.bounds, point);
}
如果需要自定义触摸区域,可以重写pointInside:withEvent:
方法。例如,对于一个圆形的视图,可以通过计算触摸点到圆心的距离来判断是否在圆形区域内。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat radius = self.bounds.size.width / 2;
CGPoint center = CGPointMake(self.bounds.origin.x + radius, self.bounds.origin.y + radius);
CGFloat distance = sqrt(pow(point.x - center.x, 2) + pow(point.y - center.y, 2));
return distance <= radius;
}
通过重写这个方法,就可以让圆形视图正确地处理触摸事件。
事件传递示例
假设有一个简单的视图层次结构,包含一个主视图(MainView),主视图中有两个子视图(SubView1和SubView2),SubView2又有一个子视图(SubSubView)。
// 创建主视图
UIView *mainView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
mainView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:mainView];
// 创建SubView1
UIView *subView1 = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
subView1.backgroundColor = [UIColor redColor];
[mainView addSubview:subView1];
// 创建SubView2
UIView *subView2 = [[UIView alloc] initWithFrame:CGRectMake(150, 150, 100, 100)];
subView2.backgroundColor = [UIColor blueColor];
[mainView addSubview:subView2];
// 创建SubSubView
UIView *subSubView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
subSubView.backgroundColor = [UIColor greenColor];
[subView2 addSubview:subSubView];
当用户触摸屏幕时,UIWindow会调用hitTest:withEvent:
方法开始寻找最佳响应者视图。假设用户触摸的点在SubSubView的区域内,首先UIWindow调用主视图的hitTest:withEvent:
方法,主视图判断触摸点在其范围内,然后遍历子视图。主视图发现SubView2包含触摸点,调用SubView2的hitTest:withEvent:
方法。SubView2又发现SubSubView包含触摸点,调用SubSubView的hitTest:withEvent:
方法。SubSubView判断触摸点在其范围内,且没有子视图,最终返回自身作为最佳响应者视图。
响应链机制
当找到最佳响应者视图后,触摸事件会沿着响应链进行传递,直到有对象能够处理该事件。响应链是一个由响应者对象(UIResponder的子类,如UIView、UIViewController等)组成的链条。
响应链的构成
- 视图的响应链:最佳响应者视图首先尝试处理触摸事件。如果它不能处理,事件会沿着响应链向上传递。对于一个视图来说,它的下一个响应者是它的父视图。例如,SubSubView的下一个响应者是SubView2,SubView2的下一个响应者是MainView。如果MainView也不能处理事件,事件会继续向上传递给UIWindow,然后是UIApplication。
- 视图控制器的响应链:如果视图是由视图控制器管理的,那么视图控制器也会参与响应链。当视图的父视图无法处理事件时,事件会传递给视图控制器。例如,在一个UIViewController中添加的视图,当视图及其父视图都无法处理事件时,事件会传递给该UIViewController。如果UIViewController也不能处理,事件会继续传递给父视图控制器(如果有),直到传递到UIWindow和UIApplication。
响应链的传递过程
- 事件处理顺序:当事件到达最佳响应者视图后,首先调用该视图的触摸事件处理方法(如
touchesBegan:withEvent:
等)。如果该视图没有实现相应的处理方法,事件会传递给下一个响应者。下一个响应者会重复相同的过程,即先尝试调用自身的触摸事件处理方法,如果没有实现则继续传递。这个过程会一直持续,直到有对象处理了事件或者事件传递到UIApplication也没有被处理。 - 示例代码:假设有一个视图控制器(ViewController),在其视图(view)上添加了一个按钮(button)。
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(100, 100, 100, 50);
[button setTitle:@"Click Me" forState:UIControlStateNormal];
[button addTarget:self action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
- (void)buttonTapped {
NSLog(@"Button tapped");
}
@end
当用户点击按钮时,按钮作为最佳响应者视图,首先尝试处理触摸事件。由于按钮是UIControl的子类,它通过addTarget:action:forControlEvents:
方法注册了一个事件处理方法buttonTapped
,所以按钮能够处理这个触摸事件。如果按钮没有注册这个方法,事件会传递给它的父视图(ViewController的view),然后可能传递给ViewController,直到被处理或者传递到UIApplication。
自定义响应链
在某些情况下,可能需要自定义响应链。例如,在一个复杂的视图结构中,希望某个视图的下一个响应者不是它的父视图,而是另一个特定的视图。可以通过重写nextResponder
方法来实现自定义响应链。
@interface CustomView : UIView
@end
@implementation CustomView
- (UIResponder *)nextResponder {
// 返回自定义的下一个响应者
return self.superview.superview;
}
@end
在上述代码中,CustomView重写了nextResponder
方法,使其下一个响应者不是直接的父视图,而是父视图的父视图。这样,当CustomView无法处理事件时,事件会直接传递给父视图的父视图,而不是按照默认的响应链传递。
触摸事件传递与响应链的优化
在实际开发中,为了提高应用的性能和用户体验,需要对触摸事件传递与响应链机制进行优化。
减少不必要的事件传递
- 设置userInteractionEnabled:对于一些不需要处理触摸事件的视图,将其
userInteractionEnabled
属性设置为NO。这样,这些视图及其子视图都不会接收触摸事件,从而减少了事件传递过程中的计算量。例如,一个纯展示的视图,没有交互需求,就可以将其userInteractionEnabled
设为NO。
UIView *displayView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
displayView.backgroundColor = [UIColor grayColor];
displayView.userInteractionEnabled = NO;
[self.view addSubview:displayView];
- 合理设置hidden和alpha:视图的
hidden
属性和alpha
值也会影响事件传递。如果一个视图隐藏(hidden = YES
)或者alpha
值小于0.01,那么它不会接收触摸事件。在一些情况下,可以通过设置这些属性来避免不必要的事件传递。例如,当一个视图在某个状态下不需要响应触摸事件时,可以将其隐藏或者设置alpha
值。
UIView *temporaryView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
temporaryView.backgroundColor = [UIColor redColor];
// 在某个条件下隐藏视图,避免接收触摸事件
if (someCondition) {
temporaryView.hidden = YES;
}
[self.view addSubview:temporaryView];
优化hitTest:withEvent:方法
- 减少计算量:在
hitTest:withEvent:
方法中,尽量减少复杂的计算。例如,避免在该方法中进行大量的循环或者复杂的图形计算。可以提前计算一些固定的值,在hitTest:withEvent:
方法中直接使用。 - 合理利用缓存:如果视图的结构或者触摸区域比较固定,可以考虑使用缓存来存储一些计算结果。比如,对于一个不规则形状的视图,每次判断触摸点是否在其范围内都需要进行复杂的计算,可以在视图初始化时计算好边界信息并缓存起来,在
hitTest:withEvent:
方法中直接使用缓存数据进行判断。
响应链处理优化
- 集中处理事件:在响应链中,尽量在靠近最佳响应者视图的地方处理事件。这样可以减少事件在响应链中传递的次数,提高处理效率。例如,对于一个视图层次结构中的多个子视图都可能触发的事件,可以在它们的共同父视图中统一处理,而不是让事件在响应链中层层传递。
- 避免循环引用:在自定义响应链或者处理事件时,要注意避免循环引用。例如,在设置下一个响应者时,要确保不会形成一个闭环,导致事件传递陷入死循环。
通过对触摸事件传递与响应链机制的深入理解和优化,可以开发出更加流畅、高效的iOS应用程序。在实际项目中,根据具体的需求和场景,灵活运用这些知识,能够提升应用的性能和用户体验。无论是简单的UI交互还是复杂的手势识别,触摸事件传递与响应链机制都是iOS开发中不可或缺的重要部分。在不断的实践和优化过程中,开发者可以更好地掌握这一机制,打造出更优秀的应用。同时,随着iOS系统的不断更新和发展,这一机制也可能会有一些细微的变化和改进,开发者需要持续关注官方文档和最新的技术动态,以确保应用的兼容性和先进性。在日常开发中,多进行一些实际的测试和性能分析,及时发现并解决触摸事件处理过程中可能出现的问题,如响应不及时、误触等,从而为用户提供更加优质的应用体验。