相信各位同仁在工作中或多或少的都遇到过关于按钮点击范围太小,希望能放大点击范围诸如此类的产品需求,最简单最低级的方法例如添加透明superView响应target事件等方法也可以解决此类问题,但这样是明显的“治标不治本”且随着业务迭代容易出现未知的错误,基于事件传递的机制,iOS提供了相应API解决此类问题; 该方法可以改变控件响应的点击范围:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = self.bounds;
bounds = CGRectInset(bounds, -10, -10);
// CGRectContainsPoint(<#CGRect rect#>, <#CGPoint point#>) 判断点是否在矩形内
return CGRectContainsPoint(bounds, point);
}
系统发生触摸事件的时候会从window到父控件到子控件一个个检测触摸点是否在其中,如果在其中,则返回YES,最后返回YES的子控件作为响应事件的控件。 我们只要重写这个方法,在其中判断,是否点击了我们想要的区域,是的话就返回YES,否则返回NO,这样就实现了自定义点击的有效区域了。注意,这边并没有改变按钮的形状,按钮还是矩形的按钮,只是改变了按钮中响应区域而已。
由此引申出的事件传递链条就是我们需要重点理解的知识了:
事件传递链条
- 产生触摸事件时,keyWindow会在它的内容视图上调用
//此方法返回的就是处理此触摸事件最合适的view
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
来完成这个寻找过程:
hitTest:withEvent方法底层实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// NSLog(@"%@--hitTest",[self class]);
// return [super hitTest:point withEvent:event];
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
- ① hitTest:withEvent在内部首先会判断该视图是否能响应事件,如果不能响应,返回nil,表示该视图不响应此触摸事件;
- ② 调用pointInside:withEvent:判断点击事件是否发生在当前视图范围内,方法返回NO,则return nil;
- ③ 如果pointInside返回YES,则向当前视图的所有子视图发送hitTest:withEvent消息,所有子视图的遍历顺序是从顶层视图一直到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或全部子视图遍历完毕。 若第一次有子视图返回非空对象,则hitTest方法返回此对象,处理结束;若所有子视图都返回非,则hitTest方法返回该视图本身
事件传递中所有pointInside返回YES的view加上控制器、UIWindow、UIApplication构成了响应者链。
- 事件传递是自下而上的(hitTest);
- 响应者链是自上而下的(touchBegin等)
(本文约定,window上最外层的view称为“上”)
关于事件传递的详细理解参照这里