本文首发于公众号【一个老码农】
-
什么是响应者链
iOS 响应者链是支撑 App 界面交互的重要基础。当点击屏幕会产生一个触摸事件,主线程runloop会接收到它并放到消息队列里,UIApplication 会从消息队列里取事件分发下去,经过多个响应者对象的传递,找到合适的响应视图。这多个响应者对象连接起来的链条,我们称之为响应者链。 -
事件传递的过程
当一个触摸事件产生时,响应者链是怎样找到响应视图的呢?
1.首页事件会先由UIApplication传递至window
2.调用UIWindow的hitTest:方法,在hitTest方法中调用pointInside方法判断点击是否在当前Window中。
3.如果window的pointInside方法返回YES(一般都会返回yes),则倒序遍历子View,并调用子view的hitTest:方法。
4.在子view的hitTest方法中再调用pointInside方法判断点击事件是否在当前子view中。
5.如果在当前view中,则判断有没有子view,如果有子view,则继续遍历子view。
6.若第一次有hitTest方法返回的UIView对象为非空。则递归返回此对象,此对象就是此次事件的最佳响应者。
7.若所有子view的hitTest方法都返回nil,则返回当前视图做为最佳响应者。
注:在查找响应者过程当中,hitTest和pointInside方法其实是会调用两次的,两次为同一个事件,只是事件的状态不同。一次为事件的begain,一次为事件的end。
-
事件响应的过程是怎样的
找到合适的响应者后,首先判断响应者是否能响应此事件,如果不能响应,则会向父view传递,直到找到响应者,如果一直没有找到响应者,则会一直到UIWindow,如果window还无法处理则会交给UIApplication,如果UIApplication还无法处理,则忽略掉此事件。
找到响应者后,会顺序调用响应者的以下几个方法
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
-
hitTest方法和pointInside方法的作用
hitTest方法的作用是,为事件寻找一个合适的view作为响应者,内部实现有以下判断:
1.view的userInteractionEnabled属性为false的不响应事件
2.view的ishidden属性为true的不响应事件
3.view透明度小于等于0.01的不响应事件
4.调用pointInside,判断点击位置是否在当前view内,不在当前view内的,不响应事件
pointInside方法会在hitTest方法内部调用,用来判断当前事件是否在当前view范围内,如果不在此view范l围内,则不能响应事件。 -
我们可以用hitTest和pointInside做哪些事情
实例1:事件透传
view上有一个button,view和button分别绑定了事件。但是我在点击button的时候,想忽略掉button点击事件,转而让view响应。
分析:在Button的hitTest方法中,如果super.hitTest返回的对象是自己,则返回nil。这样就可以忽略掉button的点击事件。
代码如下:
override func viewDidLoad() {
super.viewDidLoad()
let bgView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
bgView.backgroundColor = UIColor.blue
let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap))
view.addGestureRecognizer(tap)
view.addSubview(bgView)
let button = CustomButton(type: .custom)
button.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
button.setTitle("Click", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(click), for: .touchUpInside)
button.backgroundColor = UIColor.red
bgView.addSubview(button)
}
@objc private func viewTap() {
print("调用的是view")
}
@objc private func click() {
print("点击")
}
//重写button的hitTest方法
class CustomButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("CustomButton hitTest")
let testView = super.hitTest(point, with: event)
if testView == self {
return nil
}
return testView
}
}
点击Button之后控制台打印效果:
实例2:Button超过父View的范围,点击范围外的部分,仍然让其响应事件
如图,点击蓝色外的红色部分,仍然让其响应Click事件。
分析:在父view的pointInside里面,判断子view是否为Button类型,如果是,则判断是否点击在button范围内。
如下代码,点击蓝色区域外的button区域,则可以响应button事件:
class CustomView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subView in subviews {
if subView.classForCoder == CustomButton.classForCoder() {
let subPoint = self.convert(point, to: subView)
if subView.point(inside: subPoint, with: event) {
return true
}
}
}
return super.point(inside: point, with: event)
}
}
实例3:让UIButton的响应区域变大
如,我想让工程中所有的UIButton响应区域都至少为50*50pt。
思路:重写UIButton的pointInside方法,在此方法内判断button的响应区域,若在button的50pt范围内的,则返回true。代码如下:
extension UIButton {
///所有按钮的可点击区域不小于50*50
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds = self.bounds
let widthDelta = max(50.0 - bounds.size.width, 0)
let heightDelta = max(50.0 - bounds.size.height, 0)
bounds = bounds.insetBy(dx: -0.5 * widthDelta, dy: -0.5 * heightDelta)
return bounds.contains(point)
}
}
总结:
1.事件的传递是由UIApplication、UIWindow、UIView...从父view至子view递归的传递,直至找到最佳响应者。事件的响应是从子view至父view一层一层的传递,直至找到可以处理响应事件的view。
2.我们可以通过重写hitTest和pointInside来改变事件的响应链或修改view的响应区域。
关注公众号【一个老码农】免费获取iOS进阶学习视频
原文地址:事件的传递和响应
网友评论