UI视图-UI事件传递及视图响应链

UIView和CALayer
@property(nonatomic,readonly,strong)         CALayer  *layer;
@property(nullable, nonatomic,copy)          UIColor  *backgroundColor;
UIView的两个属性
layer为CALayer类型
backgroundColor为CALayer同名属性方法的包装
实际上,UIView的显示部分是由CALayer的contents来决定的,它对应的叫做backing store,实际上是bit map类型的位图,最终我们显示在屏幕上的控件其实都是位图
  1. UIView为CALayer提供显示的内容,同时它负责处理触摸等事件,包括参与视图的事件响应链
  2. CALayer只是负责内容contents的显示
    为什么UIView只负责事件传递及视图响应链的机制流程,而CALayer只负责显示
这符合了系统设计原则:单一职责原则
视图的事件传递及视图响应链

当点击了C2视图的位置,系统是怎样找到响应视图的


事件传递流程
UIView的两个方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event//返回响应视图
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event//判断点击位置是否在视图内

当点击了屏幕的某一个位置,点击事件会传递给UIApplication, 再传给UIWindow(继承与UIView)
在UIWindow里面会判断hitTest,来返回最终的响应视图
这个内部做法是
在hitTest内部会先调用pointInside,来判断当前点击的点是否在UIWindow范围内,如果是的话,就会遍历UIWindow的子视图来查找最终响应事件的视图,遍历的方式是倒叙遍历,意思是最后被添加到UIWindow上的视图,最优先被遍历,在遍历的每一个UIView中,内部都会去调用他们对应的hitTest方法
这其实是一个递归遍历:UIWindow会倒序遍历它全部子视图,他的子视图,又会倒序遍历自己的子视图,遍历时都是调用hitTest方法,而hitTest内部会内部调用sub子视图的所有子视图的hitTest方法,最终返回响应视图hit,如果没有hit,那么如果点击在UIWindow范围内,则返回UIWindow


hitTest方法的系统内部实现
实现一个方形Button内指定圆形区域接收事件响应(Demo百度网盘)

通过上面知道,我们应该重写button的hitTest和pointInside两个方法

1.首先判断是否可交互,是否隐藏以及是否透明,若不可交互||隐藏||透明度小于0.01,则这个button不响应点击事件
2.判断点击的位置是否在当前视图内,如果是,我们需要子视图遍历,如果不是,返回nil
3.重写pointInside方法,判断点击位置是否在当前视图内,首先从point中取出点击点的x和y坐标,然后用x2,y2来表示方形button的中心,通过平方差公式,计算当前点击的点距离中心点的距离,假如这个距离是在以当前控件中心为圆心,当前控件宽度为半径的圆内,就响应点击事件,如果不在圆内,则不响应点击事件,这样就实现了只有圆形内可以实现事件响应
- (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));
    // 67.923
    if (dis <= self.frame.size.width / 2) {
        return YES;
    }
    else{
        return NO;
    }
}
4.如果点击位置在圆形区域之内,就要
遍历当前对象子视图,通过系统提供的enumerateObjectsWithOptions方法,指定遍历参数为倒叙遍历,来遍历CustomButton所有的子视图,在子视图上,将点击的点做视图转换,转换到子视图的视图坐标系统上,再调用子视图的hitTest方法,
如果某个子视图返回了hitView,那么就说明找到了接收响应事件的视图,停止遍历,把hit返回给调用方,如果遍历了全部子视图,或者当前控件没有子视图,hit就为nil,但因为点击的范围位于当前视图范围内,那么我们就将当前视图作为最终的事件响应的视图,返回给调用方
- (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 vonvertPoint = [self convertPoint:point toView:obj];
            //调用子视图的hittest方法
            hit = [obj hitTest:vonvertPoint withEvent:event];
            // 如果找到了接受事件的对象,则停止遍历
            if (hit) {
                *stop = YES;
            }
        }];

        if (hit) {
            return hit;
        }
        else{
            return self;
        }
    }
    else{
        return nil;
    }
}
关于视图的响应流程

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIViewController
UIView



上图为视图响应链,从UILabel一直到UIApplicationDelegate

UIResponder中提供了以下对象方法来处理视图事件
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
image.png

如上图,若我们点击的是C2视图的圆心部分,最先由C2来接收这个事件,若C2不处理,会把事件传递给C2的下一个响应者即它的父视图B2,若B2也不响应,就会传递给父视图A,若A仍不响应,会沿着视图链一直向上传递直到UIApplicationDelegate,若仍不处理,则这个事件会被忽略

总结
事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event]; }

你可能感兴趣的:(UI视图-UI事件传递及视图响应链)