最近又看了遍苹果的官方文档,对事件响应链中的 hit-test view 又多了些理解,个人觉的官方文档对这块讲的非常简单,很多东西都是点到为止,hit-test view 的知识在项目的任何地方都用到了,但自己反而感知不到,接下来我会给大家讲 hit-test view 的项目中能解决痛点的三个应用 。
什么叫 hit-test view?文档说:,我的理解是:当你点击了屏幕上的某个 view,这个动作由硬件层传导到操作系统,然后又从底层封装成一个事件(Event)顺着 view 的层级往上传导,一直要找到含有这个点击点且层级最高(文档说是最低,我理解是逻辑上最靠近手指)的 view 来响应事件,这个 view 就是 hit-test view。
文档中说,决定谁 hit-test view 是通过不断递归调用 view 中的 方法和 方法来实现的,文段中的这段话太好理解,于是我仿照官方文档中这张图做了个 Demo ->
重载图中 view 的方法添加相应的 log 便于观察:
- //in every view .m overide those methods
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- NSLog(@"进入A_View---hitTest withEvent ---");
- UIView * view = [super hitTest: point withEvent: event];
- NSLog(@"离开A_View--- hitTest withEvent ---hitTestView:%@", view);
- return view;
- } - (BOOL) pointInside: (CGPoint) point withEvent: (nullable UIEvent * ) event {
- NSLog(@"A_view--- pointInside withEvent ---");
- BOOL isInside = [super pointInside: point withEvent: event];
- NSLog(@"A_view--- pointInside withEvent --- isInside:%d", isInside);
- return isInside;
- } - (void) touchesBegan: (NSSet * ) touches withEvent: (UIEvent * ) event {
- NSLog(@"A_touchesBegan");
- } - (void) touchesMoved: (NSSet < UITouch * >*) touches withEvent: (nullable UIEvent * ) event {
- NSLog(@"A_touchesMoved");
- } - (void) touchesEnded: (NSSet < UITouch * >*) touches withEvent: (nullable UIEvent * ) event {
- NSLog(@"A_touchesEnded");
- }
点击图中 View_D,看下会发生什么
一是发现这些方法都是发生在找到 hit-test view 之后,因为 touch 事件是针对能响应事件的确定的某个 view,比如你手指划出了 scrollview 的范围,只要你不松手继续滑动,scrollview 依然会响应滑动事件继续滚动;二是寻找 hit-test view 的事件链传导了两遍,而且两次的调用堆栈是不同的,这点我有点搞不懂,为啥需要两遍,查阅了很多资料也不知道原因,发现真机和模拟器以及不同的系统版本之间还会有些区别(此为真机 iOS9),大家可以下载我的进行测试与研究。
把这个寻找的逻辑换成代码如下:
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
- return nil;
- }
- if ([self pointInside: point withEvent: event]) {
- for (UIView * subview in [self.subviews reverseObjectEnumerator]) {
- CGPoint convertedPoint = [subview convertPoint: point fromView: self];
- UIView * hitTestView = [subview hitTest: convertedPoint withEvent: event];
- if (hitTestView) {
- return hitTestView;
- }
- }
- return self;
- }
- return nil;
- }
如果有某个 view 的两个子 view 位置重叠,根据文档中说的 那最高层(逻辑最靠近手指的)view 是 view subviews 数组的最后一个元素,只要寻找是从数组的第一个元素开始遍历,hit-test view 的逻辑依然是有效的。
找到 hit-test view 后,它会有最高的优先权去响应逐级传递上来的 Event,如它不能响应就会传递给它的 superview,依此类推,一直传递到 UIApplication 都无响应者,这个 Event 就会被系统丢弃了。
相信大家都遇到小图 button 点击热区太小问题,之前我是用 UIButton 的方法来设置图片解决,但是调起坐标就坑了,得各种计算不说,写出的代码还很难看不便于维护,如果我们用用的知识你就能轻松地解决这个问题。
重载 UIButton 的方法,让 Point 即使落在 Button 的 Frame 外围也返回 YES。
- //in custom button .m
- //overide this method
- - (BOOL) pointInside: (CGPoint) point withEvent: (nullable UIEvent * ) event {
- return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
- }
- CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
- CGRect hitTestingBounds = bounds;
- if (minimumHitTestWidth > bounds.size.width) {
- hitTestingBounds.size.width = minimumHitTestWidth;
- hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width) / 2;
- }
- if (minimumHitTestHeight > bounds.size.height) {
- hitTestingBounds.size.height = minimumHitTestHeight;
- hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height) / 2;
- }
- return hitTestingBounds;
- }
项目中常常遇到 button 已经超出了父 view 的范围但仍需可点击的情况,比如自定义 Tabbar 中间的大按钮,如下图闲鱼的 app,点击超出 Tabbar bounds 的区域也需要响应,此时重载父 view 的方法,去掉点击必须在父 view 内的判断,然后子 view 就能成为 hit-test view 用于响应事件了。
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
- return nil;
- }
- /**
- * 此注释掉的方法用来判断点击是否在父View Bounds内,
- * 如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回
- */
- // if ([self pointInside:point withEvent:event]) {
- for (UIView * subview in [self.subviews reverseObjectEnumerator]) {
- CGPoint convertedPoint = [subview convertPoint: point fromView: self];
- UIView * hitTestView = [subview hitTest: convertedPoint withEvent: event];
- if (hitTestView) {
- return hitTestView;
- }
- }
- return self;
- // }
- return nil;
- }
这是 app store 应用的 app 封面预览功能
上图的交互常常见于很多海报、封面展示的 app,实现这个交互的方法有很多,但选择用 scrollView 来横向滑动来做是最简单的,让 scrollview.pageEnabel = YES,就有了翻页的感觉,但这样 scoreView 的实际可滑动区域就只有一张照片那么宽,如果想让边侧留出的距离 (蓝色框部分) 响应滑动事件的话应该怎么办呢?这个时候又可以用到 hit-test view 的知识了,在 scrollview 的父 view 中把蓝色部分的事件都传递给 scrollView 就可以了,具体看下面代码:
- //in scrollView.superView .m
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- UIView * hitTestView = [super hitTest: point withEvent: event];
- if (hitTestView) {
- hitTestView = self.scrollView;
- }
- return hitTestView;
- }
事件响应链是 UI 层一个非常重要的概念,想做出非常棒的交互和动画,必须对其有一个深入的理解。我列举的只是我在开发中遇到的一些问题,如果有其他的对事件响应链的应用希望大家和我一起交流探讨。
来源: http://www.bubuko.com/infodetail-1862812.html