googleiOS面试&笔试IOS个人开发

浅谈 iOS 事件的传递和响应过程

2018-08-01  本文已影响3人  liangdahong

问题

分析

iOS 的事件可以分为三种

下面主要讲解 Touch Events(触摸事件) Touch Events事件的整个过程可以分为 传递响应 2 个阶段,

  • 传递: 是当我们触摸屏幕时,为我们找出最适合的 View
  • 响应: 当我们找出最适合的 View 后,此时只是找到了最合适的 View,但未必 此 View 可以响应此事件,所以需要继续找出能响应此事件的 View

传递过程

每当手指接触屏幕,操作系统会把事件传递给当前的 App, 在 UIApplication接收到手指的事件之后,就会去调用`UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用其 subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,便可以确定最合适的 View。

image image image

递归是向界面的根节点UIWindow发送hitTest:withEvent:消息开始的,从这个消息返回的是一个UIView,也就是手指当前位置最前面的那个 hittest view。 当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问)。如果当前的point没有在view上面,那么这个view的subview也就不会被遍历了。当事件遍历到了view B.1,发现point在view B.1里面,并且view B.1没有subview,那么他就是我们要找的hittest view了,找到之后就会一路返回直到根节点,而view B之后的view A也不会被遍历了。

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {
        return nil;
    } else {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
            if (hitView) {
                return hitView;
            }
        }
        return self;
    }
}

上面的代码来自这里

响应过程

当我们知道最合适的 View 后,事件会 由上向下【子view -> 父view,控制器view -> 控制器】来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了如下的方法:

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

如果有实现那么就由此 View 响应,如果没有实现,那么就会传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器】, 这里我们可以做一个简单的验证,在默认情况下 UIView 是不响应事件的,UIControl 就算没有添加手势一样的会由他来响应, 这里可以使用 runtime查看 UIView 和 UIControl 的方法列表, 或 查看 UIKit 源码 可知, UIView 没有实现如上的 touchesBegan方法,而 UIControl 是实现了如上的相关方法,所以验证了刚才的 UIView 不响应,和 UIControl 的响应。一旦找到最合适响应的View就结束, 在执行响应的绑定的事件,如果没有就抛弃此事件。

我的验证

    @implementation BMSonView
    - (NSArray<UIGestureRecognizer *> *)gestureRecognizers {
        NSLog(@"%@", self);
        return @[];
    }

手势返回 @[],此时点击 B 只会触发 A 的事件,由此可以说明在判断 view 是否可以处理事件实现是判断 gestureRecognizers 即是否添加了手势,上面提到了还有判断如下的方法是否实现了,默认情况下 UIView 是没有实现如下的方法的,使用在没有添加手势时他不响应事件。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

如果我们手动实现了如上的方法时,就算没有给 B 添加手势,点击 B 时, 事件不会响应 A 的方法,会到上面的方法中。从 UIControl 的源码便可清除看到。

所以个人理解:

问题解答

如上所描。

因为在事件传递的时,先到父view,当父view无法响应事,直接就跳过了遍历其子view,故只要父类关闭了事件,子 view 就已经没有机会响应事件了。

扩大点击范围,无非就是想本来没有点击 btn 但想让 btn 响应事件,那么可以在 hitTest 方法中做适当的操作,当满足xxx条件时,强行返回 btn 来达到最佳点击范围的效果,相关的实现可以自行 Google ,有一些较优雅而简洁的方式。

父View 和 子View同时响应同一事件,默认当点击子view时,如果ziview可以处理事件,那么其他父view 是不会响应的,但是在 父view 传到 子view 时我们在 hitTest 方法中是清楚知道的,使用可以在这里做相关的操作便实现了子view 和父view 同时响应事件的效果。

子view关闭了事件,事件的传递是 父view 到子view,在 父view时,父view可以响应,那么会继续访问其 子view是否可以响应,如果此时子view不可以响应,那么他会直接返回 父view,所以 子View 关闭了事件 父View 正常执行事件是必然的。

这个问题可以见上面的寻找可以响应的 view 来解决,UIControl 实现了如上的 4 大方法,而 UIView 没有实现。

参考资料

声明

上一篇 下一篇

猜你喜欢

热点阅读