最近又看了遍苹果的官方文档《Event Handling Guide for iOS》,对事件响应链中的hit-test view 又多了些理解,个人觉的官方文档对这块讲的非常简单,很多东西都是点到为止,hit-test view的知识在项目的任何地方都用到了,但自己反而感知不到,接下来我会给大家讲hit-test view的项目中能解决痛点的三个应用 。

什么叫 hit-test view?文档说:The lowest view in the view hierarchy that contains the touch point becomes the hit-test view,我的理解是:当你点击了屏幕上的某个view,这个动作由硬件层传导到操作系统,然后又从底层封装成一个事件(Event)顺着view的层级往上传导,一直要找到含有这个点击点层级最高(文档说是最低,我理解是逻辑上最靠近手指)的view来响应事件,这个view就是hit-test view

文档中说,决定谁hit-test view是通过不断递归调用view中的 - (UIView *)hitTest: withEvent: 方法和 -(BOOL)pointInside: withEvent: 方法来实现的,文段中的这段话太好理解,于是我仿照官方文档中这张图做了个Demo -> Github地址

apple doucument pic

重载图中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,看下会发生什么

View_D log

一是发现touchesBegan、touchesMoved、touchesEnded这些方法都是发生在找到hit-test view之后,因为touch事件是针对能响应事件的确定的某个view,比如你手指划出了scrollview的范围,只要你不松手继续滑动,scrollview依然会响应滑动事件继续滚动;二是寻找hit-test view的事件链传导了两遍,而且两次的调用堆栈是不同的,这点我有点搞不懂,为啥需要两遍,查阅了很多资料也不知道原因,发现真机和模拟器以及不同的系统版本之间还会有些区别(此为真机iOS9),大家可以下载我的Demo进行测试与研究。

把这个寻找的逻辑换成代码如下:

- (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 Programming Guide for iOS文档中说的 Visually, the content of a subview obscures all or part of the content of its parent view. If the subview is totally opaque, then the area occupied by the subview completely obscures the corresponding area of the parent. If the subview is partially transparent, the content from the two views is blended together prior to being displayed on the screen. Each superview stores its subviews in an ordered array and the order in that array also affects the visibility of each subview. If two sibling subviews overlap each other, the one that was added last (or was moved to the end of the subview array) appears on top of the other. 那最高层(逻辑最靠近手指的)view是view subviews数组的最后一个元素,只要寻找是从数组的第一个元素开始遍历,hit-test view的逻辑依然是有效的。

找到hit-test view后,它会有最高的优先权去响应逐级传递上来的Event,如它不能响应就会传递给它的superview,依此类推,一直传递到UIApplication都无响应者,这个Event就会被系统丢弃了。

Hit-test view的应用举例:

1、扩大UIButton的响应热区

相信大家都遇到小图button点击热区太小问题,之前我是用UIButton的setImage方法来设置图片解决,但是调起坐标就坑了,得各种计算不说,写出的代码还很难看不便于维护,如果我们用用hit-test view的知识你就能轻松地解决这个问题。

重载UIButton的-(BOOL)pointInside: withEvent:方法,让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;
}

2、子view超出了父view的bounds响应事件

项目中常常遇到button已经超出了父view的范围但仍需可点击的情况,比如自定义Tabbar中间的大按钮,如下图闲鱼的app,点击超出Tabbar bounds的区域也需要响应,此时重载父view的-(UIView *)hitTest: withEvent:方法,去掉点击必须在父view内的判断,然后子view就能成为 hit-test view用于响应事件了。

xiansyu
- (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;
}

3、ScrollView page滑动

这是app store 应用的app封面预览功能

scrollview page

上图的交互常常见于很多海报、封面展示的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.jianshu.com/p/d8512dff2b3e

