hitTest示例
往窗口里添加两个自定义的view,这样每个view的hitTest方法被访问的时候我们就能log一下:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view1 = View1(frame: CGRect(x: 110, y: 110, width: 150, height: 150))
let view2 = View2(frame: CGRect(x: 170, y: 170, width: 150, height: 150))
view1.backgroundColor = .yellow
view2.backgroundColor = .red
self.view.addSubview(view1)
self.view.addSubview(view2)
}
}
class View1 : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("enter v1 \(point)")
return super.hitTest(point, with: event)
}
}
class View2 : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("enter v2, \(point)")
return super.hitTest(point, with: event)
}
}
运行:

在空白处(bottom view)点了一下,输出:
enter v2, (-48.66007995605469, 306.0133361816406)
enter v1, (11.339920043945312, 366.0133361816406)
enter v2, (-48.66007995605469, 306.0133361816406)
enter v1, (11.339920043945312, 366.0133361816406)
enter v2, (-43.33333333333334, 325.3333333333333)
enter v1, (16.666666666666657, 385.3333333333333)
enter v2, (-43.33333333333334, 325.3333333333333)
enter v1, (16.666666666666657, 385.3333333333333)
在红框上(top view)点一下,输出:
enter v2, (38.66666666666666, 48.66666666666666)
enter v2, (38.66666666666666, 48.66666666666666)
在黄框(middle view)点一下,输出:
enter v2, (-31.210678100585938, -27.8685302734375)
enter v2, (-31.210678100585938, -27.8685302734375)
enter v2, (-25.0, -22.333333333333343)
enter v1, (35.0, 37.66666666666666)
enter v2, (-25.0, -22.333333333333343)
enter v1, (35.0, 37.66666666666666)
- 我们知道
hitTest机制是事件传递链由底向上,响应链由上到下, - 所以最底层的bottom view最先接到事件就开始找响应者
- 它开始从它的最顶层subview开始找响应者(
v2),然后再往下(v1),均没找到,所以就是自己了 - 为何调了四次呢?(未深究)
Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.
- 第二次测试,直接在最顶层view就找到了(所谓的找到,就是点击的位置在这个view的bounds内,后面说)
- 至于为什么坐标会变?(未深究)
- 第三次测试,点了v1,可以看到日志,在v2那里跑了3次,再跑了v1,不知道为什么还是没返回,还跑了一次v2后才认定v1
大体可以知道hitTest的机制了吧?以传递链的终点那个view为基础,在subviews逆向遍历(自顶向下),一直到自己。
应用1
来个简单场景,如果黄色的view是有触摸事件的,并且要求被覆盖的区域也能响应,该怎么做呢?
思考:
- 我们知道点击的位置,肯定是在黄框范围内的,所以要的就是一个入口,用来判断这个点与黄框的关系,一旦确认点的范围是在黄框里,就把认为黄框是事件响应者。
- 因此改下demo,加了事件,还加了一个parent view(这样才能在用属性的方式把黄框引用出来)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view1 = UIView(frame: CGRect(x: 110, y: 110, width: 150, height: 150))
let view2 = UIView(frame: CGRect(x: 170, y: 170, width: 150, height: 150))
view1.backgroundColor = .yellow
view2.backgroundColor = .red
let tap = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
view1.addGestureRecognizer(tap)
view1.isUserInteractionEnabled = true
let view = View(a: view1, b: view2)
self.view.addSubview(view)
}
@objc func tap(_ sender: UIGestureRecognizer) {
print("view1 taped")
}
}
class View: UIView {
var view1: UIView
var view2: UIView
init(a: UIView, b: UIView) {
self.view1 = a
self.view2 = b
super.init(frame: UIScreen.main.bounds)
self.addSubview(a)
self.addSubview(b)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let loc_v1 = self.convert(point, to: self.view1)
// 主要就是这一句
if(self.view1.point(inside: loc_v1, with: event)) {
return self.view1
}
return super.hitTest(point, with: event)
}
}
let loc_v1 = self.convert(point, to: self.view1)意思是这个点以view1为坐标系的位置if(self.view1.point(inside: loc_v1, with: event))这就是判断这个点在不在view1的bounds里面了
简单来说,就是简单粗暴地“逮住每一个机会”,问是不是你,是不是你。
应用2
如果一个按钮很小,你要扩大他的点击区域怎么做?网上有很多方法,关联属性啊,交换方法啊,可以去搜搜,我们这里继续上面的例子,知道有一个point(inside:with)方法,顾名思义,就是这个点在不在我的视图区域内
它当然也是可以被重写,自定义在什么样的范围内,都算inside,下面是网上抄的一段代码
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
//获取当前button的实际大小
CGRect bounds = self.bounds;
//若原热区小于44x44,则放大热区,否则保持原大小不变
CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);
CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
//扩大bounds
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
//如果点击的点 在 新的bounds里,就返回YES
return CGRectContainsPoint(bounds, point);
}
这个例子主要就是利用底层的CGRectContainsPoint方法,传入了新的bounds,可以理解为人为修改入参吧。代码也很明确了,自己根据当前的bounds合理做一个大一点的bounds,交给系统去做就行了。