iOS 的那些事儿

ios事件传递过程(hitTest方法的原理)

2021-10-17  本文已影响0人  我家冰箱养企鹅

事件传递执行步骤

当屏幕上发生触摸事件时,系统会先发送事件给应用程序的主窗口,主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,找到最合适的视图后会调用它的touches方法。
首先,以下三种情况下UIView不接受触摸事件:
1.userInteractionEnabled = NO
2.hidden = YES
3.alpha = 0-0.01
当父视图不接受触摸事件时,它的子视图的触摸事件也会失效。(UIImageView在默认情况下userInteractionEnabled = NO)
其次,如何找到最合适的视图来处理触摸事件?

图1 当点击黄色view时,触摸事件的传递是从父控件到子控件的过程,即:
UIApplication->UIWindow->白色->橙色->蓝色->黄色, 传递的判断标准是:
1.自己是否能接收触事件?
2.触摸点是否在自己身上?
3.从后往前遍历子控件并重复上面两个步骤。
4.如果没有返回合适的子控件则由自身来处理触摸事件
上述过程是通过hitTest函数来实现的,作用是寻找最合适的view, 当一个事件传递给当前view就会调用这个方法,返回值就是最合适的view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //输出调用者并执行系统默认方法
    NSLog(@"%s",__func__);
    return [super hitTest:point withEvent:event];
}
例如当点击白色空白部分时,事件会在第三步从UIWindow的hitTest中跳出来执行白色view的hitTest方法,并遍历自己的子视图(绿色和橙色)执行它们各自的hitTest方法判断它们是否可以响应或者有更合适的响应者,由于它们都不是,所以返回白色自身作为最合适的响应者,输出结果如下: 图2

所以,如果希望不论点击屏幕哪个地方都返回绿色view,则只需在绿色view的hitTest方法中返回self,而如果和绿色view处于同一层级橙色view的hitTest方法中也返回self,则点击屏幕的响应者是橙色view,因为橙色view在绿色view后面添加,即第三步。

第二步判断触摸点是否在自己身上是通过pointInside方法来实现的,它是在hitTest方法中调用的,需要注意的是,它的参数point应当是触摸点相对于自身的相对坐标,返回值代表触摸点是否在自己身上,如果直接返回yes意味着不管点击什么位置都在自身view范围内(如果会调用到自身的hitTest方法的话),触摸事件的响应者即为自身或者自身的子视图, 如果直接返回no意味着自身不响应触摸事件。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {

}

第三步中在遍历过程中需要把相对于当前view的point转化为相对于它的子视图的坐标,并调用子视图的hitTest方法来重复操作,转化坐标是通过convert方法来计算的,下面把它和与它相关的几个方法一起列举出来:

//自身内的点point相对于view的坐标, 若view为空表示相对主窗口的坐标
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
//view中的点point相对于自身的坐标
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
// 自身内的子视图相对于view的CGRect
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
//view中的子视图相对于自身的CGRect
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

综合以上,hitTest方法的完整代码如下。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //1.判断自己能否接受事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
        return nil;
    }
    //2.判断触摸点是否在自身范围内
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    //3.从后往前遍历寻找最合适的view
    for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
        UIView* view = self.subviews[i];
        CGPoint p = [self convertPoint:point toView:view];
        
        UIView* betView = [view hitTest:p withEvent:event];
        if (betView) {
            return betView;
        }
    }
    //4.自身就是最合适的view
    return self;
}

案例

1.如果一个UIView中有很多横向排放的子视图并且间距相同,如果想要扩大子视图的点击范围,即如果点击了子视图之间的空隙让距离点击最近的子视图响应点击,此时可以重写UIView的hitTest方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.hidden || self.alpha < 0.01 || !self.userInteractionEnabled) {
          return nil;
    }
    UIView *hitView = nil;        //返回的最合适处理事件的视图
    if ([self pointInside:point withEvent:event]) {
        CGFloat margin = _itemSpace/2;//itemSpace表示子视图之间的空隙
        //遍历每个子视图决定最终哪个视图来响应此事件
        [self.subViews enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            CGRect touchRect = CGRectMake(obj.frame.origin.x - margin, 0, obj.frame.size.width + margin*2, self.frame.size.height);
            //如果点击的点在此button的范围内就让此button响应事件
            if (CGRectContainsPoint(touchRect, point)) {
                hitView = obj;
                *stop = YES;
            }
        }];
    }
    return hitView;

2.如果一个UIView中有一个子视图的位置超出了自身的范围,但是仍然希望子视图可以处理点击事件,重写方法如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pt = [self convertPoint:point toView:self.childView];
    if ([self.childView pointInside:pt withEvent:event]) {
        return self.childView;
    }
    else{
        return [super hitTest:point withEvent:event];
    }
}
上一篇 下一篇

猜你喜欢

热点阅读