** 响应者链条**
- 在iOS中不是任何对象都能处理时间,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”
- UIApplication UIViewController UIView都是继承自UIResponder,因此他们都是响应者对象,都能够接收并处理事件。
- 当用户点击按钮,系统最先响应这个点击事件,由AppDelegate接收,然后不断上抛直到到达UIButton,如图中实线所示。当UIButton接收到这个事件,UIResponder相应的方法开始工作,系统默认做法是调用控件对应的super方法,值得一提的是:super会调用父类中的方法,而此处刚好会调到当前控件父控件所对应的方法。此时的事件传递方向刚好相反,从UIButton向底层抛,直到系统接收并做出响应,如图中虚线所示。这一来一回,我们称之为响应者链条。
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
UIView不接收触摸事件的三种情况
- 不接收用户交互
userInteractionEnabled = NO; - 隐藏
hidden = YES; - 透明
alpha = 0.0 ~ 0.01;
如果一个控件的父控件隐藏了,则子控件也将被隐藏
UIResponder内部提供了以下方法来处理事件
- UIView中的方法
继承关系:UIWindow\UIButton -> UIView -> UIResponder -> NSObject
上文中响应者链条提到过,系统接收用户点击事件后会将事件不断上抛,直到到达UIButton,那么这中间又是那些东西在运作?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
这两个不太常见的方法其实是属于UIView的。
如何找到最合适的控件来处理事件
-
流程
- 判断当前控件userInteractionEnabled、hidden、alpha这三个属性的值( 自己是否能接收触摸事件)
- 调用 pointInside: withEvent: ( 触摸点是否在自己身上)
- 从后向前遍历子控件,并调用子控件的 hitTest: withEvent: 和 pointInside: withEvent: 方法(从后往前遍历子控件,重复前面两个步骤(遍历子控件时,先取出最后添加的那个子控件(因为在最前面))
-
代码
- hitTest和point方法
/*在AppDelegate中自定义UIWindow*/
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//自定义window
self.window = [[LTYWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor redColor];
//加载storyboard
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
//加载箭头指向的控制器
UIViewController *vc = [storyboard instantiateInitialViewController];
//设置窗口的根控制器
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];
return YES;
}
/**
调用事件:当事件传递给控件的时候,就会调用控件的这个方法,去寻找最合适的view
作用:寻找最合适的view
point:当前的触摸点,point这个点的坐标系就是方法调用者
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//调用系统的做法取寻找最合适的view,返回最合适的view
UIView *fitView = [super hitTest:point withEvent:event];
NSLog(@"%s-----%@",__func__,fitView);
return fitView;
}
/**作用:判断当前这个点在不在方法调用者(控件)上*/
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return YES;
}
- 事件的产生和传递
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@",[self class]);
}
//UIApplication -> [UIWindow hitTest:wthTest:withEvent:] -> [subView hitTest:wthTest:withEvent:]
//因为所有的视图类都是继承BaseView
- (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.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger 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;
}
}
//循环结束,表示没有比自己更适合的view
return self;
}