一.触摸事件由触屏生成后如何传递到当前应用?
触屏后的传递系统响应
1.手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
2.IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。
- SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
所以,触屏生成触摸事件后,由IOKit将触摸事件传递给SpringBoard进程,再由SpringBoard分发给当前前台APP处理。
二.触摸事件的产生和传递:
iOS中的事件可以分为3大类型:
- 触摸事件
- 加速计事件(例如手机摇一摇)
- 远程控制事件(例如使用遥控给使用iOS系统的设备发出指令)
今天记录的是触摸事件,即app接收到触摸事件后如何做的。
1.事件的产生和传递
- 发生触摸事件后,系统将触摸事件放到UIApplication队列中(队列:先进先出,栈:先进后出)(先来的事件先处理);
- 按顺序取出事件,UIApplication的keyWindow作为主窗口,在keywindow上面找到合适的view进行处理;
- 找到合适的view之后,就会调用控件的touches方法来做具体的事件处理:
touchesBegan...
touchesMoved...
touchedEnded...
这是事件传递的开始;
也就是说:
触摸事件的传递是从父控件传递到子控件;
先把事件放到UIApplication的队列里面,然后UIApplication->window->寻找处理事件最合适的view
注 意:
如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
如何找到合适的控件来处理事件?
- 询问自己是否能接收触摸事件?
- 触摸点是否在自己身上?(在自己身上pointInside方法会返回yes)
- 前面两条都满足之后:从后往前(就是先看后添加的)遍历子控件,重复前面两个步骤。找到合适的view来接收事件
- 找到合适的view后,再遍历这个合适view的子控件,直到没有合适的view。
- 如果遍历到最后没有找到合适的view,自己就是合适的view
1. 询问自己是否能接收触摸事件?/不可以接收触摸事件的条件:
透明度0.0~0.01之间;
隐藏(hidden = yes);
userInteractionEnabled = no;
2. 触摸点是否在自己身上?
- hitTest方法(返回合适的view)
//当事件传递给当前view时,会调用hitTest方法
//作用:寻找最适合的view
//返回值:返回谁,谁就是最适合的view响应事件,就会调用谁的touchs方法(通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。)
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView * fitView = [super hitTest:point withEvent:event];
return fitView;
}
注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
- pointInside方法(触摸点在当前view身上返回yes,否则返回no)
//作用:判断点在不在自己身上,是调用当前view的一个方法
//什么时候调用:在hitTest内部调用
//返回值:YES--》在当前viwe身上;No--》不在当前view身上;
//point:当前触摸点
//注意:必须得要跟方法调用者在同一个坐标系
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return [super pointInside:point withEvent:event];
// return YES/NO;
}
3. 从后往前(就是看后添加的先看)遍历子控件,重复前面两个步骤
事件传递过程2.模拟实现事件传递底层
#import "WSWindow.h"
@implementation WSWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[I];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
三.小测验
1.HitTest方法运用:
在controller里面,添加了一下几个view,顺序如下:
白色WhiteView上面添加:1.1绿色 Greenview、1.2橙色 OrangeView
橙色上面添加:2.1 蓝色 BlueView、2.2 红色 RedView
蓝色上面添加:3 黄色
黄色上面添加:4
如下:WhiteView的hitTest里面 return [super hitTest:point withEvent:event];
绿色、橙色、蓝色view的hitTest里面 return self;
问:点击空白处(WhiteView),哪个view响应事件
@implementation WhiteView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touche:%s",__func__);
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return [super hitTest:point withEvent:event];
}
@end
@implementation GreenView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touche:%s",__func__);
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
@end
@implementation OrangeView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touche:%s",__func__);
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
@end
@implementation BlueView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touche:%s",__func__);
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
@end
答案:橙色;打印: touche:-[OrangeView touchesBegan:withEvent:]
原因:
1.白色view可以接收事件(没有隐藏、不透明)
2.点在白色view身上(pointInside 返回yes)
3.从后往前遍历子控件看哪个view做适合的view(hitTest返回的view):
顺序:橙色-->绿色
遍历到橙色的时候,橙色里面的hitTest返回self,那橙色的view就是合适的view。所以橙色view的touche事件响应。
2.pointInside方法运用:
问:在1问的基础上,WhiteView里面添加以下方法,点击空白的地方,谁响应事件?
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return NO;
}
答:window响应。因为Whiteview的pointInside返回no,whiteview不响应事件,就是他的父控件响应。
四.hitTest方法实例运用
1.运用1
- 问题:如下图,在Main.storyboard界面上添加橙色按钮(OrangeBtn)、绿色view(GreenView)。在两个控件重叠的红色区域,橙色按钮事件无法响应,如何解决?
-
解决思路:
每次事件传递的时候都会调用hitTest方法来获取合适的view作为响应对象;
当触摸重合区域的时候,让橙色按钮成为合适的view。 -
解决:
1.在绿色view(GreenView)里面获取到橙色按钮
但是在GreenView无法从storyboard里面引入OrangeBtn。我们反向思维,在GreenView先写好要引入的控件,再反向拖进来,如下图:
2.处理GreenView里面的hitTest方法,重叠的时候让橙色按钮响应
#import "GreenView.h"
@implementation GreenView
//传递事件的时候会掉这里
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//如果触摸点在橙色按钮上面,就让橙色按钮成为合适的view
CGPoint inOrangePoint = [self convertPoint:point toView:self.orangeBtn];
if ([self.orangeBtn hitTest:inOrangePoint withEvent:event]) {
return self.orangeBtn;
}
//否则,按原本的操作进行
return [super hitTest:point withEvent:event];
}
@end
2.运用2
- 问题:在storyBoard上面添加红色按钮(RedBtn),点击红色按钮后添加黄色按钮(yellowBtn)在红色按钮上面。因为黄色按钮在红色按钮外面,所以黄色按钮的点击事件失效,现在需要黄色按钮可以响应事件。
viewcontroller里面:
//红色按钮点击事件
- (IBAction)redBtnClick:(id)sender {
//点击后添加黄色按钮
UIButton * yellowBtn = [UIButton buttonWithType:UIButtonTypeCustom];
yellowBtn.frame = CGRectMake(20, - 20, 100, 30);
[yellowBtn setBackgroundColor:[UIColor yellowColor]];
[_redBtn addSubview:yellowBtn];
_redBtn.yellowBtn = yellowBtn;//把黄色按钮传给红色按钮
[yellowBtn addTarget:self action:@selector(yellowBtnClick) forControlEvents:UIControlEventTouchUpInside];
}
//黄色按钮的点击事件
-(void)yellowBtnClick{
NSLog(@"newBtnclick");
}
- 解决:
如果触摸点点在黄色按钮身上,就让黄色按钮成为合适的view;
这样点击黄色view的时候,黄色view的事件就可以执行啦!!
//事件传递的时候会掉这里
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//当一个控件不在父控件里面的时候无法接收到事件
//如果点击的按钮在_yellowBtn上,就让_yellowBtn成为合适的view,触发_yellowBtn的事件
CGPoint getPoint = [self convertPoint:point toView:_yellowBtn];
if ([_yellowBtn pointInside:getPoint withEvent:event]) {
return _yellowBtn;
}
return [super hitTest:point withEvent:event];
}
所以,想让谁响应事件,就让谁成为合适的view就行。
五.事件的响应
前面有记录:找到合适的view之后,就会执行合适view的touchesBegan方法。
而touchesBegan方法的默认做法是,将事件顺着响应链交给上一个响应者处理。
那么响应链条是什么呢,上一个响应者又是谁呢?
响应链条
- 响应者链条:是由多个响应者链接起来的链条
- 作用:能看清楚每个响应者之间的联系,并且让一个事件多个对象处理
-
响应者对象:能处理事件的对象
响应者链条示意图
上一个响应者是谁?
- 如果当前view是控制器的view,那么
控制器
就是上一个响应者 - 如果当前这个view不是控制器的view,那么
父控件
就是上一个响应者
证明
如下图,有一个ViewController;
ViewController的view取名为WhiteView;
WhiteView里面有一个OrangeView;
证明当前view不是控制器的view,那么父控件就是上一个响应者
我们证明OrangeView为适合view的时候,父控件WhiteView就是上一个响应者。
分析:
- OrangeView为适合view的时候会调用OrangeView的touchesBegan方法;
- 如果OrangeView的touchesBegan方法我们不重写/ [super touchesBegan:touches withEvent:event]; 方法,就会调用上一个响应者的方法。
- 我们不重写OrangeView的touchesBegan方法,看它是否调用WhiteView的touchesBegan方法。如果是,那父控件WhiteView的就是上一个响应者。
操作:
如下,WhiteView的touchesBegan调用会打印,OrangeView里面没有写任何代码。
触摸OrangeView,打印:touche:-[WhiteView touchesBegan:withEvent:]
结论:
OrangeView为适合view的时候,父控件WhiteView就是上一个响应者。
#import "WhiteView.h"
@implementation WhiteView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"touche:%s",__func__);
}
@end
#import "OrangeView.h"
@implementation OrangeView
@end
如果当前view是控制器的view,那么控制器就是上一个响应者
我们证明WhiteView为适合view的时候,控制器ViewController就是上一个响应者。
分析:
- WhiteView是适合view的时候会调用WhiteView的touchesBegan方法;
- 如果WhiteView的touchesBegan方法我们不重写/或者[super touchesBegan:touches withEvent:event];,就会调用上一个响应者的方法。
- 我们不重写WhiteView的touchesBegan方法,看它是否调用ViewController的touchesBegan方法。如果是,控制器ViewController就是上一个响应者。
操作:
如下,WhiteView的touchesBegan调用 [super touchesBegan:touches withEvent:event];,ViewControlledr的touchesBegan调用会打印。
触摸WhiteView,打印:-[ViewController touchesBegan:withEvent:]
结论:
WhiteView为适合view的时候,控制器ViewController就是上一个响应者。
#import "WhiteView.h"
@implementation WhiteView
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
}
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
六.事件完整的响应过程(事件如何传递、如何响应)
事件完整的响应过程- 先把事件放到Application队列里面,按照先进先出的方式取出事件;
- Application一般取出keyWindow作为主窗口,由上往向下传递(由父控件传递给子控件),找到合适的view;
3.合适的view对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
4.响应者对于接收到的事件有3种操作:
-
不拦截,默认操作
事件会自动沿着默认的响应链往下传递 -
拦截,不再往下分发事件
重写 touchesBegan:withEvent: 进行事件处理,不调用[super touchesBegan] -
拦截,继续往下分发事件
重写 touchesBegan:withEvent: 进行事件处理,同时调用[super touchesBegan] 将事件往下传递
关于如何判断上一个响应者:
-
UIView
1.如果当前view是控制器的view,那么上一个响应者就是控制器;
2.如果当前view不是控制器的view,那么上一个响应者就是父控件; -
UIViewController
若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。 -
UIWindow
nextResponder为UIApplication对象。 -
UIApplication
若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
嗯。。。事件的传递和响应记录完了。属于很简单的一个知识点,这次简单的学习也发现了之前写法的一些漏洞,也感受到了学习的喜悦,祝愿自己能再接再力,多多学习吧!
这部分的面试考点:
1.事件如何传递和响应的?
传递的时候:
主要通过hitTest和pointInside两个方法判断;
1.先把事件加入到Application队列中,然后以keywindow作为主窗口传递事件;
2.判断view是否可以点击(比如是否隐藏、透明度是否是0-0.01之间,userInteractionEnabled是否为no)
3.通过pointInside判断点是否在这个view上面;
4.第2、3两条都满足之后,从后往前遍历view的子控件,调用子控件的hitTest方法(即2、3)两步寻找合适的view;
5.如果子控件里面都没有找到合适的view,父控件就是合适的view;
事件响应:
合适的view默认把事件顺着响应链向下传递。它也可以通过touchesBegan方法拦截事件,自己处理。
如果合适的view重写touchesBegan方法,不调用[super touchesBegan] ,就自己处理了。
如果合适的view重写touchesBegan方法,调用[super touchesBegan] ,自己处理后也会把事件向下传递。
2.事件响应,如果一直都没有响应对象会怎么样?
会忽略掉,无任何反应。(就像我们在controller上门添加了一个view,对view什么都不处理,触摸的时候也不会崩溃)
3.UIView和CALayer的关系(美团的面试题)
- UIView提供内容,处理触摸等事件,参与响应链;
- CALayer负责显示内容contents
4.为什么UIView只提供事件处理,CALyer只负责内容显示?
单一职责原则:UIView只提供事件处理;CALyer只负责内容显示
5.如下,红色正方形按钮中,有一个圆。实现红色部分不可点击,圆形部分可以点击。
示例图思路:判断触摸的点是否在圆内,如果圆内就可以点击,否则不可点击;
判断一个点是否在圆内:判断这个点到圆心的距离>半径,那么点不在圆内;
例如判断A点到圆心B的距离,就是求距离c的大小。
直角三角形的斜边 = (直角边的平方 + 直角边的平方)的平方根
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGFloat x1 = point.x;
CGFloat y1 = point.y;
CGFloat x2 = self.frame.size.width / 2;
CGFloat y2 = self.frame.size.height / 2;
//sqrt:求平方根
//直角三角形的斜边 = (直角边的平方 + 直角边的平方)的平方根 ; C的平方 = a的平方 + b的平方
double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
//这个点到圆心的距离小于半径,点就在圆内,可以点击
if (dis <= self.frame.size.width / 2) {
return YES;
}
else{
return NO;
}
}
网友评论