序言:翻阅资料,学习,探究,总结,借鉴,谢谢探路者,我只是个搬运工。
参考、转发资料:
http://www.jianshu.com/p/2e074db792ba
http://www.jianshu.com/p/2b34ea0b6762
http://blog.csdn.net/a316212802/article/details/50061317
1. 事件的传递、响应流程。
事件传递机制。首先要有个事件(这里我们我们只讲解最常用的触摸事件),然后怎么这个事件是被谁发现的存储在哪,中间是怎么个传递过程,怎么判断谁能执行谁不能执行,最后谁处理。我想这些就是我们需要理解掌握的。
- 事件源:
我们研究的是触摸事件,触摸事件其实也分为很多种。例如- 轻击手势 TapGestureRecognizer
- 轻扫手势 SwipeGestureRecognizer
- 长按手势 LongPressGestureRecognizer
- 拖动手势 PanGestureRecognizer
- 捏合手势 PinchGestureRecognizer
- 旋转手势 RotationGestureRecognizer
为了方便以下讲述的使用轻击手势。
-
事件是被谁发现的存储在哪
当程序中发现触摸事件之后,系统会将事件加入到UIApplication管理的一个任务队列中,以堆的形式存储,先进先出先执行。- UIApplication是什么?
UIApplication对象是应用程序的象征,每个应用都有一个自己的UIApplication对象。在一个iOS程序启动后创建的第一个对象就是UIApplication对象,所以在AppDelegate.m文件中执行了UIApplicationDelegate方法供我们使用,而且UIApplication对象以一个单例的形式创建的,所以在其他文件中都能通过单例的形式获取到UIApplication对象。
- UIApplication是什么?
中间是怎么个传递过程,怎么判断谁能执行谁不能执行,最后谁处理
- 系统会将事件加入到UIApplication管理的一个任务队列中,以栈的形式存储,先进先出先执行。
- UIApplication会将事件发送给我们最底层的窗口UIWindow,这里的UIWindow值的是keyWindow(主),也只有显示在keyWindow上的视图才能接受点击事件的响应。
- UIWindow将事件发送给控制器(或者视图),如果控制器能处理事件的响应,而且触摸点在自己的身上,那么继续寻找子视图。
- 遍历所有的子视图,重复上一步的判断,以此循环,直到条件不满足。
- 到了这里表示上一步骤的条件不满足。
- 如果找到的作用点在子视图上,但是子视图的userInteractionEnabled属性设置NO(是否响应的设置),那么这个事件就会被废弃。
- 如果找不到触摸点没有在点击的子视图上,那么这个事件由父视图执行。
- 如果摸点在子视图的区域,但是设置了隐藏或者透明度为0时,那这个事件同样由父视图执行。
这就是判断谁能执行谁不能执行,最后谁处理的理由。
2. 具体如何通过代码判断
-
hitTest:withEvent:
方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
}
这个方法可以返回最合适的view,什么叫最合适,首先这个方法的返回值,可以是自己,也以是子视图,也或者是nil。在我们不重写这个方法的时候,就要子视图是否满足我们的查询要求。
-
pointInside:withEvent:
方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
}
方法判断触摸点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
- 内部的实现大概是这样的
- (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
{
// 判断点在不在区域内
if (CGRectContainsPoint(self.button.bounds, tempoint))
{
return YES ;
}
return NO ;
}
3. 响应者链
UIResponder作为一个响应者事件,所有的可交互控件都是UIResponder直接或者间接的子类。
响应者链的起点为: 上面我们所寻找到的最最合适的View,而这个View将调用自身的
touchesBegan:withEvent:
方法来开始事件响应者链的起始传递。添加到屏幕中的视图层级关系,从最上层的可能是button的点击事件,传递给父类或者自身的控制器并调用父类的
touchesBegan:withEvent:
方法。一直到最底层显示的keyWindow上这就是响应者链的传递。响应者链是由多层视图组成的结构(不管是View,还是控制器都是继承于响应者类UIResponder的)。由最上面的子控件传递给最底层的window。-
如何判断上一个响应者
- 如果当前这个view是控制器的view,那么控制器就是上一个响应者
- 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
-
事件响应和响应者链的区别:
- 事件响应是从最底层的keyWindow开始向上分发事件的,而响应者链是从最合适View开始向下传递的。 方向不同。
4. 实际案例。
- button部分视图区域超出了父视图,怎么实现点击超出部分也执行点击效果。
例子:
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
AView *aView = [[AView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)] ;
aView.backgroundColor = [UIColor grayColor] ;
[self.view addSubview:aView] ;
[aView _initViews] ;
}
// AView.m
// 创建试图
- (void)_initViews
{
self.button = [UIButton buttonWithType:UIButtonTypeCustom] ;
self.button.frame = CGRectMake(-50, -50, 100, 100) ;
self.button.backgroundColor = [UIColor redColor] ;
[self.button addTarget:self action:@selector(buttoClick:) forControlEvents:UIControlEventTouchUpInside] ;
[self addSubview:self.button] ;
}
// 点击方法
- (void)buttoClick:(id)sender
{
NSLog(@"点击了button") ;
}
如果在AView中不做任何处理,那么
- 解决办法:
在AView中重写hitTest:withEvent:
方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
if (view == nil) {
/*
// 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view;
// 将像素point从view中转换到当前视图中,返回在当前视图中的像素值
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view;
*/
CGPoint tempoint = [self.button convertPoint:point fromView:self];
if (CGRectContainsPoint(self.button.bounds, tempoint))
{
view = self.button;
}
}
return view;
}
```
当点击的触摸手势在Button的视图上,这样手动判断返回我们指定的View为Button,就可以了。
2. 有时候我们经常有这样的需求,在子视图View中想拿到View所在的控制器进行一些操作,通过事件响应者链寻找子视图所在的控制器。(这里我们抛弃基础的通过superView的方法获取)
首先我们要明白一点就是:可交互控件都是UIResponder直接或者间接的子类。
import "UIView+ViewController.h"
@implementation UIView (ViewController)
/*
为UIView扩展一个类目,通过这个方法可以获取这个视图所在的控制器
*/
- (UIViewController *)viewController
{
// 获取当前对象的下一响应者
UIResponder *nextResp = self.nextResponder;
while (![nextResp isKindOfClass:[UIViewController class]] && nextResp != nil) {
// 获取nextResp对象的下一响应者
nextResp = nextResp.nextResponder;
}
return (UIViewController *)nextResp;
}
使用方法,例如我们还是在我们刚写的deme中的button点击方法中使用,看看效果,记得导入类别。
- (void)buttoClick:(id)sender
{
NSLog(@"点击了button") ;
UIViewController *vc = [self viewController] ;
NSLog(@"%@",[NSString stringWithUTF8String:object_getClassName(vc)]) ;
// 2017-03-17 10:57:57.289 Init[2384:473093] 点击了button
// 2017-03-17 10:57:57.290 Init[2384:473093] ViewController
}