Hit-Test和响应链
什么叫 hit-test view?文档说:The lowest view in the view hierarchy that contains the touch point becomes the hit-test view,我的理解是:当你点击了屏幕上的某个view,这个动作由硬件层传导到操作系统,然后又从底层封装成一个事件(Event),从keyWindow开始顺着view的层级往上传导,一直要找到含有这个点击点且层级最高的view来响应事件,这个view就是hit-test view。
如果在hit-test中调用默认的super hittest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView * view = [super hitTest:point withEvent:event];
return view;
}
则其内在的行为等价于:
- (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是view subviews数组的最后一个元素,只要寻找是从数组的第一个元素开始遍历,hit-test view的逻辑依然是有效的。(即reverseObjectEnumerator逆序枚举中的首个元素执行到return self的优先级最高。)
hit-test
找到hit-test view后,它会有最高的优先权去响应逐级传递上来的Event,如它不能响应就会传递给它的superview,依此类推,一直传递到UIApplication都无响应者,这个Event就会被系统丢弃了。
responder chain
可以看到,持有View的View Controller会先于super View得到响应时间的机会。
应用举例
1、扩大UIButton的响应热区
重载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中间的大按钮,如在底部TabberBar中间放置宇哥大按钮,点击超出Tabbar bounds的区域也需要响应,此时重载父view的-(UIView *)hitTest: withEvent:方法,去掉点击必须在父view内的判断,然后子view就能成为 hit-test view用于响应事件了。
- (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滑动
当使用scrollview进行分页显示的时候(PageEnable = YES),分页的宽度是scrollview的宽度,通常我们使用scrollview全屏显示内容,如果我们需要半屏幕宽或者其他小于屏幕宽度的分页,则需要以下步骤:
- 设置你的UIScrollView的宽度为Width/2;
- 开启分页模式:self.pagingEnabled = YES;
- 关闭self.clipsToBounds = NO; 这样超出范围的视图也会显示。
- 然后重写UIScrollView所在的parentView的hitTest事件,让其返回值是UIScrollView对象. 此举可以使scrollview两侧的区域也能响应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;
}
网友评论