事件处理

这篇文章主要讲以下两个方法,如果你对响应者链条还不够熟悉,请接着往下看,如果足够熟悉请跳到正文。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event ;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
响应者链条

在这里演示一个最简单的按钮响应

事件处理_第1张图片
响应者链条示意图.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;

通过上文分析,不难看出这两个方法可以实现某些特殊需求,如局部拦截控件的响应事件,实现子控件在父控件之外仍可响应等等。

你可能感兴趣的:(事件处理)