美文网首页
事件的传递与响应

事件的传递与响应

作者: 痴人会说梦 | 来源:发表于2018-04-04 12:32 被阅读6次

事件的产生

当我们的手指触摸屏幕,就会产生一个触摸事件,UIApplication会将该事件加入到其管理的事件队列中,队列是先进先出(FIFO)的,也符合先产生的事件优先处理的逻辑.UIApplication会取出最前面的事件,并将事件分发下去进行处理.

事件的传递

UIApplication会将事件发送给程序的主窗口,主窗口根据其视图层级结构中(图层树)找到最合适的视图来处理该事件.
所以事件的传递是从父视图到子视图的
其中涉及的主要方法为hitTest:withEvent(返回最合适的视图来处理事件),其内部会调用pointInside:withEvent:(判断触摸点是否在当前视图范围内)

主要逻辑

主窗口在它的内容视图上调用hitTest:withEvent:来找寻最合适处理触摸事件的view.
hitTest:withEvent:在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。然后再调用pointInside:withEvent:判断点击事件发生的位置是否处于当前视图范围内。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil。
如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回nil,则hitTest:withEvent:方法返回该视图自身。
不接收触摸事件的三种情况
(1)不接收用户交互 userInteractionEnabled = NO(2)隐藏 hidden = YES(3)透明 alpha = 0.0 ~ 0.01

hitTest:withEvent:底层实现

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1.判断自己能否接收触摸事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判断触摸点在不在自己范围内,point是该视图的坐标系上的点
    if (![self pointInside:point withEvent:event]) return nil;
    // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
    int count = self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        //转换为子视图的坐标位置
        CGPoint childPoint = [self convertPoint:point toView:childView];
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        if (fitView) {
            return fitView;
        }
    }
    // 没有找到比自己更合适的view
    return self;
}

每个能执行hitTest:方法的view都属于事件传递的一部分,但是,只有pointInside返回YES的view才属于响应者链条。

事件的响应

响应者:继承UIResponder的对象称之为响应者对象,能够处理touchesBegan等触摸事件,加速器事件,远程控制等。响应者链条:由很多响应者链接在一起组合起来的一个链条称之为响应者链条.

通过事件传递找到最合适的处理触摸事件的view后(就是最后一个pointInside返回YES的view,它是第一响应者),如果该view是控制器view,那么上一个响应者就是控制器。如果它不是控制器view,那么上一个响应者就是前面一个pointInside返回YES的view(其实就是它的父控件)。 最后这些所有pointInside返回YES的view加上它们的控制器、UIWindow和UIApplication共同构成响应者链条。响应者链条是从子控件到父控件的(上到下),事件的传递是(父控件到子控件)自下而上的。

应用

  • 超出父控件依然能够响应事件
  • 扩大按钮的点击范围
  • 获取view所属的控制器

超出父控件依然能够响应事件

在父视图中重写hitTest:withEvent:

//重写父视图的hitTest方法,遍历子视图,如果子视图的bounds在点击范围内就返回这个子视图,即这个子视图就能响应这个事件
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
        for (UIView *subView in self.subviews) {
            CGPoint myPoint = [subView convertPoint:point fromView:self];
            if (CGRectContainsPoint(subView.bounds, myPoint)) {
                return subView;
            }
        }
    }
    return view;
}

扩大按钮的点击范围

在需要扩大的视图中重写pointInside:withEvent:方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    //1.获取原来的bounds
    CGRect bounds = self.bounds;
    int insetOffsert = -20;
    //2.CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy)
    //以原rect为中心,再参考dx,dy,进行缩放或者放大。 +缩小,-放大,可以理解为内边距,两边都有边距,所以取1/2
    bounds = CGRectInset(bounds, 0.5 * insetOffsert, 0.5 *insetOffsert);
    
    //3.判断扩大后的区域是否包含点击坐标
    return CGRectContainsPoint(bounds, point);
}

获取view所属的控制器

//可以获取到父容器的控制器的方法
- (UIViewController *)View:(UIView *)view{
    UIResponder *responder = view;
    //循环获取下一个响应者,直到响应者是一个UIViewController类的一个对象为止,然后返回该对象.
    while ((responder = [responder nextResponder])) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)responder;
        }
    }
    return nil;
}

相关文章

网友评论

      本文标题:事件的传递与响应

      本文链接:https://www.haomeiwen.com/subject/osskhftx.html