最近又看了遍苹果的官方文档《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中的if __name__='__main__'语句的作用

    笔者在自学Python的过程中,对于if __name__='__main__'的用法感到很困惑,在think Python一书中原作者的源代码是这么解释if __name__='__main__'语 ...

  2. 《深入理解Java虚拟机》学习笔记1-内存数据区域

         1.程序计数器 作用-较小的内存空间,用于存储当前线程所执行的字节码的行号 特性-每条线程有需要一个独立的程序计数器,各线程间互不影响,独立存储,称为"线程私有"的内存 ...

  3. MFC像窗体坐标位置发送 点击消息

    int x11=495;                                        int y22=600;                                     ...

  4. KVM 虚拟机联网方式:NAT 和 Bridge

    KVM 客户机网络连接有两种方式: 用户网络(User Networking):让虚拟机访问主机.互联网或本地网络上的资源的简单方法,但是不能从网络或其他的客户机访问客户机,性能上也需要大的调整.NA ...

  5. 解决VS2015安装后stdio.h ucrtd.lib等文件无法识别问题

    今天突然想在windows上装个 VS2015 玩玩,结果遇到了如下bug:安装完 VS2015 后,直接新建项目->win32控制台->运行,结果报错!"无法打开包括文件: & ...

  6. Google Code Jam 2016 Round 1C C

    题意:三种物品分别有a b c个(a<=b<=c),现在每种物品各选一个进行组合.要求每种最和最多出现一次.且要求任意两个物品的组合在所有三个物品组合中的出现总次数不能超过n. 要求给出一 ...

  7. go:多核并行化问题

    分别用串行和并行实现了一个NUM次加法的程序,代码如下: package main import ( "fmt" //"runtime" //执行并行段时需要引 ...

  8. WPF去边框与webbrowser的冲突

    首先建一个类,比如NativeMethods.cs class NativeMethods{     public const int WS_CAPTION=0x00C0000;     public ...

  9. Servlet调用过程

    (1)在浏览器输入地址,浏览器先去查找hosts文件,将主机名翻译为ip地址,如果找不到就再去查询dns服务器将主机名翻译成ip地址. (2)浏览器根据ip地址和端口号访问服务器,组织http请求信息 ...

  10. CodeForces 589J Cleaner Robot

    题目链接 题意:一个机器人打扫卫生,URDL代表初始时机器人面对的方向上右下左. ' . ' 代表可以打扫的, ' * ' 代表家具,如果机器人遇到家具就顺时针转90度,问机器人能打扫多少面积. 题解 ...