问题
iOS
中View
的事件到底是怎么传递
和响应
的?- 为什么
父View
关闭了事件响应时,子View
就无法响应事件? 底层原理? - 如何扩大
Button
的点击范围 ? - 如何让
父View
和子View
同时响应同一事件?默认情况下只会响应子View
的事件回调。 - 为什么
子View
关闭了事件,但其父View
开启事件的情况下,点击子View
时,父View
可以正常响应事件? - 为什么 子View 是 UIView时,如果没有添加手势,点击子 View时,会由其父View来响应,而 子View 是 UIControl 时,子View 没有添加手势,一样不会由 父View 来响应
- …
分析
iOS
的事件可以分为三种
Touch Events(触摸事件)
Motion Events(运动事件,比如重力感应和摇一摇等)
Remote Events(远程事件,比如用耳机上得按键来控制手机)
下面主要讲解
Touch Events(触摸事件)
Touch Events
事件的整个过程可以分为传递
和响应
2 个阶段,
- 传递: 是当我们触摸屏幕时,为我们找出最适合的
View
,- 响应: 当我们找出最适合的
View
后,此时只是找到了最合适的View
,但未必 此View
可以响应此事件,所以需要继续找出能响应此事件的View
。
传递过程
每当手指接触屏幕,操作系统会把事件传递给当前的
App
, 在UIApplication
接收到手指的事件之后,就会去调用`UIWindow的hitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用其 subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,便可以确定最合适的 View。
- 引用几张图来说明
- 图片表示的内容可用下面的话描述来自这里
递归是向界面的根节点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
方法的内部实现
1 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event |
响应过程
- 个人对响应过程的理解如下:
当我们知道最合适的 View 后,事件会 由上向下【子view -> 父view,控制器view -> 控制器】来找出合适响应事件的 View,来响应相关的事件。如果当前的 View 有添加手势,那么直接响应相应的事件,不会继续向下寻找了,如果没有手势事件,那么会看其是否实现了如下的方法:
1 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; |
如果有实现那么就由此 View 响应,如果没有实现,那么就会传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器】, 这里我们可以做一个简单的验证,在默认情况下 UIView 是不响应事件的,UIControl 就算没有添加手势一样的会由他来响应, 这里可以使用 runtime查看 UIView 和 UIControl 的方法列表, 或 查看 UIKit 源码 可知, UIView 没有实现如上的 touchesBegan
方法,而 UIControl
是实现了如上的相关方法,所以验证了刚才的 UIView 不响应,和 UIControl 的响应。一旦找到最合适响应的View就结束, 在执行响应的绑定的事件,如果没有就抛弃此事件。
我的验证
- 首先处理添加了手势时,其便可以处理事件。
- 我们创建一个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 是没有实现如下的方法的,使用在没有添加手势时他不响应事件。
- (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 的源码便可清除看到。
所以个人理解:
- 事件在传递时和上面的 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 的事件响应等。
参考资料
- http://smnh.me/hit-testing-in-ios/
- https://zhoon.github.io/ios/2015/04/12/ios-event.html
- http://southpeak.github.io/blog/2015/03/07/uiresponder/
- https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollView_Class/index.html#//apple_ref/doc/uid/TP40006922
- https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/event_delivery_responder_chain/event_delivery_responder_chain.html
- https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2
- https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2