iOS开发 -- 响应事件
一、响应过程
在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。而我们常用的可以响应事件的几个类:UIViewController,UIView,UIApplication都是直接继承自UIResponder。So,跟着来了解下。
在UIResponder中有四个方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//开始触摸
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸移动
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸结束
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸取消
所有进行自定义触摸处理的响应器都应该覆盖这四种方法,
我们用这四个方法来帮助我们了解下响应过程。
先写个视图,CustomView是自定的一个UIView:
- (void)viewDidLoad {
[super viewDidLoad];
CustomView *redView = [[CustomView alloc] initWithFrame:CGRectMake(10, 100, 300, 300)];
redView.name aaa@qq.com"红色";
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
CustomView *greenView = [[CustomView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
greenView.name aaa@qq.com"绿色";
greenView.backgroundColor = [UIColor greenColor];
[redView addSubview:greenView];
}
运行长这样:
然后重写响应方法和一个测试方法:
/** 手指按下时响应 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
[super touchesBegan:touches withEvent:event];
NSLog(@"--->手指按下时响应");
[self eventGoWhere:[touches anyObject].view];
}
- (void)eventGoWhere:(UIView *)view {
// 遍历响应者链。返回第一个找到视图控制器
UIResponder *responder = view;
NSLog(@"responder --- %@",responder);
while ((responder = [responder nextResponder])){
NSLog(@"responder nextResponder--- %@",responder);
}
}
然后 点击greenView,控制台输出:
从输出看出,响应链顺序是:greenView -> redView -> UIViewController.View -> UIViewController -> UIWindow -> UIApplication -> AppDelegate
第一响应者是greenView,如果greenView不能处理响应事件,那就传递给nextResponder。如果一个View有一个视图控制器(View Controller),它的下一个响应者是这个视图控制器,紧接着才是它的父视图(Super View),如果一直到Root View都没有处理这个事件,事件会传递到UIWindow(iOS中有一个单例Window),此时Window如果也没有处理事件,便进入UIApplication,UIApplication是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。
ok,那我们来思考一下,当我们的手指触摸到屏幕的时候,系统是如何找到第一个响应者的呢?
事实上,iOS系统检测到手指触摸(Touch)操作时,会将其放入当前活动Application的事件队列。UIApplication会从事件队列中取出触摸事件,并传递给key window(当前接收用户事件的窗口)处理。window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。
hitTest:withEvent:方法 什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用:寻找并返回最合适的view(能够响应事件的那个最合适的view)
事件传递给谁,就会调用谁的hitTest:withEvent:方法。
如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
我们来测试一下这个过程,再加几个视图:
CustomView *blueView = [[CustomView alloc] initWithFrame:CGRectMake(10, 450, 300, 200)];
blueView.name aaa@qq.com"蓝色";
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];
CustomView *purpleView = [[CustomView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
purpleView.name aaa@qq.com"紫色";
purpleView.backgroundColor = [UIColor purpleColor];
[blueView addSubview:purpleView];
CustomView *grayView = [[CustomView alloc] initWithFrame:CGRectMake(60, 60, 50, 50)];
grayView.name aaa@qq.com"灰色";
grayView.backgroundColor = [UIColor grayColor];
[blueView addSubview:grayView];
然后Custome里重写hitTest:withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@",self.name);
//判断是否合格
if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
BOOL pointInside =[self pointInside: point withEvent:event];
if (pointInside) {
NSLog(@"%@ -- YES",self.name);
}else{
NSLog(@"%@ -- NO",self.name);
}
}
return [super hitTest:point withEvent:event];
}
注:hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。
这时候再点击purpleView:
控制台输出可见,先找到blueView,发现pointInside:withEvent:返回为YES,然后查找其子视图grayView,返回NO,再找到了另一个子视图purpleView,返回YES并且其没有子视图。
hitTest 的顺序如下:
总结hitTest:withEvent:方法的处理流程如下:
1.在*视图(Root View)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;;
2.若返回NO,则hitTest:withEvent:返回nil;
3.若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
4.若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
5.如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。
当我们手指触摸屏幕是,系统先找寻Initial View,即Hit-Test 机制:
顺序为:UIApplication -> UIWindow -> Root View -> ··· -> subview
然后找到了Initial View后,如果Initial View并没有或者无法正常处理此次Touch。这个时候,系统就会通过响应者链寻找下一个响应者,也就是前面那个顺序:
Initial View -> View Controller(如果存在) -> superview -> · ·· -> rootView -> UIWindow -> UIApplication
这就是整个响应过程,是两个相反的顺序。
总结一下视图没有响应的击中情况:
1.userInteractionEnabled=NO;
2.hidden=YES;
3.alpha=0~0.01;
4.没有实现touchesBegan:withEvent:方法,直接执行touchesMove:withEvent:等方法;
5.目标视图点击区域不在父视图的Frame上 (superView背景色为clear Color的时候经常会忽略这个问题)。
实际应用,事件拦截
拦截事件的处理
因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
我们来测试一下,两个子视图,一个在下面被上面一个覆盖,但是点击在下面一个视图的范围内时,希望响应的是下面视图的事件。
在CustomView里加两个Button
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//红色
UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
btn1.frame = CGRectMake(0, 100, 300, 300);
btn1.backgroundColor = [UIColor redColor];
[btn1 addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
btn1.tag = 1;
[self addSubview:btn1];
//绿色
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
btn2.frame = CGRectMake(50, 200, 200, 300);
btn2.backgroundColor = [UIColor greenColor];
[btn2 addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
btn2.tag = 2;
[self addSubview:btn2];
}
return self;
}
然后点击绿色按钮,
响应绿色按钮的事件。
然后我们重写hitTest
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = [super hitTest:point withEvent:event];
CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
if ([underButton pointInside:buttonPoint withEvent:event]) {
return underButton;
}
return result;
}
- (void)buttonClick:(UIButton *)button
{
NSLog(@"--- %ld",button.tag);
}
然后我们点击被覆盖重叠的部分,和只有绿色按钮的部分。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!
上一篇: Linux源码安装
下一篇: 33.MySQL中的视图