了解事件响应链的同学应该知道hitTest
和point
方法,我们先来简单回顾一下
hitTest
内部实现
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1.判断当前控件能否接收事件
if isUserInteractionEnabled == false || hidden == true || alpha <= 0.01 {
return nil
}
// 2. 判断点在不在当前控件
if self.point(inside: point, with: event) == false {
return nil
}
// 3.从后往前遍历自己的子控件
let count: Int = subviews.count
var i = count - 1
while i >= 0 {
let childView: UIView? = subviews[i]
// 把当前控件上的坐标系转换成子控件上的坐标系
let childP: CGPoint = convert(point, to: childView)
let fitView: UIView? = childView?.hitTest(childP, with: event)
if fitView != nil {
// 寻找到最合适的view
return fitView
}
i -= 1
}
// 循环结束,表示没有比自己更合适的view
return self
}
总结
- 作用:去寻找最适合的View
- 调用:当一个事件传递给当前View,就会调用
- 返回值:返回的是谁,谁就是最适合的View(就会调用最适合的View的touch方法)
point
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// 如果返回false就代表,当前点不在红色view(self)上面,那么当我们确实点击红丝view,红色view也不会响应事件。
// return false;
//如果返回true就代表,当前点在红色view(self)上面,那么即使我们没有点击红丝view,红色view也会响应事件。
}
总结
- 作用:判断当前点在不在它调用View,(谁调用pointInside,这个View就是谁)
- 调用:它是在hitTest方法当中调用的
- 注意:point点必须得要跟它方法调用者在同一个坐标系里面
究竟什么时候重写hitTest,什么时候重写point?
很多情况下hitTest和pointInside方法任选其一都可以实现某个功能,比如在屏蔽中,point返回false可以实现的话,都可以用hitTest返回nil代替。
但是,hitTest更强大。因为point在一般情况下其内部顶多只能根据情况判断怎么返回false,屏蔽掉自己和子控件的事件响应。所以只要是想保留子控件对触摸事件响应,屏蔽其父控件的响应,单独重写point无法办到,必须要重写hitTest方法。
触摸事件原本该由某个view响应,现在你不想让它处理而让别的控件处理,那么就应该在该view内重写hitTest或point方法。
运用Runtime扩大UIButton点击区域
根据上面的介绍可以知道通过在UIButton的extension中重写hitTest
或point
,下面是简单实现
extension UIButton {
private struct cs_associatedKeys {
static var topKey = "cs_topKey"
static var rightKey = "cs_rightKey"
static var bottomKey = "cs_bottomKey"
static var leftKey = "cs_leftKey"
static var marginKey = "cs_marginKey"
}
//方法一
func setEnlargeEdgeWith(top: CGFloat, right: CGFloat, bottom: CGFloat, left : CGFloat) {
objc_setAssociatedObject(self, &cs_associatedKeys.topKey, top, .OBJC_ASSOCIATION_COPY_NONATOMIC)
objc_setAssociatedObject(self, &cs_associatedKeys.rightKey, right, .OBJC_ASSOCIATION_COPY_NONATOMIC)
objc_setAssociatedObject(self, &cs_associatedKeys.bottomKey, bottom, .OBJC_ASSOCIATION_COPY_NONATOMIC)
objc_setAssociatedObject(self, &cs_associatedKeys.leftKey, left, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
private var enlargedRect: CGRect {
let topEdge = objc_getAssociatedObject(self, &cs_associatedKeys.topKey) as? CGFloat
let rightEdge = objc_getAssociatedObject(self, &cs_associatedKeys.rightKey) as? CGFloat
let bottomEdge = objc_getAssociatedObject(self, &cs_associatedKeys.bottomKey) as? CGFloat
let leftEdge = objc_getAssociatedObject(self, &cs_associatedKeys.leftKey) as? CGFloat
guard let top = topEdge, let right = rightEdge, let botton = bottomEdge, let left = leftEdge else {
return bounds
}
return CGRect(x: CGFloat(bounds.origin.x - left), y: CGFloat(bounds.origin.y - top), width: CGFloat(bounds.size.width + left + right), height: CGFloat(bounds.size.height + top + botton))
}
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let rect = self.enlargedRect
if rect.equalTo(bounds) {
return super.hitTest(point, with: event)
}
return rect.contains(point) ? self : nil
}
//方法二
var margin: CGFloat{
get {
if let accpetEventInterval = objc_getAssociatedObject(self, &cs_associatedKeys.marginKey) as? CGFloat {
return accpetEventInterval
}
return 0.0
}
set {
objc_setAssociatedObject(self, &cs_associatedKeys.marginKey, newValue as CGFloat, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds = self.bounds
bounds = bounds.insetBy(dx: -margin, dy: -margin)
return bounds.contains(point)
}
}
使用
let btn = UIButton()
btn.frame = CGRect(x: 100, y: 100, width: 40, height: 40)
//方法一
// btn.setEnlargeEdgeWith(top: 100, right: 100, bottom: 100, left: 100)
//方法二
btn.margin = 100
btn.backgroundColor = UIColor.red
btn.addTarget(self, action: #selector(ViewController.didTap), for: UIControlEvents.touchUpInside)
self.view.addSubview(btn)