当我们点击屏幕上某个点的时候, IOS会检查到手指触摸操作(Touch),并生产一个UITouch对象,将其打包成一个UIEvent对象。然后将其放入当前活动的Application的 事件对列 , UIApplication会从 事件对列 中按照对列的顺序,通常先分发给应用程序主窗口,主窗口会调用hitTest:withEvent:方法(假设称为方法A,注意这是UIView的方法),查找合适的事件触发视图(这里通常称为“hit-test view”)。
看下面的图就一目了然
关于方法- hitTest withEvent:
1.以下三种情况,当前视图就不再接收消息了,直接返回 nil。
self.userInteractionEnabled = NO
self.hidden = YES
self.alpha <= 0.01
2.判断触摸点是否在该类范围内,若pointInside返回 YES就倒序遍历 subview,subView 调用自己的 hit 方法,一层subview一层subview 的找下去,若最终找到一个目标 view,该类的 hit 方法就返回这个 目标view,若没有这个 view,则返回 self。若pointInside返回 NO 返回 nil。
上代码:
- (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;
}
首先MainView 上的子view包括 viewA,ViewB(ViewB1,ViewB2),ViewC,添加顺序也是依次添加。
由图和代码我们可以看出:遍历当前视图的所有子视图的顺序是从当前视图的subviews数组的末尾向前遍历(倒序),即先判断最后添加到当前视图上的子视图
下面是整个触摸的传递过程,传递到不同的 View 调用- hitTest withEvent:判断触摸点是否在当前视图范围内
1.主 window:-pointInside: withEvent:–>return YES,以此为根节点向下进行倒序遍历搜索
2.MainView :-pointInside: withEvent:–>return YES,以此为根节点向下进行倒序遍历搜索
3.ViewC:-pointInside: withEvent:–>return NO,不再向下进行搜索
4.ViewB :-pointInside: withEvent:–> YES,以此为根节点向下进行搜索
5.ViewB2 :–> NO,不再向下进行搜索
6.ViewB1 :–> YES,好了到这 ViewB2没有子视图(或者子视图的 hitView 都返回了nil),直接返回 self
至此,我们已经找到了 hitView,接下来把 hitView 向上传递给 UIApplication,UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给hit-testview,hitView会调用touchesBegan方法响应事件
事件的响应
UIResponder 的 touchesBegan 默认实现如下,将事件抛给nextResponder
//UIResponder
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self nextResponder] touchesBegan:touches withEvent:event];
}
UIView没有重写该方法,会传递给nextResponder;
UIControl重写了touchesBegan,未传递给nextResponder,所以会打断事件响应链,不再外下传递
//UIControl
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_touchInside = YES;
_tracking = [self beginTrackingWithTouch:touch withEvent:event];
self.highlighted = YES;
if (_tracking) {
UIControlEvents currentEvents = UIControlEventTouchDown;
if (touch.tapCount > 1) {
currentEvents |= UIControlEventTouchDownRepeat;
}
[self _sendActionsForControlEvents:currentEvents withEvent:event];
}
}
下面来看几种情况:
1.当前view,包括子 view 都不接收事件,将事件传递给兄弟viewA:当前view 重写- hitTest withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return nil;
}
2.当前view不接收事件,但是,子 view 要接收,将事件传递给兄弟viewA(即:点击当前 view 空白的地方,让和他重叠的兄弟 view 接收事件):当前view 重写- hitTest withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
if(view == self){
return nil;
}
return view;
}
3.设置 scrollView 的分页宽度。现在有这么一个需求:有一个 VC,上面有一个parentView,parentView 上面有个 scrollView,scrollView 上显示三张图片,滑动一下,就会滚出来一张图片,看清楚一张,就是说 scrollView 的分页是1/3VC.View。
思路:
设置scrollView的宽度为1/3VC.View。//这样分页宽度就是自己的宽度。
设置scrollView的 center 横向居中。
设置 scrollView.contentoffset = 1/3VC.View。//这样就显示了3张图片
开启分页模式:scrollView.pagingEnabled = YES。
设置scrollView.clipsToBounds = NO。//这样超出范围的视图也会显示。
然后重写scrollView所在的parentView的hitTest事件,让其返回值是UIScrollView对象。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [self pointInside:point withEvent:event] ? scrollView : nil;
}