首先介绍一下这个方法:
hitTest:withEvent:调用过程
iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window(当前接收用户事件的窗口)处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。
window对象会在首先在view hierarchy的顶级view上调用hitTest:withEvent:,此方法会在视图层级结构中的每个视图上调用pointInside:withEvent:,如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是hit-test view。
hitTest:withEvent:方法的处理流程如下:
- 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
- 若返回NO,则hitTest:withEvent:返回nil;
- 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
- 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
- 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
对于每个触摸操作都会有一个UITouch对象,UITouch对象用来表示一个触摸操作,即一个手指在屏幕上按下、移动、离开的整个过程。UITouch对象在触摸操作的过程中在不断变化,所以在使用UITouch对象时,不能直接retain,而需要使用其他手段存储UITouch的内部信息。UITouch对象有一个view属性,表示此触摸操作初始发生所在的视图,即上面检测到的hit-test view,此属性在UITouch的生命周期不再改变,即使触摸操作后续移动到其他视图之上。
事件的响应链其实这一切都是建立在事件响应链之上
UIResponder 这个类定义了很多用来处理响应和时间处理的类。他的子类有UIApplication,UIView以及UIWindow等。
IOS中分为两类事件:触摸事件,和移动事件。最原始的事件处理方是touchesBegan:withEvent:
,touchesMoved:withEvent:
, touchesEnded:withEvent:
, and touchesCancelled:withEvent:。
无论任何时候手指只要触摸屏幕或是在屏幕上移动拖拽甚至离开屏幕都会导致一个UIEvent对象产生。
在UIResponder中有一个非常重要的概念叫做Responder Chain,个人的理解是这是按照一定规则组织的响应、处理事件的一条链表。在了解UIResponder之前还得在了解一个概念Hit-Testing。在IOS中通常使用hit-testing去找到那个被触摸的视图。这个视图叫hit-test view,当IOS找到hit-test view后就把touch event交个那个视图来处理。下面画个图来说明一下,当点击视图E时看一下hit-testing的工作过程。
1.确定改触摸事件发生在view A范围内,接下来测试view B以及view C。
2.检查发现事件不再view B范围内发生,接下来检查view C发现触摸事件发生在了view C中,所以检查 view D,view E。
3.最后发现事件发生在view E的范围内所以view E成为了hit-test view。
下面是关于调用hit-test的官方说明:
The hitTest:withEvent:
method returns the hit test view for a given CGPoint
and UIEvent
. The hitTest:withEvent:
method begins by calling thepointInside:withEvent:
method on itself. If the point passed into hitTest:withEvent:
is inside the bounds of the view, pointInside:withEvent:
returns YES
. Then, the method recursively calls hitTest:withEvent:
on every subview that returns YES
.
Responder Chain 是由responder对象组成的
canBecomeFirstResponder
方法让他返回YES
Responder Chain 遵循一个特定的传播路径


其实简而言之,当你触摸到屏幕上了,首先会将事件送window上,然后他一层一层往上传递,如果在一个view上有两个子view,那么从最上面的view先执行hitTest:withEvent:,如果该view上还有子view会继续向上传递,直到返回正确的对象,如果没有子view,那就是返回本身,还有个对象就是Responder,在hitTest:withEvent:执行的时候,还有一个操作就是设置responder的层级,如果最后找到的view,那么该view就设置伟fristResponder,当然我们通过responder可以找到对应的view或者viewcontroller。示例如下:
看了这么多,我们来看一个实现,他是如何使用的hitTest:withEvent:
我们直到,UITableViewCell左滑可以实现自定义的菜单,他的核心思想就是hitTest:withEvent:
首先自定义UITableViewCell ,以下是一段伪代码
-(void)configWithData:(NSIndexPath *)indexPath menuData:(NSArray *)menuData cellFrame:(CGRect)cellFrame{
View * a,b,c;
cell add a,b,c;
a,b,c insert cell at index 0;
}
然后当你点击到左滑Cell的时候,重新设置contentView的frame,然后加上一层OverLayerView,关键就是这一个覆盖层,然后任何触摸事件的时候,去处理。
- (UIView *)overLayView:(OverLayView *)view didHitPoint:(CGPoint)didHitPoint withEvent:(UIEvent *)withEvent{
BOOL shoudReceivePointTouch = YES;
CGPoint location = [self.view convertPoint:didHitPoint fromView:view];
CGRect rect = [self.view convertRect:self.activeCell.frame toView:self.view];
shoudReceivePointTouch = CGRectContainsPoint(rect, location);
if (!shoudReceivePointTouch) {
[self hideMenuActive:YES];
}
return (shoudReceivePointTouch) ? [self.activeCell hitTest:didHitPoint withEvent:withEvent] : view;
}
本来应该一层层向上透传的hitTest:withEvent:,当触摸到OverLayerView的时候,被强制传递到了Cell上,这样就实现了即使出现了OverLayerView,Cell也能处理事件的问题,是不是很棒!
看看实现的截图吧,来自网络哦