iOS
中 View
的事件究竟是怎么传递
和响应
的?父View
关闭了事件响应时,子View
就没法响应事件? 底层原理?Button
的点击范围 ?父View
和 子View
同时响应同一事件?默认状况下只会响应 子View
的事件回调。子View
关闭了事件,但其 父View
开启事件的状况下,点击 子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
@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个方法。
图以下
如上所描。
由于在事件传递的时,先到父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 没有实现。