// 作用: 去寻找最适合的View
// 什么时候调用: 当一个事件传递给当前View,就会调用.
// 返回值: 返回的是谁,谁就是最适合的View(就会调用最适合的View的touch方法)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
hitTest
的底层实现:
- 1.先看自己是否能接受触摸事件
- 2.再看触摸点是否在自己身上
- 3.从后往前遍历子控件,拿到子控件后,再次重复1,2步骤,要把父控件上的坐标点转换为子控件坐标系下的点,再次执行hitTest方法
- 4.若是最后还没有找到合适的view,那么就return self,自己就是合适的view
备注:当控件接收到触摸事件的时候,不管能不能处理事件,都会调用hitTest方法
应用实例
1.扩大UIButton的响应热区
import UIKit
private var ts_touchAreaEdgeInsets: UIEdgeInsets = .zero
extension UIButton {
/// Increase your button touch area.
/// If your button frame is (0,0,40,40). Then call button.ts_touchInsets = UIEdgeInsetsMake(-30, -30, -30, -30), it will Increase the touch area
public var ts_touchInsets: UIEdgeInsets {
get {
if let value = objc_getAssociatedObject(self, &ts_touchAreaEdgeInsets) as? NSValue {
var edgeInsets: UIEdgeInsets = .zero
value.getValue(&edgeInsets)
return edgeInsets
}else {
return .zero
}
}
set(newValue) {
var newValueCopy = newValue
let objCType = NSValue(uiEdgeInsets: .zero).objCType
let value = NSValue(&newValueCopy, withObjCType: objCType)
objc_setAssociatedObject(self, &ts_touchAreaEdgeInsets, value, .OBJC_ASSOCIATION_RETAIN)
}
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(self.ts_touchInsets, .zero) || !self.isEnabled || self.isHidden {
return super.point(inside: point, with: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.ts_touchInsets)
return hitFrame.contains(point)
}
}
使用示例
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
areaBtn.ts_touchInsets = .init(top: -30, left: -30, bottom: -30, right: -30)
}
也可以自定义UIButton,在自定义的button里实现
import UIKit
class TouchIncreaseButton: UIButton {
private let btnWidth : CGFloat = 44
private let btnHeight : CGFloat = 44
private func hitTestBounds(minimumHitTestWidth minWidth : CGFloat,minimumHitTestHeight minHeight : CGFloat) -> CGRect {
var hitTestBounds = self.bounds
if minWidth > bounds.size.width {
hitTestBounds.size.width = minWidth
hitTestBounds.origin.x -= (hitTestBounds.size.width - bounds.size.width)/2
}
if minHeight > bounds.size.height {
hitTestBounds.size.height = minHeight
hitTestBounds.origin.y -= (hitTestBounds.size.height - bounds.size.height)/2
}
return hitTestBounds
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = hitTestBounds(minimumHitTestWidth: btnWidth, minimumHitTestHeight: btnHeight)
return rect.contains(point)
}
}
2.子view超出了父view
的bounds
响应事件
重载父view
的hitTest(_ point: CGPoint, with event: UIEvent?)
方法
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for subView in self.subviews {
// 把父类point点转换成子类坐标系下的点
let convertedPoint = subView.convert(point, from: self)
// 注意点:hitTest方法内部会调用pointInside方法,询问触摸点是否在这个控件上
// 根据point,找到适合响应事件的这个View
let hitTestView = subView.hitTest(convertedPoint, with: event)
if hitTestView != nil {
return hitTestView
}
}
return nil
}
3.使部分区域失去响应.
场景需求:如图,tableView
占整个屏幕,tableView
底下是一个半透明的HUD
,点击下面没有内容区域,要让HUD
去响应事件.
在自定义的tableView
中重载hitTest
方法
// tableView会拦截底部`backgroundView`事件的响应,
// 实现点击tableViewCell 之外的地方,让tableView底下的backgroundView响应tap事件
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let hitView = super.hitTest(point, with: event) , hitView.isKind(of: BTTableCellContentView.self) {
return hitView
}
// 返回nil 那么事件就不由当前控件处理
return nil;
}
4.让非scrollView
区域响应scrollView
拖拽事件
如图,这是一个使用scrollView
自定义实现的卡片式轮播器,如何实现拖拽scrollView
两边的view
区域,和拖拽中间scrollView一样的效果呢?只需要在scrollView
的父View重载hitTest
方法
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) == true {
return scrollView
}
return nil
}
3.1.使自定义Button失去响应
class NoEventButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
}
参考:iOS事件响应链中Hit-Test View的应用
ios开发事件处理之 四:hittest方法的底层实现与应用