记录一个有趣的东西。
我们知道,对于iOS和浏览器来说,页面上的控件都可以有点击事件,而对于父子关系的组件他们的点击事件在两个平台的表现却不同。
<div class="a" id="a">
<div class="b" id="b">
</div>
</div>
<style>
.a {
background: red;
width: 100px;
height: 100px;
}
.b {
position: relative;
// left: 100px; 位置错开
top: 0;
background: blue;
width: 50px;
height: 50px;
}
</style>
<script>
let a = document.getElementById('a')
let b = document.getElementById('b')
// 事件冒泡和捕获刚好是相反的的过程,
// 先由父级捕获并将捕获向子级传递,然后由子级将点击由子级向父级冒泡,
// 若父级捕获期间stopPropagation(),则后续事件全部中断,即子级无法捕获,无法响应点击,也无法向父级冒泡,
// 若子级捕获期间stopPropagation(),则后续事件全部中断,即子级无法响应点击,也无法向父级冒泡。
// 事件冒泡
a.addEventListener('click', (e) => {
// e.preventDefault()
console.log("click a")
})
b.addEventListener('click', (e) => {
// 阻止事件向父级方向冒泡
e.stopPropagation()
console.log("click b")
})
// 事件捕获
a.addEventListener('click', (e) => {
// 阻止捕获事件向子级方向传递
e.stopPropagation()
console.log('a catch')
}, true)
b.addEventListener('click', (e) => {
console.log('b catch')
}, true)
</script>
注意,这里有一个问题。如果父子并不是重叠的,而是互相错开,那么上述事件还会如期发生吗? 答案是会。也就是说它只要是父子关系即可,不需要位置上的重叠。
同样的,iOS也有点击事件,默认点在哪个控件上,就触发哪个控件的点击事件。
如果我要实现点击某个控件,点击事件穿透它到它下面的控件,触发后者的点击事件,可以将上层控件的isUserInteractionEnabled
设为false即可。 注意二者不必是父子关系,但必须是上下重叠关系 这跟js是不同的。
但是这样会带来一个问题,那就是上层控件内的所有子控件也将是不可点状态,这显然不能满足所有需求,此时完美的解决方案是,重写目标穿透控件的hitTest
或者point
方法,但要注意此时控件的isUserInteractionEnabled
须为true:
class TestView: UIView {
// 寻找此次事件最适合响应的view
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 父view在这个点上点击,最适合的当然是父view
let view = super.hitTest(point, with: event)
// 父view显然不等于当前view,故走else,返回父view
if view == self {
return nil
} else {
return view
}
}
// 判断point在不在方法调用者上
// hitTest方法底层会调用point方法,判断点在不在控件上。
// override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// for subview in subviews {
// if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
// return true
// }
// }
// return false
// }
}
这样就可以满足所有需求了。
这里事实上涉及的是iOS的事件传递和响应者链,传递和响应的方向刚好相反,这跟js捕获和冒泡是不是很像?
可以参考iOS事件传递和响应者链