Files
featherVoice/TUIKit/TUICallKit/TUICallKit-Swift/View/Component/FloatView/FloatingWindowSignalView.swift
2025-08-08 10:49:36 +08:00

337 lines
13 KiB
Swift

//
// FloatingWindow.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import UIKit
import TUICore
class FloatingWindowSignalView: UIView {
let viewModel = FloatingWindowViewModel()
weak var delegate: FloatingWindowViewDelegate?
let selfCallStatusObserver = Observer()
let remoteUserListObserver = Observer()
let callTimeObserver = Observer()
let kFloatingWindowVideoViewRect = CGRect(x: 0,
y: 0,
width: kMicroVideoViewWidth - 16.scaleWidth(),
height: kMicroVideoViewHeight - 16.scaleWidth())
var localPreView: VideoView {
if VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: viewModel.selfUser.value.id.value, frame: CGRect.zero)
}
VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
var remotePreView: VideoView {
guard let remoteUser = viewModel.remoteUserList.value.first else { return VideoView(frame: CGRect.zero) }
if VideoFactory.instance.viewMap[remoteUser.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: remoteUser.id.value, frame: CGRect.zero)
}
VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
let avatarImageView: UIImageView = {
let avatarImageView = UIImageView()
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.clipsToBounds = true
avatarImageView.layer.cornerRadius = 8.scaleWidth()
avatarImageView.isUserInteractionEnabled = false
return avatarImageView
}()
let containerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
containerView.layer.cornerRadius = 12.scaleWidth()
containerView.layer.masksToBounds = true
containerView.isUserInteractionEnabled = false
return containerView
}()
let audioContainerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
return containerView
}()
let shadowView: UIView = {
let shadowView = UIView()
shadowView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
shadowView.layer.shadowColor = UIColor.t_colorWithHexString(color: "353941").cgColor
shadowView.layer.shadowOpacity = 0.4
shadowView.layer.cornerRadius = 12.scaleWidth()
shadowView.layer.shadowRadius = 4.scaleWidth()
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
return shadowView
}()
let imageView: UIImageView = {
let imageView = UIImageView()
if let image = TUICallKitCommon.getBundleImage(name: "icon_float_dialing") {
imageView.image = image
}
imageView.isUserInteractionEnabled = false
return imageView
}()
let audioDescribeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.FloatingWindow.waitAccept") ?? ""
return describeLabel
}()
let videoDescribeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.FloatingWindow.waitAccept") ?? ""
return describeLabel
}()
lazy var timerLabel: UILabel = {
let timerLabel = UILabel()
timerLabel.font = UIFont.systemFont(ofSize: 12.0)
timerLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
timerLabel.textAlignment = .center
timerLabel.isUserInteractionEnabled = false
timerLabel.text = viewModel.getCallTimeString()
return timerLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserverState()
constructViewHierarchy()
activateConstraints()
bindInteraction()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
viewModel.selfCallStatus.removeObserver(selfCallStatusObserver)
viewModel.selfCallStatus.removeObserver(callTimeObserver)
}
func constructViewHierarchy() {
addSubview(shadowView)
addSubview(containerView)
containerView.addSubview(avatarImageView)
containerView.addSubview(audioContainerView)
audioContainerView.addSubview(imageView)
audioContainerView.addSubview(audioDescribeLabel)
audioContainerView.addSubview(timerLabel)
containerView.addSubview(videoDescribeLabel)
}
func activateConstraints() {
shadowView.snp.makeConstraints { make in
make.edges.equalTo(containerView)
}
containerView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.top.equalTo(8.scaleWidth())
}
avatarImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(45.scaleWidth())
}
audioContainerView.snp.makeConstraints { make in
make.top.centerX.equalTo(containerView)
make.width.height.equalTo(72.scaleWidth())
}
imageView.snp.makeConstraints { make in
make.top.equalTo(audioContainerView).offset(8.scaleWidth())
make.centerX.equalTo(audioContainerView)
make.width.height.equalTo(36.scaleWidth())
}
audioDescribeLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(4.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
timerLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(4.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
videoDescribeLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(containerView)
make.bottom.equalTo(containerView).offset(-8.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
}
func bindInteraction() {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(tapGesture: )))
let pan = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(panGesture: )))
self.addGestureRecognizer(tap)
pan.require(toFail: tap)
self.addGestureRecognizer(pan)
}
func clearSubview() {
for subview in subviews {
subview.removeFromSuperview()
}
}
// MARK: Action Event
@objc func tapGestureAction(tapGesture: UITapGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
self.delegate?.tapGestureAction(tapGesture: tapGesture)
}
}
@objc func panGestureAction(panGesture: UIPanGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
self.delegate?.panGestureAction(panGesture: panGesture)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserverState() {
registerCallStatusObserver()
registerCallTimeObserver()
}
func registerCallStatusObserver() {
viewModel.selfCallStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateUI()
})
}
func registerCallTimeObserver() {
viewModel.callTime.addObserver(callTimeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
DispatchCallKitMainAsyncSafe {
self.timerLabel.text = self.viewModel.getCallTimeString()
}
})
}
func userVideoAvailableChange() {
if viewModel.remoteUserList.value.first != nil {
viewModel.remoteUserList.value.first?.videoAvailable.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateUI()
})
}
}
// MARK: Update UI
func updateUI() {
cleanView()
if viewModel.mediaType.value == .audio {
updateSingleAudioUI()
} else if viewModel.mediaType.value == .video {
updateSingleVideoUI()
}
}
func updateSingleAudioUI() {
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "FFFFFF")
if viewModel.selfCallStatus.value == .waiting {
setSingleAudioWaitingUI()
} else if viewModel.selfCallStatus.value == .accept {
setSingleAudioAcceptUI()
}
}
func updateSingleVideoUI() {
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "303132")
if viewModel.selfCallStatus.value == .waiting {
setSingleVideoWaitingUI()
} else if viewModel.selfCallStatus.value == .accept {
guard let remoteUser = viewModel.remoteUserList.value.first else { return }
if remoteUser.videoAvailable.value == true {
setSingleVideoAcceptUI()
} else {
setSingleVideoAcceptWithUnavailableUI()
}
}
userVideoAvailableChange()
}
func setSingleAudioWaitingUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
audioDescribeLabel.isHidden = false
}
func setSingleAudioAcceptUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
timerLabel.isHidden = false
}
func setSingleVideoWaitingUI() {
localPreView.frame = kFloatingWindowVideoViewRect
videoDescribeLabel.isHidden = false
containerView.addSubview(localPreView)
containerView.bringSubviewToFront(videoDescribeLabel)
}
func setSingleVideoAcceptUI() {
remotePreView.frame = kFloatingWindowVideoViewRect
remotePreView.isHidden = false
containerView.addSubview(remotePreView)
guard let remoteUser = self.viewModel.remoteUserList.value.first else { return }
viewModel.startRemoteView(user: remoteUser, videoView: remotePreView)
}
func setSingleVideoAcceptWithUnavailableUI() {
avatarImageView.isHidden = false
remotePreView.isHidden = true
containerView.bringSubviewToFront(avatarImageView)
guard let remoteUser = viewModel.remoteUserList.value.first else { return }
let userIcon: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if remoteUser.avatar.value == "" {
guard let image = userIcon else { return }
avatarImageView.image = image
} else {
avatarImageView.sd_setImage(with: URL(string: remoteUser.avatar.value), placeholderImage: userIcon)
}
}
func cleanView() {
avatarImageView.isHidden = true
audioContainerView.isHidden = true
imageView.isHidden = true
audioDescribeLabel.isHidden = true
timerLabel.isHidden = true
videoDescribeLabel.isHidden = true
}
}