事件处理
这篇文章主要讲以下两个方法,如果你对响应者链条还不够熟悉,请接着往下看,如果足够熟悉请跳到正文。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
响应者链条
在这里演示一个最简单的按钮响应
响应者链条示意图.png在主窗口控制器的view上添加一个按钮,当用户点击按钮时,系统究竟做了哪些事情?首先我们要知道UIResponder,只有继承自UIResponder的对象才可以处理事件。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
这几个很常用的方法都是从UIResponder继承过来的。
回到响应者链条。当用户点击按钮,系统最先响应这个点击事件,由AppDelegate接收,然后不断上抛直到到达UIButton,如图中实线所示。当UIButton接收到这个事件,UIResponder相应的方法开始工作,系统默认做法是调用控件对应的super方法,值得一提的是:super会调用父类中的方法,而此处刚好会调到当前控件父控件所对应的方法
。此时的事件传递方向刚好相反,从UIButton向底层抛,直到系统接收并做出响应,如图中虚线所示。这一来一回,我们称之为响应者链条
。
UIView中的方法
首先来回顾一下继承关系:UIWindow\UIButton -> UIView -> UIResponder -> NSObject
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
这两个不太常见的方法其实是属于UIView的。
上文中响应者链条提到过,系统接收用户点击事件后会将事件不断上抛,直到到达UIButton,那么这中间又是那些东西在运作?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
这个方法很好理解,event事件在point点是否响应。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;
下面重点说说这个方法究竟做了些什么。
- 文字
1.判断当前控件userInteractionEnabled、hidden、alpha这三个属性的值
2.调用 pointInside: withEvent: 方法
3.从后向前遍历子控件,并调用子控件的 hitTest: withEvent: 和 pointInside: withEvent: 方法
- 代码
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
int count = (int)self.subviews.count - 1;
for (int i = count; i >= 0 ; i--) {
UIView *view = self.subviews[i];
CGPoint p = [view convertPoint:point fromView:self];
if ([view pointInside:p withEvent:event]) {
return [view hitTest:p withEvent:event];
break;
}
}
return self;
}
先判断当前控件能否接收事件,如果不能返回空,如果能再判断当前点能否响应事件,如果不能返回空,如果能从后向前遍历子控件,判断当前子控件当前点能否响应事件,如果不能继续判断下一个子控件直到子控件遍历完成(如果所有子控件都不满足条件,那么当前控件就是响应这个事件最合适的控件),如果能那么返回当前子控件的hitTest方法并跳出循环,进入子控件hitTest重复当前操作。
再解释一下为什么要从后向前遍历子控件。这个问题可以从用户的角度思考,通常情况用户点击app某个位置,最好是展现在用户最上层的控件做出响应,而非底层(不排除某些特殊需求)。所以苹果选择倒序遍历,而不是顺序。
- 总结
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
通过上文分析,不难看出这两个方法可以实现某些特殊需求,如局部拦截控件的响应事件,实现子控件在父控件之外仍可响应等等。