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

事件的传递与响应

作者: 痴人会说梦 | 来源:发表于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