iOS探索:UI视图之事件传递&视图响应

2,924 阅读4分钟

事件传递

事件传递的两个核心方法

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

第一个方法返回的是一个UIView,是用来寻找最终哪一个视图来响应这个事件 第二个方法是用来判断某一个点击的位置是否在视图范围内,如果在就返回YES

事件传递的流程

WX20181205-193658@2x.png

流程描述
  • 我们点击屏幕产生触摸事件,系统将这个事件加入到一个由UIApplication管理的事件队列中,UIApplication会从消息队列里取事件分发下去,首先传给UIWindow

  • 在UIWindow中就会调用hitTest:withEvent:方法去返回一个最终响应的视图

  • 在hitTest:withEvent:方法中就回去调用pointInside: withEvent:去判断当前点击的point是否在UIWindow范围内,如果是的话,就会去遍历它的子视图来查找最终响应的子视图

  • 遍历的方式是使用倒序的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都回去调用它的hitTest:withEvent:方法,可以理解为是一个递归调用

  • 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIWindow作为响应者

hitTest:withEvent:

WX20181205-201230@2x.png

流程描述
  • 首先会判断当前视图的hiden属性、是否可以交互以及透明度是否大于0.01,如果满足条件则进入下一步,否则返回nil

  • 调用pointInside: withEvent:方法来判断这个点是否在当前视图范围内,如果满足条件则进入下一步,否则返回nil

  • 然后以倒序的方式遍历它的子视图,在每个子视图中去调用hitTest:withEvent:方法,如果有一个子视图返回了一个最终的响应视图,那么就将这个视图返回给调用方;如果全部遍历完成都没有找到一个最终的响应视图,因为点击位置在当前视图范围内,就将当前视图作为最终响应视图返回

实例场景

接下来我们通过一个具体的实例来进一步的理解事件传递,例如:在一个方形按钮中点击中间的圆形区域有效,而点击四角无效

按钮图片.png

核心思想是在pointInside: withEvent:方法中修改对应的区域

代码如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha <= 0.01) {
        return nil;
    }

    //判断当前视图是否在点击范围内
    if ([self pointInside:point withEvent:event]) {
        //遍历当前对象的子视图(倒序)
        __block UIView *hit = nil;
        [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //坐标转换
            CGPoint convertPoint = [self convertPoint:point toView:obj];
            //调用子视图的hitTest方法
            hit = [obj hitTest:convertPoint withEvent:event];
            //如果找到了就停止遍历
            if (hit) *stop = YES;
        }];

        //返回当前的视图对象
        return hit?hit:self;
    }else {
        return nil;
    }
}

- (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;
    
    //判断是否在圆形区域内
    double dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    
    if (dis <= self.frame.size.width / 2) {
        return YES;
    }
    else{
        return NO;
    }
}

视图的响应者链

首先我们要知道事件传递和响应过程是相反的

如果hitTest:withEvent:找到了第一响应者initial view,但是该响应者没有处理该事件,那么事件会沿着响应者链向上传递:第一响应者 -> 父视图 -> 视图控制器,如果传递到最顶级视图还没处理事件,那么就传递给UIWindow去处理,若window对象也不处理那么就交给UIApplication处理,如果UIApplication对象还不处理,就丢弃该事件(但是并不会引起崩溃

并且在iOS中,能够响应事件的对象都是UIResponder的子类对象,UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用。

系统回调方法

// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象

响应者链流程图

WX20181205-205455@2x.png

GitHub

Demo