0%

响应者链hitTest释疑

Q:UIButton想要扩大它的响应区域?
A:重载UIButton的hitTest或pointInside.
重载hitTest:

1
2
3
4
5
6
7
8
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
CGRect touchRect = CGRectInset(self.bounds, -40, -40);
if (CGRectContainsPoint(touchRect, point)) {
return self;
}
return [super hitTest:point withEvent:event];
}

或重载pointInside:

1
2
3
4
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL rs = CGRectContainsPoint(CGRectInset(self.bounds, -40, -40), point);
return rs;
}

这里推荐重写pointInside方法.因为重写hitTest方法需要注意判断view的一些属性:if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;如果不判断会出Bug,比如设置按钮hidden为YES了,但是摸到按钮的区域,按钮的响应方法一样会被调用.而重写pointInside方法则不需要考虑这些.

PS:如果设置了userInteractionEnabled=NO;即使重写hitTest返回按钮自己,按钮的响应方法也不会回调.应该是系统在回调之前又判断了一下该属性的值.

userInteractionEnabled的说明如下:

When set to NO, touch, press, keyboard, and focus events intended for the view are ignored and removed from the event queue. When set to YES, events are delivered to the view normally. The default value of this property is YES.

During an animation, user interactions are temporarily disabled for all views involved in the animation, regardless of the value in this property. You can disable this behavior by specifying the UIViewAnimationOptionAllowUserInteraction option when configuring the animation.

当btn作为第一响应者被返回时,如果userInteractionEnabled属性为NO,则与它相关的事件(触摸,按压)都将被忽略.因此btn的响应方法不会被调用.

Q:点击子视图超出父视图的区域仍让子视图响应事件?
A:必须重载父视图(**父视图父视图父视图**三遍!!!)的pointInside方法.这是由于如果父视图的pointInside返回NO的话,其上的子视图就不会进行hitTest,因此重载子视图的相关方法将无效.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var rs = super.point(inside: point, with: event)

if !rs {
for subView in subviews.reversed() {
let subPoint = subView.convert(point, from: self)
if subView.bounds.contains(subPoint) {
rs = true
break
}
}
}

return rs
}

在项目中遇到一个场景:collectionViewCell上的按钮超出了cell的边界,想点击按钮超出部分按钮也能够响应事件,按照上述办法重写了collectionViewCell的pointInside方法,但是却没效果.于是又重写了cell的hitTest方法,发现hitTest方法返回的是cell自身,并没有返回button.这下糊涂了,是上面的理论错了吗?上面的理论是没有错的,分析了半天发现根本原因是cell并不是button的父视图,button的父视图是contentView,而contentView的父视图才是cell.虽然cell的pointInside返回了YES,但是contentView的pointInside依然返回的是NO.所以不会再hitTest contentView上的子视图,button自然就没有反应了.真相终于明了,但问题还没解决,如何解决呢?

由于contentView是cell的内部属性没办法自定义,所以这里我们重写cell的hitTest方法,判断如果点击的点位于按钮区域内,则直接将按钮返回.这下点击超出区域按钮也能够响应事件了,完美.

Q:点击subview却让superview响应?
A:重载subview的hitTest方法:

1
2
3
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.superview
}

Q:hitTest和pointInside的关系?
A:通过真机调试发现(模拟器可能不准),系统会先调用父视图的hitTest方法,hitTest方法继而会调用pointInside方法,如果pointInside返回YES,那么系统将对其上的子视图重复上面的过程进行hitTest.

hitTest的一种可能实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{

//1.判断自己能否接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
//2.判断当前点在不在当前View.
if (![self pointInside:point withEvent:event]) {
return nil;
}
//3.从后往前遍历自己的子控件.让子控件重复前两步操作,(把事件传递给,让子控件调用hitTest)
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
//取出每一个子控件
UIView *chileV = self.subviews[i];
//把当前的点转换成子控件坐标系上的点.
CGPoint childP = [self convertPoint:point toView:chileV];
UIView *fitView = [chileV hitTest:childP withEvent:event];
//判断有没有找到最适合的View
if(fitView){
return fitView;
}
}

//4.没有找到比它自己更适合的View.那么它自己就是最适合的View
return self;
}
觉得文章有帮助可以打赏一下哦!