响应者对象:能处理事件的对象,也就是继承自UIResponder的对象。
响应者链是由多个响应者对象连接起来的链条。
只有继承UIResponder的的类,才能处理事件,UIApplication、UIView、UIViewController都是继承自UIResponder类,可以响应和处理事件。CALayer不是UIResponder的子类,无法处理事件。
用队列管理事件,而不用栈:队列先进先出,能保证先产生的事件先处理。栈先进后出。
先看一个例子来引出相关概念:
如图:控制器的view上有一个子视图AView,在AView上有一个子视图BView
我们通过点击事件来打印出最合适的view:
#import "AView.h"
@implementation AView
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view == self) {
NSLog(@"AView上最合适view是%@",@"AView");
return self;
}else if(view == self.subviews[0]){
NSLog(@"AView上最合适view是%@",@"BView");
return view;
}else{
NSLog(@"在AView上没有最合适的view");
return nil;
}
}
@end
==================================================
#import "BView.h"
@implementation BView
@end
==================================================
#import "ViewController.h"
#import "AView.h"
#import "BView.h"
@interface ViewController ()
@property (nonatomic, strong) AView *aView;
@property (nonatomic, strong) BView *bView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"ViewController";
[self.view addSubview:self.aView];
[_aView addSubview:self.bView];
}
-(AView *)aView{
if (!_aView) {
_aView = [[AView alloc] init];
_aView.backgroundColor = [UIColor yellowColor];
_aView.frame = CGRectMake(20, 100, 200, 200);
}
return _aView;
}
-(BView *)bView{
if (!_bView) {
_bView = [[BView alloc] init];
_bView.backgroundColor = [UIColor greenColor];
_bView.frame = CGRectMake(0, 0, 100, 100);
}
return _bView;
}
@end
执行结果:
333.gif-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法可以找到最合适的view成为事件的响应者。
但是他是怎么找到的呢?
我们队上面代码进行改写:
###//在AView.m文件中:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
NSLog(@"在AView上self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES导致它和其子视图没有最合适的view");
return nil;
}
// 判断点在不在当前控件
if (![self pointInside:point withEvent:event]) {
NSLog(@"触摸事件不在AView上");
return nil;
}
NSInteger count = self.subviews.count;
//从外向里遍历自己的子控件
for (NSInteger i = count-1; i>=0; i--) {
UIView *subView = self.subviews[i];
CGPoint subPoint = [self convertPoint:point toView:subView];
UIView *suitView = [subView hitTest:subPoint withEvent:event];
if (suitView) {
NSLog(@"AView上最合适view是%@",@"BView");
return suitView;
}
}
NSLog(@"AView上最合适view是%@",@"AView");
return self;
}
###//在BView.m文件中:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view) {
if (view == self) {
NSLog(@"BView上最合适view是%@",@"BView");
return self;
}else if(view == self.subviews[0]){
NSLog(@"BView上最合适view是%@",@"CView");
return view;
}else{
NSLog(@"BView上没有最合适的view");
return nil;
}
}else{
NSLog(@"触摸事件BView上");
return nil;
}
}
分别点击BView、AView和视图控制器对应的view,打印结果分别如下:
BView:
AView:
image.pngview:
image.png我如果把AView的self.userInteractionEnabled设为NO
点击三个view的运行结果一样:
image.png分析:
点击BView
1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView符合要求(什么要求在下面总结),然后再开始遍历其子视图,看看是否有更合适的。
2、BView是AView的唯一子视图,执行BView的hitTest:withEvent:发现符合条件
3、而且BView上没有子视图,所以其成为最合适view,返回。
点击AView
1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView符合要求(什么要求在下面总结),然后再开始遍历其子视图,看看是否有更合适的。
2、BView是AView的唯一子视图,执行BView的hitTest:withEvent:发现不符合条件(触摸点不在BView上)。所以AView就是最合适的view, 返回。
点击控制器对应的view
1、首先执行控制器view对应的hitTest:withEvent:,发现符合条件,
2、view上有子视图AView、BView在AView上。所以遍历view的子视图找到AView,执行AView的hitTest:withEvent:发现不符合条件(触摸点不再AView上)
3、因为AView已经不符合条件了,所以就不会再去遍历AView的子视图,最终确定控制器对应的view就是最合适的view,返回。
在AView的self.userInteractionEnabled设为NO或者 _aView.alpha = 0.001;
或者 _aView.hidden = YES;
的情况下
1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView不符合要求(userInteractionEnabled为NO或者alpha<=0.01或者隐藏),同时不会再遍历其子视图。
总结:
view符合条件必须同时满足:
1、view的userInteractionEnabled为YES;
2、view的hidden为NO;
3、view的alpha大于0.01;
事件的传递:就是寻找最合适View的过程
1、当iOS程序中发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中。
2、UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。
3、AppDelegate 的 window 收到事件,并开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
4、View首先看自己是否能处理事件、是否隐藏、触摸点是否在自己身上、如果这三个条件都满足,那么继续遍历寻找子视图。
5、如果没有找到合适的子视图,则说明自己就是做合适的view。
7、如果有则执行4步骤。
事件传递到某视图后,就调用其hitTest:withEvent:方法寻找更合适的view。想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件。
return nil的意思是调用当前的view不是合适的view。
image.png上图视图层级从外向里顺序依次是 CDA EFB 父视图。当点击界面发生触摸事件时,遍历父视图的子视图AB,从后往前遍历,先遍历A视图。
事件的响应:
1、首先看找到的最合适的view能否处理这个事件;
2、如果不能则会将事件传递给其父视图;
3、如果父视图仍然无法处理则会继续往上传递;一直传递到视图控制器;
4、判断视图控制器的根视图view是否能处理此事件:如果不能则接着判断该视图控制器能否处理此事件;
5、如果还是不能则继续向上传递;
6、一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。
在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递(子控件到父控件)。
判断上一个响应者nextResponder
1、 当前view是控制器的view,那么控制器就是上一个响应者
2、 当前view不是控制器的view,那么父视图就是上一个响应者
应用:
在自定义的view中如果想在自己里面做页面push跳转,需要找到最上面的一个UINavigationController
#import "UIView+WGNavigationController.h"
@implementation UIView (WGNavigationController)
- (UINavigationController *)wgNavigationController{
//响应连上一个响应者
UIResponder *responder = self.nextResponder;
while (responder) {
if ([responder isKindOfClass:[UINavigationController class]]) {
return (UINavigationController*)responder;
}
responder = responder.nextResponder;
}
return nil;
}
@end
参考文章:
1、史上最详细的iOS之事件的传递和响应机制-原理篇
网友评论