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

事件传递

事件传递的两个核心方法

- (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,是用来寻找最终哪个视图来响应这个事件 第二个方法是用来判断某一个点击的位置是否在视图范围内,若是在就返回YESgit

事件传递的流程

WX20181205-193658@2x.png

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

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

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

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

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

hitTest:withEvent:

WX20181205-201230@2x.png

流程描述
  • 首先会判断当前视图的hiden属性、是否能够交互以及透明度是否大于0.01,若是知足条件则进入下一步,不然返回nilcode

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

  • 而后以倒序的方式遍历它的子视图,在每一个子视图中去调用hitTest:withEvent:方法,若是有一个子视图返回了一个最终的响应视图,那么就将这个视图返回给调用方;若是所有遍历完成都没有找到一个最终的响应视图,由于点击位置在当前视图范围内,就将当前视图做为最终响应视图返回对象

实例场景

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

按钮图片.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

相关文章
相关标签/搜索