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

问题

  • iOSView 的事件究竟是怎么传递响应的?
  • 为何 父View 关闭了事件响应时,子View 就没法响应事件? 底层原理?
  • 如何扩大 Button 的点击范围 ?
  • 如何让 父View子View 同时响应同一事件?默认状况下只会响应 子View 的事件回调。
  • 为何 子View 关闭了事件,但其 父View 开启事件的状况下,点击 子View 时,父View 能够正常响应事件?
  • 为何 子View 是 UIView时,若是没有添加手势,点击子 View时,会由其父View来响应,而 子View 是 UIControl 时,子View 没有添加手势,同样不会由 父View 来响应
  • ...

分析

iOS 的事件能够分为三种html

  • Touch Events(触摸事件)
  • Motion Events(运动事件,好比重力感应和摇一摇等)
  • Remote Events(远程事件,好比用耳机上得按键来控制手机)

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

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

传递过程

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

  • 引用几张图来讲明

递归是向界面的根节点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也不会被遍历了。github

  • 下面是 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 方法的内部实现
- (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;
    }
}

复制代码

上面的代码来自这里bash

响应过程

  • 我的对响应过程的理解以下:

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

- (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就结束, 在执行响应的绑定的事件,若是没有就抛弃此事件。iview

个人验证ide

  • 首先处理添加了手势时,其即可以处理事件。
  • 咱们建立一个view A 在 A 中添加一个 view B, 若是咱们给 A 加了手势,B没有加手势,
  • 咱们在点击 B 时,会响应 A 的事件,很是正常的状况,那么它是怎么判断 B 是否能够处理的呢?
  • 咱们如今给 B 加一个手势,那么一样的操做时会触发 B 的手势,如今咱们 给 B 增长一个方法,
@implementation BMSonView
	- (NSArray<UIGestureRecognizer *> *)gestureRecognizers {
	    NSLog(@"%@", self);
	    return @[];
	}
复制代码

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

- (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 的源码即可清除看到。spa

因此我的理解:

  • 事件在传递时和上面的 hit 方法有关,一层层向上传递,【窗口---> view】由其相应的 view 中具体的实现来肯定谁才是是最合适响应的view

  • 在响应时,又上向下找出第一个能处理的view来处理事件,[view ---> 窗口],在寻找刚过程当中 会判断是否增长了手势 和是否实现了如上的 触摸方法。

  • 至于 UIControl Button 的特殊事件相应,我的认为是在其m文件中实现了上面的4个方法,在这4个方法中作了相关的处理,这里能够从 UIControl 代码中在知道一些内容。

  • 因此若是想本身实现 UIControl Button ,首先要想办法处理好上面的4个方法。

  • 图以下

问题解答

  • iOS 中 View 的事件究竟是怎么传递和响应的?

如上所描。

  • 为何 父View 关闭了事件响应时,子View 就没法响应事件?

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

  • 如何扩大 Button 的点击范围?

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

  • 如何让 父View 和 子View 同时响应同一事件?

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

  • 为何子View 关闭了事件,但其 父View 开启事件的状况下,点击 子View 时,父View 能够响应事件?

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

  • 为何 子View 是 UIView时,若是没有添加手势,点击子 View时,会由其父View来响应,而 子View 是 UIControl 时,子View 没有添加手势,同样不会由 父View 来响应

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

  • 这里其实还有许多内容待挖掘,好比:scrollview 的事件响应等。

参考资料

声明

相关文章
相关标签/搜索