iOS Hit-Test应用的更多相关文章

  1. Flipper & React Native

    Flipper & React Native Flipper Flipper是一款用于调试移动应用程序的出色开发人员工具,在Android和iOS社区中颇受欢迎. Flipper is a g ...

  2. iOS事件传递->处理->响应

    前言: 按照时间顺序,事件的生命周期是这样的: 事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view.寻找最合适的view的底层实现.拦截事件的处理)->找到最合适的view后 ...

  3. 一篇对iOS音频比较完善的文章

    转自:http://www.cnblogs.com/iOS-mt/p/4268532.html 感谢作者:梦想通 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也 ...

  4. 史上最详细的iOS之事件的传递和响应机制

    前言: 按照时间顺序,事件的生命周期是这样的: 事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view.寻找最合适的view的底层实现.拦截事件的处理)->找到最合适的view后 ...

  5. iOS项目开发中的知识点与问题收集整理②(Part 二)

    1.点击UIButton 无法产生触摸事件    如果在UIImageView中添加了一个按钮,你会发现在默认情况下这个按钮是无法被点击的,需要设置UIImageView的userInteractio ...

  6. 真机远程调试 ( IOS Android 以及微信,weex)

    1.以前cordova远程调试,Android的直接连接USB后,用chrome打开chrome://inspect网址 IOS的打开Safari的developer下. 这是因为cordova的we ...

  7. IOS Core Animation Advanced Techniques的学习笔记(一)

    转载. Book Description Publication Date: August 12, 2013 Core Animation is the technology underlying A ...

  8. iOS 动画

    图层树.寄宿图以及图层几何学(一)图层的树状结构 技术交流新QQ群:414971585 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命 ...

  9. iOS 动画学习

    图层树.寄宿图以及图层几何学(一)图层的树状结构 技术交流新QQ群:414971585 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命 ...

  10. Tomcat单向Https验证搭建,亲自实现与主流浏览器、Android/iOS移动客户端安全通信

    众所周知,iOS9已经开始在联网方面默认强制使用Https替换原来的Http请求了,虽然Http和Https各有各的优势,但是总得来说,到了现在这个安全的信息时代,开发者已经离不开Https了. 网上 ...

随机推荐

  1. 用Python生成测试数据

    转载自:http://blog.csdn.net/caz28/article/details/45972109 有时我们需要大量的数据,一般编个程序生成一堆随机数据,Python很适合文本处理,干这个 ...

  2. Markdown编辑器:Typora

    现在网上的Markdown有很多,各有各的特色,同时也有各自的缺点.比如有的传图片比较麻烦,有的导出pdf需要付费,有的数学公式编辑比较麻烦等等...... 而最近接触到Typora,发现这真是一款良 ...

  3. [转载]jquery版小型婚礼(可动态添加祝福语)

    原文链接:http://www.cnblogs.com/tattoo/p/3788019.html 前两天在网上不小心看到“js许愿墙”这几个字,我的神经就全部被调动了.然后就开始我 的百度生涯,一直 ...

  4. PCA、ZCA白化

    白化是一种重要的预处理过程,其目的就是降低输入数据的冗余性,使得经过白化处理的输入数据具有如下性质:(i)特征之间相关性较低:(ii)所有特征具有相同的方差. 白化又分为PCA白化和ZCA白化,在数据 ...

  5. C 语言Struct 实现运行类型识别 RTTI

    通过RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型.c++通过下面两个操作符提供RTTI. (1)typeid:返回指针或引用所指对象的实际类型.    (2)dynamic_cast: ...

  6. 某互联网后台自动化组合测试框架RF+Sikuli+Python脚本

    某互联网后台自动化组合测试框架RF+Sikuli+Python脚本 http://www.jianshu.com/p/b3e204c8651a 字数949 阅读323 评论1 喜欢0 一.**Robo ...

  7. 导入maven工程错误

    有时候导入maven工程会报空指针异常: An internal error occurred during: “Updating Maven Project”. java.lang.NullPoin ...

  8. html JS 打开本地程序及文件

    在网页打开本地应用程序示例: 一.在本地注册表自定义协议:以自定义调用Viso为例 1.在HKEY_CLASSES_ROOT下添加项ZVISIO. 2.修改ZVISIO项下的"(默认)&qu ...

  9. APM程序分析-ArduCopter.cpp

    该文件是APM的主文件. #define SCHED_TASK(func, rate_hz, max_time_micros) SCHED_TASK_CLASS(Copter, &copter ...

  10. eclipse maven maven-archetype-webapp 创建失败

    如果在eclipse中发现创建maven失败,大部分的原因是因为本地仓库坏了,或是少东西了,最直接的方法就时删掉重新下载就好了