什么叫 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来响应事件,这个view就是hit-test view。
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把这个寻找的逻辑换成代码如下:
- (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;
}
找到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层一个非常重要的概念,想做出非常棒的交互和动画,必须对其有一个深入的理解。我列举的只是我在开发中遇到的一些问题,如果有其他的对事件响应链的应用希望大家和我一起交流探讨。