需求:
- 有一个bottomView,这个View上面添加一些子控件,这些子控件需要能够响应点击事件。
- 有一个topView,这个View上面也添加了一些子控件,这些子控件需要能够响应点击事件。
- 需要把topView覆盖在bottomView上面,且不影响各自图层上面的事件点击/滑动。
应用场景:
一个多人语音/视频聊天直播间,bottomView多个人的头像展示或者视频展示,topView则承载了可滑动的聊天公屏。左滑把聊天公屏滑走,以便无干扰的观看直播画面,这时候点击这个画面,可以弹出对应主播的个人信息卡片。右滑的滑把聊天公屏滑入,点击聊天公屏上的头像、特权标志等,也要弹出对应的说明信息。这个左滑右滑就实现了一个清屏的功能。
实现:
可以把bottomView和topView都加到控制器的view上,作为同级视图的关系。
也可以把topView加到bottomView上,作为父视图和子视图的关系。
但是这些都不重要,重要的是如何处理事件的传递和响应关系。
事件传递和响应处理
如图,bottomView上有个红色按钮,topView(继承TouchBaseScrollView)上有个黄色按钮。点击黄色按钮需要响应黄色按钮的事件,点击没被黄色按钮挡住的红色按钮部分,要能透过topView响应红色按钮事件。
方法一:
优点: 超级简单,能满足简单的事件透传场景。(当然像多人直播间、聊天公屏这种复杂的场景难以满足需求)
缺点: 能传递点击事件,但是scrollView的滑动事件也会失效。
实现: 封装一个ScrollView,重写hitTest方法。清屏功能所用的ScrollView直接用这个封装好的ScrollView就行了。
class TouchBaseScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if self == view {
return nil
}
return view
}
}
方法二:
优点: 能满足实际复杂项目的需求,传递点击事件时,滑动事件也不回失效。(目前我们自己项目就用的这种方法)
缺点: 需要计算/转换点击坐标,判断控件是否在这个点击坐标内。实现比较复杂,尤其topView上子控件层级繁多时。
实现: 封装一个ScrollView,重写hitTest方法。清屏功能所用的
class TouchBaseScrollView: UIScrollView {
//点击事件传出去
var touchActionBlock: ((_ touch: UITouch, _ event: UIEvent) -> Void)?
//滑动不会触发touchesBegan,点击有事件响应的子控件也不回触发这里,只有点击scrollView空白处,才会得到这里
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
///只需要接收单指点击事件
if let count = event?.allTouches?.count, count > 1 { return }
guard let touch = touches.first else { return }
guard let event = event else { return }
//所有不需要响应的view,tag都打了99。这里不直接判读touch.view == self,是因为scrollView上可能还会加一个baseview啥的,用tag处理方便灵活。
if touch.view?.tag == 99 {
//点击点传出去给bottomView响应
touchActionBlock?(touch, event)
}
}
}
实现回调:
//点击事件处理
scrollView.touchActionBlock = {[weak self] (touch, event) in
self?.bottomView?.touchActionHandle(touch: touch, event: event)
}
private func touchActionHandle(touch: UITouch, event: UIEvent) {
let point = touch.location(in: self.view)
dprint("事件通知, 点: \(point)")
//右侧头像处理点击事件
let point1 = touch.location(in: self.micAvatarListView.scrollView)
let b = micAvatarListView.actionHandleWithClickPoint(point1, event: event)
//右侧头像列表不能响应,麦列表才需要响应
if b == false {
if vm.haveVideo {
//视频麦处理点击事件
micVideosView.actionHandleWithClickPoint(point, event: event)
}else {
//语音麦处理点击事件
let count = vm.microphones.value.count
micItemsView.actionHandleWithClickPoint(point, event: event, micCount: count)
}
}
}
func actionHandleWithClickPoint(_ point: CGPoint, event: UIEvent) {
//遍历所有的视频用户画布,看哪个画布能响应这个事件
for (_, view) in self.videoUsersView.enumerated() {
let point1 = self.convert(point, to: view)
if let _ = view.hitTest(point1, with: event) {
//响应事件,弹出用户资料卡片
view.clicked?(view.uid)
return
}
}
}
如果有人有更高明,更完美的方法,欢迎讨论!