提交
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// CallWaitingHintView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CallWaitingHintView: UIView {
|
||||
|
||||
|
||||
let label: UILabel = {
|
||||
let waitingInviteLabel = UILabel(frame: CGRect.zero)
|
||||
waitingInviteLabel.textColor = UIColor.t_colorWithHexString(color: "#999999")
|
||||
waitingInviteLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
|
||||
waitingInviteLabel.backgroundColor = UIColor.clear
|
||||
waitingInviteLabel.textAlignment = .center
|
||||
waitingInviteLabel.text = TUICallKitLocalize(key: "TUICallKit.calleeTip")
|
||||
return waitingInviteLabel
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(label)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.label.snp.makeConstraints { make in
|
||||
make.centerX.size.equalTo(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// BaseControlButton.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
typealias ButtonActionBlock = (_ sender: UIButton) -> Void
|
||||
|
||||
class BaseControlButton: UIView {
|
||||
|
||||
var buttonActionBlock: ButtonActionBlock?
|
||||
var imageSize: CGSize
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let titleLabel = UILabel(frame: CGRect.zero)
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 12.0)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.numberOfLines = 2
|
||||
titleLabel.lineBreakMode = .byTruncatingTail
|
||||
return titleLabel
|
||||
}()
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
return button
|
||||
}()
|
||||
|
||||
static func create(frame: CGRect, title: String, imageSize: CGSize, buttonAction: @escaping ButtonActionBlock) -> BaseControlButton {
|
||||
let controlButton = BaseControlButton(frame: frame, imageSize: imageSize)
|
||||
controlButton.titleLabel.text = title
|
||||
controlButton.buttonActionBlock = buttonAction
|
||||
return controlButton
|
||||
}
|
||||
|
||||
init(frame: CGRect, imageSize: CGSize) {
|
||||
self.imageSize = imageSize
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor.clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(button)
|
||||
addSubview(titleLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.equalTo(self)
|
||||
make.centerX.equalTo(self)
|
||||
make.size.equalTo(imageSize)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self)
|
||||
make.top.equalTo(button.snp.bottom).offset(10)
|
||||
make.width.equalTo(100.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
button.addTarget(self, action: #selector(buttonActionEvent(sender: )), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: Update Info
|
||||
func updateImage(image: UIImage) {
|
||||
button.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
|
||||
func updateTitle(title: String) {
|
||||
titleLabel.text = title
|
||||
}
|
||||
|
||||
func updateTitleColor(titleColor: UIColor) {
|
||||
titleLabel.textColor = titleColor
|
||||
}
|
||||
|
||||
// MARK: Event Action
|
||||
@objc func buttonActionEvent(sender: UIButton) {
|
||||
guard let buttonActionBlock = buttonActionBlock else { return }
|
||||
buttonActionBlock(sender)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// UIButtonCommon.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TUIButtonEdgeInsetsStyle {
|
||||
case TUIButtonEdgeInsetsStyleTop
|
||||
case TUIButtonEdgeInsetsStyleLeft
|
||||
case TUIButtonEdgeInsetsStyleBottom
|
||||
case TUIButtonEdgeInsetsStyleRight
|
||||
}
|
||||
|
||||
extension UIButton {
|
||||
|
||||
func layoutButtonWithEdgeInsetsStyle(style: TUIButtonEdgeInsetsStyle, space: CGFloat) {
|
||||
let imageWidth = imageView?.frame.size.width ?? 0.0
|
||||
let imageHeight = imageView?.frame.size.height ?? 0.0
|
||||
let labelWidth = titleLabel?.intrinsicContentSize.width ?? 0.0
|
||||
let labelHeight = titleLabel?.intrinsicContentSize.height ?? 0.0
|
||||
var imageEdgeInsets: UIEdgeInsets = .zero
|
||||
var labelEdgeInsets: UIEdgeInsets = .zero
|
||||
switch style {
|
||||
case .TUIButtonEdgeInsetsStyleTop:
|
||||
imageEdgeInsets = UIEdgeInsets(top: -labelHeight - space / 2.0, left: 0, bottom: 0, right: -labelHeight)
|
||||
labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth, bottom: -labelHeight - space / 2.0, right: 0)
|
||||
break
|
||||
case .TUIButtonEdgeInsetsStyleLeft:
|
||||
imageEdgeInsets = UIEdgeInsets(top: 0, left: -space / 2.0, bottom: 0, right: space / 2.0)
|
||||
labelEdgeInsets = UIEdgeInsets(top: 0, left: space / 2.0, bottom: 0 , right: -space / 2.0)
|
||||
break
|
||||
case .TUIButtonEdgeInsetsStyleBottom:
|
||||
imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -labelHeight - space / 2.0, right: -labelWidth)
|
||||
labelEdgeInsets = UIEdgeInsets(top: -imageHeight - space / 2.0, left: -imageWidth, bottom: 0 , right: 0)
|
||||
break
|
||||
case .TUIButtonEdgeInsetsStyleRight:
|
||||
imageEdgeInsets = UIEdgeInsets(top: 0, left: labelWidth+space / 2.0, bottom: 0, right: -labelWidth - space / 2.0)
|
||||
labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth - space / 2.0, bottom: 0 , right: imageWidth + space / 2.0)
|
||||
break
|
||||
}
|
||||
self.titleEdgeInsets = labelEdgeInsets
|
||||
self.imageEdgeInsets = imageEdgeInsets
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// UIColorCommon.swift
|
||||
// TUICallKitSwift
|
||||
//
|
||||
// Created by vincepzhang on 2023/1/3.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
|
||||
static func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
|
||||
var colorImage: UIImage
|
||||
var rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
UIGraphicsBeginImageContext(rect.size)
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(rect)
|
||||
guard let cImage = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() }
|
||||
colorImage = cImage
|
||||
UIGraphicsEndImageContext()
|
||||
return colorImage
|
||||
}
|
||||
|
||||
static func t_colorWithHexString(color: String) -> UIColor {
|
||||
return UIColor.t_colorWithHexString(color: color, alpha: 1)
|
||||
}
|
||||
|
||||
static func t_colorWithHexString(color: String, alpha: CGFloat) -> UIColor {
|
||||
var colorString: String = color.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
|
||||
|
||||
if colorString.count < 6 {
|
||||
return UIColor.clear
|
||||
}
|
||||
|
||||
if colorString.hasPrefix("0X") {
|
||||
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 2))
|
||||
}
|
||||
|
||||
if colorString.hasPrefix("#") {
|
||||
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 1))
|
||||
}
|
||||
|
||||
if colorString.count < 6 {
|
||||
return UIColor.clear
|
||||
}
|
||||
|
||||
let rString = String(colorString.prefix(2))
|
||||
colorString.removeFirst(2)
|
||||
let gString = String(colorString.prefix(2))
|
||||
colorString.removeFirst(2)
|
||||
let bString = String(colorString.prefix(2))
|
||||
|
||||
var r = Int32()
|
||||
var g = Int32()
|
||||
var b = Int32()
|
||||
Scanner(string: rString).scanHexInt32(&r)
|
||||
Scanner(string: gString).scanHexInt32(&g)
|
||||
Scanner(string: bString).scanHexInt32(&b)
|
||||
|
||||
return UIColor(red: CGFloat(r) / 255.0 , green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: alpha)
|
||||
}
|
||||
|
||||
static func t_colorWithAlphaHexString(color: String) -> UIColor {
|
||||
var colorString: String = color.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
|
||||
|
||||
if colorString.count < 6 {
|
||||
return UIColor.clear
|
||||
}
|
||||
|
||||
if colorString.hasPrefix("0X") {
|
||||
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 2))
|
||||
}
|
||||
|
||||
if colorString.hasPrefix("#") {
|
||||
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 1))
|
||||
}
|
||||
|
||||
if colorString.count < 6 {
|
||||
return UIColor.clear
|
||||
}
|
||||
|
||||
let rString = String(colorString.prefix(2))
|
||||
colorString.removeFirst(2)
|
||||
let gString = String(colorString.prefix(2))
|
||||
colorString.removeFirst(2)
|
||||
let bString = String(colorString.prefix(2))
|
||||
colorString.removeFirst(2)
|
||||
let alphaString = String(colorString.prefix(2))
|
||||
|
||||
var r: Float = Float()
|
||||
var g: Float = Float()
|
||||
var b: Float = Float()
|
||||
var alpha: Float = Float()
|
||||
Scanner(string: rString).scanHexFloat(&r)
|
||||
Scanner(string: gString).scanHexFloat(&g)
|
||||
Scanner(string: bString).scanHexFloat(&b)
|
||||
Scanner(string: alphaString).scanHexFloat(&alpha)
|
||||
|
||||
return UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(alpha))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// UIViewControllerCommon.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/1/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
static func getCurrentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
|
||||
if let nav = base as? UINavigationController {
|
||||
return getCurrentViewController(base: nav.visibleViewController)
|
||||
}
|
||||
if let tab = base as? UITabBarController {
|
||||
return getCurrentViewController(base: tab.selectedViewController)
|
||||
}
|
||||
if let presented = base?.presentedViewController {
|
||||
return getCurrentViewController(base: presented)
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import UIKit
|
||||
|
||||
extension UIWindow {
|
||||
public func t_makeKeyAndVisible() {
|
||||
if #available(iOS 13.0, *) {
|
||||
for windowScene in UIApplication.shared.connectedScenes {
|
||||
if windowScene.activationState == UIScene.ActivationState.foregroundActive ||
|
||||
windowScene.activationState == UIScene.ActivationState.background {
|
||||
self.windowScene = windowScene as? UIWindowScene
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
self.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
static func getKeyWindow() -> UIWindow? {
|
||||
var keyWindow: UIWindow?
|
||||
if #available(iOS 13, *) {
|
||||
keyWindow = UIApplication.shared.connectedScenes
|
||||
.filter({ $0.activationState == .foregroundActive })
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
.first(where: { $0.isKeyWindow })
|
||||
} else {
|
||||
keyWindow = UIApplication.shared.keyWindow
|
||||
}
|
||||
return keyWindow
|
||||
}
|
||||
|
||||
static func getTopFullscreenWindow() -> UIWindow? {
|
||||
let topWindow = UIApplication.shared.windows
|
||||
.filter { !$0.isHidden && $0.bounds.equalTo(UIScreen.main.bounds) }
|
||||
.max(by: { $0.windowLevel.rawValue < $1.windowLevel.rawValue })
|
||||
|
||||
return topWindow
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FloatWindowViewController.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICore
|
||||
|
||||
protocol FloatingWindowViewDelegate: NSObject {
|
||||
func tapGestureAction(tapGesture: UITapGestureRecognizer)
|
||||
func panGestureAction(panGesture: UIPanGestureRecognizer)
|
||||
}
|
||||
|
||||
class FloatWindowViewController: UIViewController, FloatingWindowViewDelegate {
|
||||
|
||||
let callEventObserver = Observer()
|
||||
weak var delegate: FloatingWindowViewDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if TUICallState.instance.scene.value == .group {
|
||||
addFloatingWindowGroupView()
|
||||
} else if TUICallState.instance.scene.value == .single {
|
||||
addFloatingWindowSignalView()
|
||||
}
|
||||
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.event.removeObserver(callEventObserver)
|
||||
for view in view.subviews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func addFloatingWindowGroupView() {
|
||||
let floatView = FloatingWindowGroupView(frame: CGRect.zero)
|
||||
view.addSubview(floatView)
|
||||
floatView.snp.makeConstraints { make in
|
||||
make.center.equalTo(self.view)
|
||||
make.size.equalTo(self.view)
|
||||
}
|
||||
floatView.delegate = self
|
||||
}
|
||||
|
||||
func addFloatingWindowSignalView() {
|
||||
let floatView = FloatingWindowSignalView(frame: CGRect.zero)
|
||||
view.addSubview(floatView)
|
||||
floatView.snp.makeConstraints { make in
|
||||
make.center.equalTo(self.view)
|
||||
make.size.equalTo(self.view)
|
||||
}
|
||||
floatView.delegate = self
|
||||
}
|
||||
|
||||
// MARK: FloatingWindowViewDelegate
|
||||
func tapGestureAction(tapGesture: UITapGestureRecognizer) {
|
||||
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
|
||||
self.delegate?.tapGestureAction(tapGesture: tapGesture)
|
||||
}
|
||||
}
|
||||
|
||||
func panGestureAction(panGesture: UIPanGestureRecognizer) {
|
||||
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
|
||||
self.delegate?.panGestureAction(panGesture: panGesture)
|
||||
}
|
||||
}
|
||||
|
||||
func registerObserver() {
|
||||
TUICallState.instance.event.addObserver(callEventObserver) { newValue, _ in
|
||||
if newValue.eventType == .ERROR {
|
||||
guard let errorCode = newValue.param[EVENT_KEY_CODE] as? Int32 else { return }
|
||||
guard let errorMessage = newValue.param[EVENT_KEY_MESSAGE] as? String else { return }
|
||||
TUITool.makeToast("error:\(errorCode):\(errorMessage)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// FloatingWindowButton.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FloatingWindowButton: UIView {
|
||||
|
||||
let mediaTypeObserver = Observer()
|
||||
|
||||
let floatButton: FloatingWindowCustomButton = {
|
||||
let floatButton = FloatingWindowCustomButton(type: .system)
|
||||
return floatButton
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
updateImage()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(floatButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
floatButton.snp.makeConstraints { make in
|
||||
make.center.equalTo(self)
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
floatButton.addTarget(self, action: #selector(clickFloatButton(sender: )), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
@objc func clickFloatButton(sender: UIButton) {
|
||||
WindowManager.instance.showFloatWindow()
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.mediaType.addObserver(mediaTypeObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateImage()
|
||||
}
|
||||
}
|
||||
|
||||
func updateImage() {
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_min_window") {
|
||||
floatButton.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingWindowCustomButton: UIButton {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let expandedBounds = bounds.insetBy(dx: -6, dy: -6)
|
||||
return expandedBounds.contains(point) ? self : nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// FloatingWindowStatusView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FloatingWindowGroupStatusView: UIView {
|
||||
|
||||
let isCameraOpenObserver = Observer()
|
||||
let isMicMuteObserver = Observer()
|
||||
|
||||
let videoImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_float_group_video_on" : "icon_float_group_video_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let audioImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let imageName = TUICallState.instance.isMicMute.value ? "icon_float_group_audio_off" : "icon_float_group_audio_on"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_group_status_view_bg_color",
|
||||
defaultHex: "#F9F6F4")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
|
||||
TUICallState.instance.isMicMute.removeObserver(isMicMuteObserver)
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(videoImageView)
|
||||
addSubview(audioImageView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
videoImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self)
|
||||
make.leading.equalTo(self).offset(14.scaleWidth())
|
||||
make.width.height.equalTo(16.scaleWidth())
|
||||
}
|
||||
audioImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self)
|
||||
make.trailing.equalTo(self).offset(-14.scaleWidth())
|
||||
make.width.height.equalTo(16.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
isCameraOpenChange()
|
||||
isMicMuteChange()
|
||||
}
|
||||
|
||||
func isCameraOpenChange() {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
let imageName = newValue ? "icon_float_group_video_on" : "icon_float_group_video_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
self.videoImageView.image = image
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func isMicMuteChange() {
|
||||
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
let imageName = newValue ? "icon_float_group_audio_off" : "icon_float_group_audio_on"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
self.audioImageView.image = image
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
//
|
||||
// FloatingWindowGroupView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TUICore
|
||||
|
||||
class FloatingWindowGroupView: UIView {
|
||||
|
||||
let viewModel = FloatingWindowViewModel()
|
||||
weak var delegate: FloatingWindowViewDelegate?
|
||||
|
||||
let selfCallStatusObserver = Observer()
|
||||
let remoteVideoAvailableObserver = Observer()
|
||||
let callTimeObserver = Observer()
|
||||
let currentSpeakUserObserver = Observer()
|
||||
let currentSpeakUserVideoAvailableObserver = Observer()
|
||||
|
||||
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 {
|
||||
let remoteUser = viewModel.currentSpeakUser.value
|
||||
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.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.shadowRadius = 4.scaleWidth()
|
||||
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
|
||||
shadowView.layer.cornerRadius = 12.scaleWidth()
|
||||
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
|
||||
}()
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
let groupNameLabel: UILabel = {
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.font = UIFont.systemFont(ofSize: 12.0)
|
||||
nameLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
|
||||
nameLabel.textAlignment = .center
|
||||
nameLabel.isUserInteractionEnabled = false
|
||||
return nameLabel
|
||||
}()
|
||||
|
||||
let groupStatusView: UIView = {
|
||||
let statusView = FloatingWindowGroupStatusView()
|
||||
return statusView
|
||||
}()
|
||||
|
||||
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(currentSpeakUserObserver)
|
||||
viewModel.currentSpeakUser.value.videoAvailable.removeObserver(currentSpeakUserVideoAvailableObserver)
|
||||
viewModel.selfCallStatus.removeObserver(callTimeObserver)
|
||||
for index in 0..<viewModel.remoteUserList.value.count {
|
||||
guard index < viewModel.remoteUserList.value.count else {
|
||||
break
|
||||
}
|
||||
viewModel.remoteUserList.value[index].videoAvailable.removeObserver(remoteVideoAvailableObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(shadowView)
|
||||
addSubview(containerView)
|
||||
containerView.addSubview(avatarImageView)
|
||||
containerView.addSubview(audioContainerView)
|
||||
audioContainerView.addSubview(imageView)
|
||||
audioContainerView.addSubview(audioDescribeLabel)
|
||||
audioContainerView.addSubview(timerLabel)
|
||||
containerView.addSubview(groupNameLabel)
|
||||
containerView.addSubview(groupStatusView)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
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(5.scaleWidth())
|
||||
make.height.equalTo(20.scaleWidth())
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.top.centerX.width.equalToSuperview()
|
||||
make.bottom.equalTo(groupStatusView.snp.top)
|
||||
}
|
||||
groupNameLabel.snp.makeConstraints { make in
|
||||
make.centerX.width.equalTo(containerView)
|
||||
make.bottom.equalTo(groupStatusView.snp.top)
|
||||
make.height.equalTo(20.scaleWidth())
|
||||
}
|
||||
groupStatusView.snp.makeConstraints { make in
|
||||
make.centerX.width.equalTo(containerView)
|
||||
make.bottom.equalTo(containerView)
|
||||
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()
|
||||
registerCurrentSpeakUserObserver()
|
||||
registerCurrentSpeakUserVideoAvailableObserver()
|
||||
registerCallTimeObserver()
|
||||
registerUserVideoAvailableChange()
|
||||
}
|
||||
|
||||
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 registerCurrentSpeakUserObserver() {
|
||||
viewModel.currentSpeakUser.addObserver(currentSpeakUserObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if self.viewModel.scene.value == .group {
|
||||
self.updateUI()
|
||||
self.groupNameLabel.text = newValue.nickname.value
|
||||
self.avatarImageView.sd_setImage(with: URL(string: newValue.avatar.value),
|
||||
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func registerCurrentSpeakUserVideoAvailableObserver() {
|
||||
viewModel.currentSpeakUser.value.videoAvailable.addObserver(currentSpeakUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if self.viewModel.scene.value == .group {
|
||||
self.updateUI()
|
||||
self.groupNameLabel.text = self.viewModel.currentSpeakUser.value.nickname.value
|
||||
self.avatarImageView.sd_setImage(with: URL(string: self.viewModel.currentSpeakUser.value.avatar.value),
|
||||
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func registerUserVideoAvailableChange() {
|
||||
for index in 0..<viewModel.remoteUserList.value.count {
|
||||
guard index < viewModel.remoteUserList.value.count else {
|
||||
break
|
||||
}
|
||||
viewModel.remoteUserList.value[index].videoAvailable.addObserver(remoteVideoAvailableObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if (self.viewModel.currentSpeakUser.value.id.value == self.viewModel.remoteUserList.value[index].id.value) {
|
||||
self.updateUI()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func updateUI() {
|
||||
cleanView()
|
||||
|
||||
if viewModel.selfCallStatus.value == .waiting {
|
||||
setGroupWaitingUI()
|
||||
} else if viewModel.selfCallStatus.value == .accept {
|
||||
setGroupAcceptUI()
|
||||
}
|
||||
}
|
||||
|
||||
func setGroupWaitingUI() {
|
||||
audioContainerView.isHidden = false
|
||||
imageView.isHidden = false
|
||||
audioDescribeLabel.isHidden = false
|
||||
groupStatusView.isHidden = false
|
||||
}
|
||||
|
||||
func setGroupAcceptUI() {
|
||||
if viewModel.currentSpeakUser.value.id.value == viewModel.selfUser.value.id.value {
|
||||
setGroupLocalUI()
|
||||
} else if viewModel.currentSpeakUser.value.id.value.count > 0 {
|
||||
setGroupRemoteUI()
|
||||
} else {
|
||||
setGroupDefineUI()
|
||||
}
|
||||
}
|
||||
|
||||
func setGroupDefineUI() {
|
||||
audioContainerView.isHidden = false
|
||||
imageView.isHidden = false
|
||||
timerLabel.isHidden = false
|
||||
groupStatusView.isHidden = false
|
||||
}
|
||||
|
||||
func setGroupLocalUI() {
|
||||
groupStatusView.isHidden = false
|
||||
groupNameLabel.isHidden = false
|
||||
|
||||
if TUICallState.instance.isCameraOpen.value {
|
||||
setGroupLocalVideoAvailableUI()
|
||||
} else {
|
||||
setGroupVideoNotAvailableUI()
|
||||
}
|
||||
|
||||
containerView.bringSubviewToFront(groupNameLabel)
|
||||
}
|
||||
|
||||
func setGroupRemoteUI() {
|
||||
groupStatusView.isHidden = false
|
||||
groupNameLabel.isHidden = false
|
||||
|
||||
if viewModel.currentSpeakUser.value.videoAvailable.value {
|
||||
setGroupRemoteVideoAvailableUI()
|
||||
} else {
|
||||
setGroupVideoNotAvailableUI()
|
||||
}
|
||||
|
||||
containerView.bringSubviewToFront(groupNameLabel)
|
||||
}
|
||||
|
||||
func setGroupLocalVideoAvailableUI() {
|
||||
avatarImageView.isHidden = true
|
||||
containerView.addSubview(localPreView)
|
||||
localPreView.snp.remakeConstraints { make in
|
||||
make.edges.equalTo(avatarImageView)
|
||||
}
|
||||
}
|
||||
|
||||
func setGroupRemoteVideoAvailableUI() {
|
||||
avatarImageView.isHidden = true
|
||||
containerView.addSubview(remotePreView)
|
||||
remotePreView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(avatarImageView)
|
||||
}
|
||||
viewModel.startRemoteView(user: viewModel.currentSpeakUser.value, videoView: remotePreView)
|
||||
}
|
||||
|
||||
func setGroupVideoNotAvailableUI() {
|
||||
avatarImageView.isHidden = false
|
||||
containerView.bringSubviewToFront(avatarImageView)
|
||||
}
|
||||
|
||||
func cleanView() {
|
||||
avatarImageView.isHidden = true
|
||||
audioContainerView.isHidden = true
|
||||
imageView.isHidden = true
|
||||
audioDescribeLabel.isHidden = true
|
||||
timerLabel.isHidden = true
|
||||
groupStatusView.isHidden = true
|
||||
groupNameLabel.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// AudioAndVideoCalleeWaitingView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICallEngine
|
||||
|
||||
class AudioAndVideoCalleeWaitingView: UIView {
|
||||
|
||||
lazy var acceptBtn: BaseControlButton = {
|
||||
weak var weakSelf = self
|
||||
let acceptBtn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.answer") ?? "",
|
||||
imageSize: kBtnSmallSize) { sender in
|
||||
weakSelf?.acceptTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_dialing") {
|
||||
acceptBtn.updateImage(image: image)
|
||||
}
|
||||
acceptBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return acceptBtn
|
||||
}()
|
||||
|
||||
lazy var rejectBtn: BaseControlButton = {
|
||||
weak var weakSelf = self
|
||||
let rejectBtn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.decline") ?? "",
|
||||
imageSize: kBtnSmallSize) { sender in
|
||||
weakSelf?.rejectTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
|
||||
rejectBtn.updateImage(image: image)
|
||||
}
|
||||
rejectBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return rejectBtn
|
||||
}()
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(rejectBtn)
|
||||
addSubview(acceptBtn)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
rejectBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 80.scaleWidth() : -80.scaleWidth())
|
||||
make.top.bottom.equalTo(self)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
acceptBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -80.scaleWidth() : 80.scaleWidth())
|
||||
make.top.bottom.equalTo(self)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Event Action
|
||||
func rejectTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.reject()
|
||||
}
|
||||
|
||||
func acceptTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.accept()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// AudioCallerWaitingAndAcceptedView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TUICallEngine
|
||||
|
||||
class AudioCallerWaitingAndAcceptedView : UIView {
|
||||
|
||||
let isMicMuteObserver = Observer()
|
||||
let audioDeviceObserver = Observer()
|
||||
|
||||
lazy var muteMicBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
|
||||
let muteAudioBtn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.muteMicEvent(sender: sender)
|
||||
})
|
||||
|
||||
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
muteAudioBtn.updateImage(image: image)
|
||||
}
|
||||
muteAudioBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return muteAudioBtn
|
||||
}()
|
||||
|
||||
lazy var hangupBtn: BaseControlButton = {
|
||||
let hangupBtn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.hangup") ?? "",
|
||||
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.hangupEvent(sender: sender)
|
||||
})
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
|
||||
hangupBtn.updateImage(image: image)
|
||||
}
|
||||
hangupBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return hangupBtn
|
||||
}()
|
||||
|
||||
lazy var changeSpeakerBtn: BaseControlButton = {
|
||||
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
|
||||
let changeSpeakerBtn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.changeSpeakerEvent(sender: sender)
|
||||
})
|
||||
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
changeSpeakerBtn.updateImage(image: image)
|
||||
}
|
||||
changeSpeakerBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return changeSpeakerBtn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.isMicMute.removeObserver(isMicMuteObserver)
|
||||
TUICallState.instance.audioDevice.removeObserver(audioDeviceObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(muteMicBtn)
|
||||
addSubview(hangupBtn)
|
||||
addSubview(changeSpeakerBtn)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
muteMicBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
|
||||
make.centerY.equalTo(hangupBtn)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
hangupBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self)
|
||||
make.bottom.equalTo(self.snp.bottom)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
changeSpeakerBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
|
||||
make.centerY.equalTo(self.hangupBtn)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
func muteMicEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.muteMic()
|
||||
}
|
||||
|
||||
func changeSpeakerEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.changeSpeaker()
|
||||
}
|
||||
|
||||
func hangupEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.hangup()
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateMuteAudioBtn(mute: newValue)
|
||||
})
|
||||
|
||||
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
|
||||
})
|
||||
}
|
||||
|
||||
func updateMuteAudioBtn(mute: Bool) {
|
||||
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
|
||||
muteMicBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func updateChangeSpeakerBtn(isSpeaker: Bool) {
|
||||
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
|
||||
changeSpeakerBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
//
|
||||
// GroupCallerAndCalleeAcceptedView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/8.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SnapKit
|
||||
|
||||
protocol GroupCallerAndCalleeAcceptedViewDelegate: AnyObject {
|
||||
func showAnimation()
|
||||
func restoreExpansion()
|
||||
func handleTransform(animationScale: CGFloat)
|
||||
}
|
||||
|
||||
let groupFunctionAnimationDuration = 0.3
|
||||
let groupFunctionBaseControlBtnHeight = 60.scaleWidth() + 5.scaleHeight() + 20
|
||||
let groupFunctionBottomHeight = Bottom_SafeHeight > 1 ? Bottom_SafeHeight : 8
|
||||
let groupFunctionViewHeight = 22 + groupFunctionBaseControlBtnHeight + 20.scaleHeight() + 60.scaleWidth() + groupFunctionBottomHeight
|
||||
let groupSmallFunctionViewHeight = 22 + 60.scaleWidth() + groupFunctionBottomHeight
|
||||
|
||||
class GroupCallerAndCalleeAcceptedView: UIView {
|
||||
|
||||
weak var delegate: GroupCallerAndCalleeAcceptedViewDelegate?
|
||||
|
||||
let isCameraOpenObserver = Observer()
|
||||
let showLargeViewUserIdObserver = Observer()
|
||||
let isMicMuteObserver = Observer()
|
||||
let audioDeviceObserver = Observer()
|
||||
|
||||
var isShowLittleContainerView = false
|
||||
var panGestureBeganY = 0.0
|
||||
|
||||
lazy var containerView: UIView = {
|
||||
let containerView = UIView(frame: CGRect.zero)
|
||||
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "#4F586B")
|
||||
containerView.alpha = 0.5
|
||||
containerView.isUserInteractionEnabled = true
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
|
||||
containerView.addGestureRecognizer(panGesture)
|
||||
return containerView
|
||||
}()
|
||||
|
||||
lazy var muteMicBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.muteMicEvent(sender: sender)
|
||||
}
|
||||
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var closeCameraBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.closeCameraTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off") {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var changeSpeakerBtn: BaseControlButton = {
|
||||
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.changeSpeakerEvent(sender: sender)
|
||||
}
|
||||
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var hangupBtn: BaseControlButton = {
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.hangupTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var matchBtn: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.addTarget(self,action:#selector(matchTouchEvent(sender:)), for: .touchUpInside)
|
||||
btn.setBackgroundImage(TUICallKitCommon.getBundleImage(name: "icon_match"), for: .normal)
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
|
||||
TUICallState.instance.showLargeViewUserId.removeObserver(showLargeViewUserIdObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
setContainerViewCorner()
|
||||
let isHidden: Bool = (TUICallState.instance.showLargeViewUserId.value.count <= 1)
|
||||
containerView.isHidden = isHidden
|
||||
matchBtn.isHidden = isHidden
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(containerView)
|
||||
addSubview(muteMicBtn)
|
||||
addSubview(changeSpeakerBtn)
|
||||
addSubview(closeCameraBtn)
|
||||
addSubview(hangupBtn)
|
||||
addSubview(matchBtn)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
let top = 22.0
|
||||
containerView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: frame.size.width, height: groupFunctionViewHeight))
|
||||
|
||||
muteMicBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
|
||||
make.centerY.equalTo(changeSpeakerBtn)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
changeSpeakerBtn.snp.makeConstraints { make in
|
||||
make.top.equalTo(self).offset(top)
|
||||
make.centerX.equalTo(self)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
closeCameraBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
|
||||
make.centerY.equalTo(self.changeSpeakerBtn)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
hangupBtn.snp.makeConstraints { make in
|
||||
make.top.equalTo(changeSpeakerBtn.snp.bottom).offset(5.scaleHeight() + 20 + 20.scaleHeight())
|
||||
make.centerX.equalTo(self)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
matchBtn.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(hangupBtn)
|
||||
make.leading.width.height.equalTo(30.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateCloseCameraBtn(open: newValue)
|
||||
})
|
||||
|
||||
TUICallState.instance.showLargeViewUserId.addObserver(showLargeViewUserIdObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if newValue.count > 1 {
|
||||
self.showAnimation()
|
||||
self.containerView.isHidden = false
|
||||
self.matchBtn.isHidden = false
|
||||
} else {
|
||||
self.restoreExpansion()
|
||||
}
|
||||
}
|
||||
|
||||
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateMuteAudioBtn(mute: newValue)
|
||||
})
|
||||
|
||||
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
func muteMicEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.muteMic()
|
||||
updateMuteAudioBtn(mute: TUICallState.instance.isMicMute.value == true)
|
||||
}
|
||||
|
||||
func closeCameraTouchEvent(sender: UIButton) {
|
||||
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
|
||||
if TUICallState.instance.isCameraOpen.value == true {
|
||||
CallEngineManager.instance.closeCamera()
|
||||
} else {
|
||||
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
|
||||
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
|
||||
}
|
||||
}
|
||||
|
||||
func changeSpeakerEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.changeSpeaker()
|
||||
updateChangeSpeakerBtn(isSpeaker: TUICallState.instance.audioDevice.value == .speakerphone)
|
||||
}
|
||||
|
||||
@objc func hangupTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.hangup()
|
||||
}
|
||||
|
||||
@objc func matchTouchEvent(sender: UIButton) {
|
||||
if isShowLittleContainerView {
|
||||
restoreExpansion()
|
||||
} else {
|
||||
showAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: containerView)
|
||||
let scale = ((translation.y - panGestureBeganY) / 105)
|
||||
|
||||
if gesture.state == .began {
|
||||
panGestureBeganY = translation.y
|
||||
} else if gesture.state == .changed {
|
||||
if scale > 0 && !isShowLittleContainerView {
|
||||
// Swipe down
|
||||
handleTransform(animationScale: (scale > 1) ? 1 : scale)
|
||||
} else if scale < 0 && isShowLittleContainerView {
|
||||
// Swipe up
|
||||
handleTransform(animationScale: (scale < -1) ? 0 : (1 + scale))
|
||||
}
|
||||
} else if gesture.state == .ended {
|
||||
if scale > 0 {
|
||||
// Swipe down
|
||||
if (scale > 0.5) {
|
||||
showAnimation()
|
||||
} else {
|
||||
restoreExpansion()
|
||||
}
|
||||
} else {
|
||||
// Swipe up
|
||||
if (scale < -0.5) {
|
||||
restoreExpansion()
|
||||
} else {
|
||||
showAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showAnimation() {
|
||||
isShowLittleContainerView = true
|
||||
delegate?.showAnimation()
|
||||
|
||||
UIView.animate(withDuration: groupFunctionAnimationDuration) {
|
||||
self.handleTransform(animationScale: 1)
|
||||
}
|
||||
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotationAnimation.toValue = NSNumber(value: Double.pi)
|
||||
rotationAnimation.duration = groupFunctionAnimationDuration
|
||||
rotationAnimation.fillMode = .forwards
|
||||
rotationAnimation.isRemovedOnCompletion = false
|
||||
self.matchBtn.layer.add(rotationAnimation, forKey: "rotationAnimation")
|
||||
}
|
||||
|
||||
func restoreExpansion() {
|
||||
isShowLittleContainerView = false
|
||||
delegate?.restoreExpansion()
|
||||
|
||||
UIView.animate(withDuration: groupFunctionAnimationDuration, animations: {
|
||||
self.muteMicBtn.titleLabel.alpha = 1
|
||||
self.changeSpeakerBtn.titleLabel.alpha = 1
|
||||
self.closeCameraBtn.titleLabel.alpha = 1
|
||||
|
||||
self.muteMicBtn.titleLabel.transform = CGAffineTransform.identity
|
||||
self.changeSpeakerBtn.titleLabel.transform = CGAffineTransform.identity
|
||||
self.closeCameraBtn.titleLabel.transform = CGAffineTransform.identity
|
||||
|
||||
self.muteMicBtn.button.transform = CGAffineTransform.identity
|
||||
self.changeSpeakerBtn.button.transform = CGAffineTransform.identity
|
||||
self.closeCameraBtn.button.transform = CGAffineTransform.identity
|
||||
self.hangupBtn.button.transform = CGAffineTransform.identity
|
||||
|
||||
self.muteMicBtn.transform = CGAffineTransform.identity
|
||||
self.changeSpeakerBtn.transform = CGAffineTransform.identity
|
||||
self.closeCameraBtn.transform = CGAffineTransform.identity
|
||||
self.hangupBtn.transform = CGAffineTransform.identity
|
||||
self.matchBtn.transform = CGAffineTransform.identity
|
||||
})
|
||||
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotationAnimation.toValue = NSNumber(value: 0.0)
|
||||
rotationAnimation.duration = groupFunctionAnimationDuration
|
||||
rotationAnimation.fillMode = .forwards
|
||||
rotationAnimation.isRemovedOnCompletion = true
|
||||
self.matchBtn.layer.add(rotationAnimation, forKey: "rotationAnimation")
|
||||
}
|
||||
|
||||
func handleTransform(animationScale: CGFloat) {
|
||||
delegate?.handleTransform(animationScale: animationScale)
|
||||
|
||||
let alpha: CGFloat = 1 - 1 * animationScale
|
||||
let scale: CGFloat = 1 - 1 / 3 * animationScale
|
||||
let muteMicX = ((TUICoreDefineConvert.getIsRTL() ? -28 : 28) * animationScale).scaleWidth()
|
||||
let changeSpeakerX = ((TUICoreDefineConvert.getIsRTL() ? 2 : -2) * animationScale).scaleWidth()
|
||||
let closeCameraX = ((TUICoreDefineConvert.getIsRTL() ? 28 : -28) * animationScale).scaleWidth()
|
||||
let hangupX = ((TUICoreDefineConvert.getIsRTL() ? -138 : 138) * animationScale).scaleWidth()
|
||||
let hangupTranslationY = -(groupFunctionBaseControlBtnHeight + 20.scaleHeight()) * animationScale
|
||||
let titleLabelTranslationY = -12.scaleWidth() * animationScale
|
||||
|
||||
self.muteMicBtn.titleLabel.alpha = alpha
|
||||
self.changeSpeakerBtn.titleLabel.alpha = alpha
|
||||
self.closeCameraBtn.titleLabel.alpha = alpha
|
||||
|
||||
self.muteMicBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
|
||||
self.changeSpeakerBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
|
||||
self.closeCameraBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
|
||||
|
||||
self.muteMicBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
self.changeSpeakerBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
self.closeCameraBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
self.hangupBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
|
||||
self.muteMicBtn.transform = CGAffineTransform(translationX: muteMicX, y: 0)
|
||||
self.changeSpeakerBtn.transform = CGAffineTransform(translationX: changeSpeakerX, y: 0)
|
||||
self.closeCameraBtn.transform = CGAffineTransform(translationX: closeCameraX, y: 0)
|
||||
self.hangupBtn.transform = CGAffineTransform(translationX: hangupX, y: hangupTranslationY)
|
||||
self.matchBtn.transform = CGAffineTransform(translationX: 0, y: hangupTranslationY)
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func updateMuteAudioBtn(mute: Bool) {
|
||||
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
|
||||
muteMicBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func updateChangeSpeakerBtn(isSpeaker: Bool) {
|
||||
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
|
||||
changeSpeakerBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCloseCameraBtn(open: Bool) {
|
||||
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
|
||||
closeCameraBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func setContainerViewCorner() {
|
||||
let maskLayer = CAShapeLayer()
|
||||
let path = UIBezierPath(roundedRect: containerView.bounds,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 20, height: 20))
|
||||
maskLayer.path = path.cgPath
|
||||
containerView.layer.mask = maskLayer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// VideoCallerAndCalleeAcceptedView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SnapKit
|
||||
|
||||
class VideoCallerAndCalleeAcceptedView: UIView {
|
||||
|
||||
let isCameraOpenObserver = Observer()
|
||||
let isMicMuteObserver = Observer()
|
||||
let audioDeviceObserver = Observer()
|
||||
|
||||
lazy var muteMicBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.muteMicEvent(sender: sender)
|
||||
}
|
||||
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var closeCameraBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.closeCameraTouchEvent(sender: sender)
|
||||
}
|
||||
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var changeSpeakerBtn: BaseControlButton = {
|
||||
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.changeSpeakerEvent(sender: sender)
|
||||
}
|
||||
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var hangupBtn: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.addTarget(self,action:#selector(hangupTouchEvent(sender:)), for: .touchUpInside)
|
||||
btn.setBackgroundImage(TUICallKitCommon.getBundleImage(name: "icon_hangup"), for: .normal)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var switchCameraBtn: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "switch_camera") {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(switchCameraTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var virtualBackgroundButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "virtual_background") {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(virtualBackgroundTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(muteMicBtn)
|
||||
addSubview(changeSpeakerBtn)
|
||||
addSubview(closeCameraBtn)
|
||||
addSubview(hangupBtn)
|
||||
addSubview(switchCameraBtn)
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
addSubview(virtualBackgroundButton)
|
||||
}
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
muteMicBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
|
||||
make.centerY.equalTo(changeSpeakerBtn)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
changeSpeakerBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self)
|
||||
make.bottom.equalTo(hangupBtn.snp.top).offset(-20.scaleHeight())
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
closeCameraBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
|
||||
make.centerY.equalTo(self.changeSpeakerBtn)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
hangupBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self)
|
||||
make.bottom.equalTo(snp.bottom)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
switchCameraBtn.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(hangupBtn)
|
||||
make.leading.equalTo(hangupBtn.snp.trailing).offset(40.scaleWidth())
|
||||
make.size.equalTo(CGSize(width: 28.scaleWidth(), height: 28.scaleWidth()))
|
||||
}
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
virtualBackgroundButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(hangupBtn)
|
||||
make.trailing.equalTo(hangupBtn.snp.leading).offset(-40.scaleWidth())
|
||||
make.size.equalTo(CGSize(width: 28.scaleWidth(), height: 28.scaleWidth()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
func muteMicEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.muteMic()
|
||||
updateMuteAudioBtn(mute: TUICallState.instance.isMicMute.value == true)
|
||||
}
|
||||
|
||||
func closeCameraTouchEvent(sender: UIButton) {
|
||||
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
|
||||
if TUICallState.instance.isCameraOpen.value == true {
|
||||
CallEngineManager.instance.closeCamera()
|
||||
virtualBackgroundButton.isHidden = true
|
||||
switchCameraBtn.isHidden = true
|
||||
} else {
|
||||
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
|
||||
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
|
||||
virtualBackgroundButton.isHidden = false
|
||||
switchCameraBtn.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func changeSpeakerEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.changeSpeaker()
|
||||
updateChangeSpeakerBtn(isSpeaker: TUICallState.instance.audioDevice.value == .speakerphone)
|
||||
}
|
||||
|
||||
@objc func hangupTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.hangup()
|
||||
}
|
||||
|
||||
@objc func switchCameraTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.switchCamera()
|
||||
}
|
||||
|
||||
@objc func virtualBackgroundTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.setBlurBackground()
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func updateMuteAudioBtn(mute: Bool) {
|
||||
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
|
||||
muteMicBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func updateChangeSpeakerBtn(isSpeaker: Bool) {
|
||||
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
|
||||
changeSpeakerBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCloseCameraBtn(open: Bool) {
|
||||
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
|
||||
closeCameraBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value)
|
||||
}
|
||||
|
||||
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateMuteAudioBtn(mute: newValue)
|
||||
})
|
||||
|
||||
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//
|
||||
// VideoCallerWaitingView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class VideoCallerWaitingView: UIView {
|
||||
|
||||
let enableBlurBackgroundObserver = Observer()
|
||||
|
||||
lazy var switchCameraBtn: BaseControlButton = {
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.switchCamera") ?? "",
|
||||
imageSize: kBtnLargeSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.switchCameraTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_big_switch_camera") {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var virtualBackgroundButton: BaseControlButton = {
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.blurBackground") ?? "",
|
||||
imageSize: kBtnLargeSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.virtualBackgroundTouchEvent(sender: sender)
|
||||
}
|
||||
let imageName = TUICallState.instance.enableBlurBackground.value ? "icon_big_virtual_background_on" : "icon_big_virtual_background_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var closeCameraBtn: BaseControlButton = {
|
||||
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: titleKey) ?? "",
|
||||
imageSize: kBtnSmallSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.closeCameraTouchEvent(sender: sender)
|
||||
}
|
||||
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var hangupBtn: BaseControlButton = {
|
||||
weak var weakSelf = self
|
||||
let btn = BaseControlButton.create(frame: CGRect.zero,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.hangup") ?? "",
|
||||
imageSize: kBtnLargeSize) { [weak self] sender in
|
||||
guard let self = self else { return }
|
||||
self.hangupTouchEvent(sender: sender)
|
||||
}
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
|
||||
btn.updateImage(image: image)
|
||||
}
|
||||
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
TUICallState.instance.selfUser.removeObserver(enableBlurBackgroundObserver)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(switchCameraBtn)
|
||||
addSubview(closeCameraBtn)
|
||||
addSubview(hangupBtn)
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
addSubview(virtualBackgroundButton)
|
||||
}
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
virtualBackgroundButton.snp.makeConstraints { make in
|
||||
make.top.centerX.equalTo(self)
|
||||
make.bottom.equalTo(hangupBtn.snp.top).offset(-8.scaleWidth())
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
}
|
||||
switchCameraBtn.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(TUICallState.instance.showVirtualBackgroundButton ? virtualBackgroundButton : hangupBtn)
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
hangupBtn.snp.makeConstraints { make in
|
||||
if !TUICallState.instance.showVirtualBackgroundButton {
|
||||
make.top.equalTo(self)
|
||||
}
|
||||
make.centerX.bottom.equalTo(self)
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
closeCameraBtn.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(TUICallState.instance.showVirtualBackgroundButton ? virtualBackgroundButton : hangupBtn)
|
||||
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
|
||||
make.size.equalTo(kControlBtnSize)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
func closeCameraTouchEvent(sender: UIButton) {
|
||||
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
|
||||
if TUICallState.instance.isCameraOpen.value == true {
|
||||
CallEngineManager.instance.closeCamera()
|
||||
virtualBackgroundButton.button.isEnabled = false
|
||||
switchCameraBtn.button.isEnabled = false
|
||||
} else {
|
||||
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
|
||||
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
|
||||
virtualBackgroundButton.button.isEnabled = true
|
||||
switchCameraBtn.button.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateCloseCameraBtn(open: Bool) {
|
||||
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
|
||||
|
||||
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
|
||||
closeCameraBtn.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func hangupTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.hangup()
|
||||
}
|
||||
|
||||
func switchCameraTouchEvent(sender: UIButton ) {
|
||||
CallEngineManager.instance.switchCamera()
|
||||
}
|
||||
|
||||
func virtualBackgroundTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.setBlurBackground()
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateVirtualBackgroundButton()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateVirtualBackgroundButton() {
|
||||
let imageName = TUICallState.instance.enableBlurBackground.value ? "icon_big_virtual_background_on" : "icon_big_virtual_background_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
virtualBackgroundButton.updateImage(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// IncomingFloatView.swift
|
||||
// TUICallKit-Swift
|
||||
//
|
||||
// Created by noah on 2024/3/15.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class IncomingFloatView: UIView {
|
||||
|
||||
let remoteUserListObserver = Observer()
|
||||
let mediaTypeObserver = Observer()
|
||||
|
||||
let userHeadImageView: UIImageView = {
|
||||
let userHeadImageView = UIImageView(frame: CGRect.zero)
|
||||
userHeadImageView.layer.masksToBounds = true
|
||||
userHeadImageView.layer.cornerRadius = 7.0
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
userHeadImageView.image = image
|
||||
}
|
||||
return userHeadImageView
|
||||
}()
|
||||
|
||||
let userNameLabel: UILabel = {
|
||||
let userNameLabel = UILabel(frame: CGRect.zero)
|
||||
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
|
||||
userNameLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
|
||||
userNameLabel.backgroundColor = UIColor.clear
|
||||
userNameLabel.textAlignment = .center
|
||||
return userNameLabel
|
||||
}()
|
||||
|
||||
let callStatusTipView: UILabel = {
|
||||
let callStatusTipLabel = UILabel(frame: CGRect.zero)
|
||||
callStatusTipLabel.textColor = UIColor.t_colorWithHexString(color: "#C5CCDB")
|
||||
callStatusTipLabel.font = UIFont.boldSystemFont(ofSize: 12.0)
|
||||
callStatusTipLabel.textAlignment = .left
|
||||
return callStatusTipLabel
|
||||
}()
|
||||
|
||||
lazy var rejectBtn: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(rejectTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var acceptBtn: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
let imageStr = TUICallState.instance.mediaType.value == .video ? "icon_video_dialing" : "icon_dialing"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageStr) {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(acceptTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
userHeadImageView.layer.masksToBounds = true
|
||||
self.layer.cornerRadius = 10.0
|
||||
backgroundColor = UIColor.t_colorWithHexString(color: "#22262E")
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(showCallView(sender:)))
|
||||
self.addGestureRecognizer(tap)
|
||||
callStatusTipView.text = getCallStatusTipText()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(userHeadImageView)
|
||||
addSubview(userNameLabel)
|
||||
addSubview(callStatusTipView)
|
||||
addSubview(rejectBtn)
|
||||
addSubview(acceptBtn)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
userHeadImageView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scaleWidth())
|
||||
make.centerY.equalTo(self)
|
||||
make.width.height.equalTo(60.scaleWidth())
|
||||
}
|
||||
userNameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(userHeadImageView).offset(10.scaleWidth())
|
||||
make.leading.equalTo(userHeadImageView.snp.trailing).offset(12.scaleWidth())
|
||||
}
|
||||
callStatusTipView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(userHeadImageView.snp.trailing).offset(12.scaleWidth())
|
||||
make.bottom.equalTo(userHeadImageView).offset(-10.scaleWidth())
|
||||
}
|
||||
rejectBtn.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(acceptBtn.snp.leading).offset(-22.scaleWidth())
|
||||
make.centerY.equalTo(self)
|
||||
make.width.height.equalTo(36.scaleWidth())
|
||||
}
|
||||
acceptBtn.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16.scaleWidth())
|
||||
make.centerY.equalTo(self)
|
||||
make.width.height.equalTo(36.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Event Action
|
||||
@objc func showCallView(sender: UIButton) {
|
||||
self.removeFromSuperview()
|
||||
WindowManager.instance.showCallWindow(false)
|
||||
}
|
||||
|
||||
@objc func rejectTouchEvent(sender: UIButton) {
|
||||
self.removeFromSuperview()
|
||||
CallEngineManager.instance.reject()
|
||||
}
|
||||
|
||||
@objc func acceptTouchEvent(sender: UIButton) {
|
||||
self.removeFromSuperview()
|
||||
CallEngineManager.instance.accept()
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
remoteUserListChanged()
|
||||
mediaTypeChanged()
|
||||
}
|
||||
|
||||
func remoteUserListChanged() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.setUserImageAndName()
|
||||
})
|
||||
}
|
||||
|
||||
func mediaTypeChanged() {
|
||||
TUICallState.instance.mediaType.addObserver(mediaTypeObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.callStatusTipView.text = self.getCallStatusTipText()
|
||||
})
|
||||
}
|
||||
|
||||
func setUserImageAndName() {
|
||||
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
|
||||
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
|
||||
|
||||
if let url = URL(string: remoteUser.avatar.value) {
|
||||
userHeadImageView.sd_setImage(with: url)
|
||||
}
|
||||
}
|
||||
|
||||
func getCallStatusTipText() -> String {
|
||||
if TUICallState.instance.scene.value == .group {
|
||||
return TUICallKitLocalize(key: "TUICallKit.Group.inviteToGroupCall") ?? ""
|
||||
}
|
||||
|
||||
var tipLabelText = String()
|
||||
switch TUICallState.instance.mediaType.value {
|
||||
case .audio:
|
||||
tipLabelText = TUICallKitLocalize(key: "TUICallKit.inviteToAudioCall") ?? ""
|
||||
case .video:
|
||||
tipLabelText = TUICallKitLocalize(key: "TUICallKit.inviteToVideoCall") ?? ""
|
||||
case .unknown:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return tipLabelText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// SelectGroupMemberCell.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/5/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class SelectGroupMemberCell: UITableViewCell {
|
||||
|
||||
let userImageView = {
|
||||
let view = UIImageView(frame: CGRect.zero)
|
||||
view.layer.cornerRadius = 2
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let nameLabel = {
|
||||
let label = UILabel(frame: CGRect.zero)
|
||||
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_name_color",
|
||||
defaultHex: "#242424")
|
||||
return label
|
||||
}()
|
||||
|
||||
let selectImageView = {
|
||||
let view = UIImageView(frame: CGRect.zero)
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_cell_bg_color",
|
||||
defaultHex: "#FFFFFF")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
if isViewReady {
|
||||
return
|
||||
}
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(userImageView)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(selectImageView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
selectImageView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.width.height.equalTo(20)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
userImageView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(selectImageView.snp.trailing).offset(12)
|
||||
make.width.height.equalTo(30)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(self.userImageView.snp.trailing).offset(12)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.height.equalTo(40)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func configCell(user: User, isSelect: Bool) {
|
||||
backgroundColor = UIColor.clear
|
||||
userImageView.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
|
||||
|
||||
if isSelect {
|
||||
selectImageView.image = TUICallKitCommon.getBundleImage(name: "icon_check_box_group_selected")
|
||||
} else {
|
||||
selectImageView.image = TUICallKitCommon.getBundleImage(name: "icon_check_box_group_unselected")
|
||||
}
|
||||
|
||||
nameLabel.text = User.getUserDisplayName(user: user)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//
|
||||
// SelectGroupMemberViewController.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/5/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICore
|
||||
|
||||
class SelectGroupMemberViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
let viewModel = SelectGroupMemberViewModel()
|
||||
let groupMemberObserver = Observer()
|
||||
|
||||
lazy var navigationView: UIView = {
|
||||
let navigationView = UIView()
|
||||
navigationView.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "head_bg_gradient_start_color",
|
||||
defaultHex: "#EBF0F6")
|
||||
return navigationView
|
||||
}()
|
||||
|
||||
lazy var leftBtn: UIButton = {
|
||||
let leftBtn = UIButton(type: .custom)
|
||||
leftBtn.addTarget(self, action: #selector(goBack), for: .touchUpInside)
|
||||
let defaultImage = TUICallKitCommon.getBundleImage(name: "icon_nav_back") ?? UIImage()
|
||||
let leftBtnImage = TUICoreDefineConvert.getTUIDynamicImage(imageKey: "icon_nav_back_image",
|
||||
module: TUIThemeModule.calling,
|
||||
defaultImage: defaultImage)
|
||||
leftBtn.setImage(leftBtnImage, for: .normal)
|
||||
return leftBtn
|
||||
}()
|
||||
|
||||
lazy var centerLabel: UILabel = {
|
||||
let centerLabel = UILabel(frame: CGRect.zero)
|
||||
centerLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
|
||||
centerLabel.textAlignment = .center
|
||||
centerLabel.text = TUICallKitLocalize(key: "TUICallKit.Recents.addUser")
|
||||
centerLabel.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_title_text_color",
|
||||
defaultHex: "#000000")
|
||||
return centerLabel
|
||||
}()
|
||||
|
||||
lazy var rightBtn: UIButton = {
|
||||
let rightBtn = UIButton(type: .system)
|
||||
rightBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside)
|
||||
rightBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17.0)
|
||||
rightBtn.setTitle(TUICallKitLocalize(key: "TUICallKit.determine"), for: .normal)
|
||||
rightBtn.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_item_title_text_color",
|
||||
defaultHex: "#000000"), for: .normal)
|
||||
return rightBtn
|
||||
}()
|
||||
|
||||
let selectTableView: UITableView = {
|
||||
let selectTableView = UITableView(frame: CGRect.zero)
|
||||
selectTableView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_bg_color",
|
||||
defaultHex: "#F2F2F2")
|
||||
return selectTableView
|
||||
}()
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
override func viewDidLoad() {
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
registerObserve()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
view.addSubview(navigationView)
|
||||
view.addSubview(leftBtn)
|
||||
view.addSubview(leftBtn)
|
||||
view.addSubview(centerLabel)
|
||||
view.addSubview(rightBtn)
|
||||
view.addSubview(selectTableView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
navigationView.snp.makeConstraints({ make in
|
||||
make.leading.trailing.top.equalToSuperview()
|
||||
make.height.equalTo(StatusBar_Height + 44)
|
||||
})
|
||||
leftBtn.snp.makeConstraints({ make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.centerY.equalTo(centerLabel)
|
||||
make.width.height.equalTo(30)
|
||||
})
|
||||
centerLabel.snp.makeConstraints({ make in
|
||||
make.top.equalToSuperview().offset(StatusBar_Height)
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.equalTo(Screen_Width * 2 / 3)
|
||||
make.height.equalTo(44)
|
||||
})
|
||||
rightBtn.snp.makeConstraints({ make in
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.centerY.equalTo(centerLabel)
|
||||
})
|
||||
selectTableView.snp.makeConstraints({ make in
|
||||
make.top.equalTo(centerLabel.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
selectTableView.dataSource = self
|
||||
selectTableView.delegate = self
|
||||
selectTableView.register(SelectGroupMemberCell.self, forCellReuseIdentifier: NSStringFromClass(SelectGroupMemberCell.self))
|
||||
}
|
||||
|
||||
func registerObserve() {
|
||||
viewModel.groupMemberList.addObserver(groupMemberObserver) {[weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
self.selectTableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Action
|
||||
@objc func addUser() {
|
||||
var userIds: [String] = []
|
||||
for state in viewModel.groupMemberStateOrigin {
|
||||
if state.value {
|
||||
continue
|
||||
}
|
||||
if let isSelect = viewModel.groupMemberState[state.key] {
|
||||
if isSelect {
|
||||
userIds.append(state.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if userIds.count + TUICallState.instance.remoteUserList.value.count >= MAX_USER {
|
||||
TUITool.makeToast(TUICallKitLocalize(key: "TUICallKit.User.Exceed.Limit"))
|
||||
return
|
||||
}
|
||||
|
||||
CallEngineManager.instance.inviteUser(userIds: userIds)
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc func goBack() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SelectGroupMemberViewController {
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.groupMemberList.value.count + 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(SelectGroupMemberCell.self),
|
||||
for: indexPath) as? SelectGroupMemberCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
if indexPath.row == 0 {
|
||||
cell.configCell(user: viewModel.selfUser.value, isSelect: true)
|
||||
} else {
|
||||
let user = viewModel.groupMemberList.value[indexPath.row - 1]
|
||||
let isSelect = viewModel.groupMemberState[user.id.value]
|
||||
cell.configCell(user: user, isSelect: isSelect ?? false)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 60
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let user = viewModel.groupMemberList.value[indexPath.row - 1]
|
||||
if viewModel.groupMemberStateOrigin[user.id.value] ?? false {
|
||||
return
|
||||
}
|
||||
|
||||
if viewModel.groupMemberState[user.id.value] ?? false {
|
||||
viewModel.groupMemberState[user.id.value] = false
|
||||
} else {
|
||||
viewModel.groupMemberState[user.id.value] = true
|
||||
}
|
||||
|
||||
selectTableView.reloadData()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// InviteUserButton.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICore
|
||||
|
||||
class InviteUserButton: UIView {
|
||||
|
||||
let mediaTypeObserver = Observer()
|
||||
|
||||
let inviteUserButton: InviteUserCustomButton = {
|
||||
let inviteUserButton = InviteUserCustomButton(type: .system)
|
||||
return inviteUserButton
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
updateImage()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(inviteUserButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
inviteUserButton.snp.makeConstraints { make in
|
||||
make.center.equalTo(self)
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
inviteUserButton.addTarget(self, action: #selector(clickButton(sender: )), for: .touchUpInside)
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
@objc func clickButton(sender: UIButton) {
|
||||
let selectGroupMemberVC = SelectGroupMemberViewController()
|
||||
selectGroupMemberVC.modalPresentationStyle = .fullScreen
|
||||
UIWindow.getKeyWindow()?.rootViewController?.present(selectGroupMemberVC, animated: false)
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.mediaType.addObserver(mediaTypeObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateImage()
|
||||
}
|
||||
}
|
||||
|
||||
func updateImage() {
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_add_user") {
|
||||
inviteUserButton.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func tui_valueCallback(param: [AnyHashable: Any]) {
|
||||
guard let selectUserList = param[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel] else { return }
|
||||
if selectUserList.count > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var userIds: [String] = []
|
||||
for user in selectUserList {
|
||||
userIds.append(user.userId)
|
||||
}
|
||||
|
||||
CallEngineManager.instance.inviteUser(userIds: userIds)
|
||||
}
|
||||
}
|
||||
|
||||
class InviteUserCustomButton: UIButton {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let expandedBounds = bounds.insetBy(dx: -6, dy: -6)
|
||||
return expandedBounds.contains(point) ? self : nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// JoinInGroupCallUserCell.swift
|
||||
// TUICallKit-Swift
|
||||
//
|
||||
// Created by noah on 2024/2/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class JoinInGroupCallUserCell: UICollectionViewCell {
|
||||
|
||||
private var user: User = User()
|
||||
|
||||
private let userIcon = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.cornerRadius = 4.0
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(userIcon)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
userIcon.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self.contentView)
|
||||
}
|
||||
}
|
||||
|
||||
func setModel(user: User) {
|
||||
self.user = user
|
||||
setUserIcon()
|
||||
}
|
||||
|
||||
private func setUserIcon() {
|
||||
let userImage: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
|
||||
|
||||
if user.avatar.value == "" {
|
||||
guard let image = userImage else { return }
|
||||
userIcon.image = image
|
||||
} else {
|
||||
userIcon.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: userImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
//
|
||||
// JoinInGroupCallView.swift
|
||||
// TUICallKit-Swift
|
||||
//
|
||||
// Created by noah on 2024/1/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import TUICore
|
||||
import TUICallEngine
|
||||
|
||||
let kJoinGroupCallViewDefaultHeight: CGFloat = 52.0
|
||||
let kJoinGroupCallViewExpandHeight: CGFloat = 225.0
|
||||
let kJoinGroupCallItemWidth: CGFloat = 50.0
|
||||
let kJoinGroupCallSpacing: CGFloat = 12.0
|
||||
|
||||
protocol JoinInGroupCallViewDelegate: AnyObject {
|
||||
func updatePageContent(isExpand: Bool)
|
||||
func joinInGroupCall()
|
||||
}
|
||||
|
||||
class JoinInGroupCallView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
weak var delegate: JoinInGroupCallViewDelegate?
|
||||
var listDate = Array<User>()
|
||||
|
||||
lazy var bottomContainerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_bottom_container_bg_color",
|
||||
defaultHex: "#FFFFFF")
|
||||
view.layer.cornerRadius = 6.0
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var titleIcon: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = TUICallKitCommon.getBundleImage(name: "icon_join_group")
|
||||
return imageView
|
||||
}()
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont.systemFont(ofSize: 12.0)
|
||||
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_title_color", defaultHex: "#999999")
|
||||
label.textAlignment = .left
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var expandButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(TUICallKitCommon.getBundleImage(name: "icon_join_group_expand"), for: .normal)
|
||||
button.setImage(TUICallKitCommon.getBundleImage(name: "icon_join_group_zoom"), for: .selected)
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var expandView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_expand_bg_color", defaultHex: "#EEF0F2")
|
||||
view.isHidden = true
|
||||
view.layer.cornerRadius = 6.0
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var collectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.backgroundColor = .clear
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
lazy var lineView: UIView = {
|
||||
let view = UIView()
|
||||
view.alpha = 0.1
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_line_color", defaultHex: "#707070")
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var joinButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Semibold", size: 14.0)
|
||||
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_button_color", defaultHex: "#333333"),
|
||||
for: .normal)
|
||||
button.setTitle(TUICallKitLocalize(key: "TUICallKit.JoinGroupView.join"), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_bg_color", defaultHex: "#ECF0F5")
|
||||
self.frame = CGRect(x: 0, y: 0, width: Screen_Width, height: kJoinGroupCallViewDefaultHeight)
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(expandButtonClick(sender:)))
|
||||
self.addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(bottomContainerView)
|
||||
bottomContainerView.addSubview(titleIcon)
|
||||
bottomContainerView.addSubview(titleLabel)
|
||||
bottomContainerView.addSubview(expandButton)
|
||||
bottomContainerView.addSubview(expandView)
|
||||
expandView.addSubview(collectionView)
|
||||
expandView.addSubview(lineView)
|
||||
expandView.addSubview(joinButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
bottomContainerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(self).offset(8)
|
||||
make.leading.equalTo(self).offset(16)
|
||||
make.center.equalTo(self)
|
||||
}
|
||||
titleIcon.snp.makeConstraints { make in
|
||||
make.top.equalTo(bottomContainerView).offset(8)
|
||||
make.leading.equalTo(bottomContainerView).offset(16)
|
||||
make.width.height.equalTo(20)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(titleIcon)
|
||||
make.leading.equalTo(titleIcon.snp.trailing).offset(10)
|
||||
make.width.equalTo(200)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
expandButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(titleIcon)
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(10)
|
||||
make.trailing.equalTo(bottomContainerView).offset(-16)
|
||||
make.width.height.equalTo(30)
|
||||
}
|
||||
expandView.snp.makeConstraints { make in
|
||||
make.top.equalTo(bottomContainerView).offset(36)
|
||||
make.leading.equalTo(bottomContainerView).offset(16)
|
||||
make.trailing.equalTo(bottomContainerView).offset(-16)
|
||||
make.height.equalTo(157)
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(expandView).offset(37)
|
||||
make.leading.equalTo(expandView).offset(10)
|
||||
make.trailing.equalTo(expandView).offset(-10)
|
||||
make.height.equalTo(kJoinGroupCallItemWidth)
|
||||
}
|
||||
lineView.snp.makeConstraints { make in
|
||||
make.top.equalTo(expandView).offset(117)
|
||||
make.leading.trailing.equalTo(self.expandView)
|
||||
make.height.equalTo(1)
|
||||
}
|
||||
joinButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(lineView.snp.bottom).offset(5)
|
||||
make.centerX.equalTo(self.expandView)
|
||||
make.width.equalTo(100)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
expandButton.addTarget(self, action: #selector(expandButtonClick(sender: )), for: .touchUpInside)
|
||||
joinButton.addTarget(self, action: #selector(joinButtonClick(sender: )), for: .touchUpInside)
|
||||
collectionView.register(JoinInGroupCallUserCell.self,
|
||||
forCellWithReuseIdentifier: String(describing: JoinInGroupCallUserCell.self))
|
||||
}
|
||||
|
||||
@objc func expandButtonClick(sender: UIButton) {
|
||||
expandView.isHidden = expandButton.isSelected
|
||||
expandButton.isSelected = !expandButton.isSelected
|
||||
delegate?.updatePageContent(isExpand: expandButton.isSelected)
|
||||
}
|
||||
|
||||
@objc func joinButtonClick(sender: UIButton) {
|
||||
expandButton.isSelected = false;
|
||||
delegate?.joinInGroupCall()
|
||||
}
|
||||
|
||||
func updateView(with userList: [User], callMediaType: TUICallMediaType) {
|
||||
listDate.removeAll()
|
||||
userList.forEach {
|
||||
obj in if obj.id.value != TUILogin.getUserID() { listDate.append(obj) } }
|
||||
titleLabel.isHidden = listDate.isEmpty
|
||||
titleLabel.text = String(format: TUICallKitLocalize(key: "TUICallKit.JoinGroupView.title") ?? "",
|
||||
listDate.count,
|
||||
getCallMediaTypeStr(with: callMediaType))
|
||||
titleLabel.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
collectionView.reloadData()
|
||||
}
|
||||
|
||||
func getCallMediaTypeStr(with callMediaType: TUICallMediaType) -> String {
|
||||
var callMediaTypeStr: String = ""
|
||||
if callMediaType == .audio {
|
||||
callMediaTypeStr = TUICallKitLocalize(key: "TUICallKit.JoinGroupView.audioCall") ?? ""
|
||||
} else if callMediaType == .video {
|
||||
callMediaTypeStr = TUICallKitLocalize(key: "TUICallKit.JoinGroupView.videoCall") ?? ""
|
||||
}
|
||||
return callMediaTypeStr
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
|
||||
extension JoinInGroupCallView {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return listDate.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: JoinInGroupCallUserCell.self),
|
||||
for: indexPath) as! JoinInGroupCallUserCell
|
||||
let model = listDate[indexPath.item]
|
||||
cell.setModel(user: model)
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: kJoinGroupCallItemWidth, height: kJoinGroupCallItemWidth)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return kJoinGroupCallSpacing
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
let totalCellWidth: CGFloat = kJoinGroupCallItemWidth * CGFloat(listDate.count)
|
||||
let totalSpacingWidth: CGFloat = kJoinGroupCallSpacing * (CGFloat(listDate.count - 1) < 0 ? 0 : CGFloat(listDate.count - 1))
|
||||
let leftInset = (CGFloat(collectionView.bounds.size.width) - (totalCellWidth + totalSpacingWidth)) / 2
|
||||
if leftInset > 0 {
|
||||
let rightInset = leftInset
|
||||
let sectionInset = UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
|
||||
return sectionInset
|
||||
}
|
||||
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// TUICallRecordCallsCell.swift
|
||||
//
|
||||
//
|
||||
// Created by vincepzhang on 2023/8/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TUICallEngine
|
||||
|
||||
class TUICallRecordCallsCell: UITableViewCell {
|
||||
|
||||
private let faceURLObserver = Observer()
|
||||
private let titleObserver = Observer()
|
||||
|
||||
typealias TUICallRecordCallsCellMoreBtnClickedHandler = () -> Void
|
||||
var moreBtnClickedHandler: TUICallRecordCallsCellMoreBtnClickedHandler = {}
|
||||
|
||||
private var isViewReady = false
|
||||
private var viewModel: TUICallRecordCallsCellViewModel = TUICallRecordCallsCellViewModel(TUICallRecords())
|
||||
|
||||
private lazy var avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.layer.cornerRadius = 20
|
||||
imageView.clipsToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 1
|
||||
label.font = UIFont(name: "PingFangHK-Semibold", size: 14)
|
||||
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var mediaTypeImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var resultLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_subtitle_color",
|
||||
defaultHex: "#888888")
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var timeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_time_color",
|
||||
defaultHex: "#BBBBBB")
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .left : .right
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var moreButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(TUICallKitCommon.getBundleImage(name: "ic_recents_more"), for: .normal)
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
contentView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_bg_color",
|
||||
defaultHex: "#FFFFFF")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
unregisterObserve()
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(mediaTypeImageView)
|
||||
contentView.addSubview(resultLabel)
|
||||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(moreButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(contentView)
|
||||
make.leading.equalTo(contentView).offset(16)
|
||||
make.width.height.equalTo(40)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(contentView).offset(14)
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(8)
|
||||
make.trailing.lessThanOrEqualTo(timeLabel.snp.leading).offset(-20)
|
||||
}
|
||||
mediaTypeImageView.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(contentView).offset(-14)
|
||||
make.leading.equalTo(avatarImageView.snp.trailing).offset(8)
|
||||
make.width.equalTo(19)
|
||||
make.height.equalTo(12)
|
||||
}
|
||||
resultLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(mediaTypeImageView)
|
||||
make.leading.equalTo(mediaTypeImageView.snp.trailing).offset(4)
|
||||
make.width.equalTo(100)
|
||||
}
|
||||
timeLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(contentView)
|
||||
make.trailing.equalTo(moreButton.snp.leading).offset(-4)
|
||||
make.width.equalTo(100)
|
||||
}
|
||||
moreButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(contentView)
|
||||
make.trailing.equalTo(-8)
|
||||
make.width.height.equalTo(24)
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
moreButton.addTarget(self, action: #selector(moreButtonClick(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func configViewModel(_ viewModel: TUICallRecordCallsCellViewModel) {
|
||||
self.viewModel = viewModel
|
||||
registerObserve()
|
||||
|
||||
titleLabel.text = viewModel.titleLabelStr.value
|
||||
avatarImageView.sd_setImage(with: URL(string: viewModel.faceURL.value), placeholderImage: viewModel.avatarImage)
|
||||
resultLabel.text = viewModel.resultLabelStr
|
||||
timeLabel.text = viewModel.timeLabelStr
|
||||
mediaTypeImageView.image = TUICallKitCommon.getBundleImage(name: viewModel.mediaTypeImageStr)
|
||||
|
||||
if viewModel.callRecord.result == .missed {
|
||||
titleLabel.textColor = UIColor.red
|
||||
} else {
|
||||
titleLabel.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_title_color",
|
||||
defaultHex: "#000000")
|
||||
}
|
||||
}
|
||||
|
||||
func registerObserve() {
|
||||
viewModel.faceURL.addObserver(faceURLObserver) { [weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarImageView.sd_setImage(with: URL(string: self.viewModel.faceURL.value),
|
||||
placeholderImage: self.viewModel.avatarImage)
|
||||
}
|
||||
|
||||
viewModel.titleLabelStr.addObserver(titleObserver) { [weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
self.titleLabel.text = self.viewModel.titleLabelStr.value
|
||||
}
|
||||
}
|
||||
|
||||
func unregisterObserve() {
|
||||
viewModel.faceURL.removeObserver(faceURLObserver)
|
||||
viewModel.titleLabelStr.removeObserver(titleObserver)
|
||||
}
|
||||
|
||||
@objc private func moreButtonClick(_ button: UIButton) {
|
||||
moreBtnClickedHandler()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// TUICallRecordCallsViewController.swift
|
||||
//
|
||||
//
|
||||
// Created by vincepzhang on 2023/8/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import TUICore
|
||||
|
||||
public class TUICallRecordCallsViewController: UIViewController {
|
||||
|
||||
private let viewModel = TUICallRecordCallsViewModel()
|
||||
private let dataSourceObserver = Observer()
|
||||
|
||||
private lazy var containerView: UIView = {
|
||||
let view = UIView()
|
||||
if viewModel.recordCallsUIStyle == .classic {
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "head_bg_gradient_start_color",
|
||||
defaultHex: "#EBF0F6")
|
||||
} else {
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "callkit_recents_bg_color",
|
||||
defaultHex: "#FFFFFF")
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var editButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(TUICallKitCommon.getBundleImage(name: "ic_calls_edit"), for: .normal)
|
||||
button.titleLabel?.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var clearButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(TUICallKitLocalize(key: "TUICallKit.Recents.clear"), for: .normal)
|
||||
button.titleLabel?.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
button.contentHorizontalAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_title_text_color",
|
||||
defaultHex: "#000000"), for: .normal)
|
||||
button.isHidden = true
|
||||
button.titleLabel?.sizeToFit()
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var doneButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(TUICallKitLocalize(key: "TUICallKit.Recents.done"), for: .normal)
|
||||
button.contentHorizontalAlignment = .right
|
||||
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_item_title_text_color",
|
||||
defaultHex: "#000000"), for: .normal)
|
||||
button.titleLabel?.sizeToFit()
|
||||
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
button.isHidden = true
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var segmentedControl: UISegmentedControl = {
|
||||
let items = [TUICallKitLocalize(key: "TUICallKit.Recents.all"), TUICallKitLocalize(key: "TUICallKit.Recents.missed")]
|
||||
let control = UISegmentedControl(items: items as [Any])
|
||||
control.selectedSegmentIndex = 0
|
||||
return control
|
||||
}()
|
||||
|
||||
private lazy var notRecordCallsLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.textColor = UIColor.gray
|
||||
label.font = UIFont.systemFont(ofSize: 20)
|
||||
label.textAlignment = .center
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var tableHeaderView: UIView = {
|
||||
let tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 80))
|
||||
let label = UILabel()
|
||||
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_tableHeader_title_text_color",
|
||||
defaultHex: "#000000")
|
||||
label.text = TUICallKitLocalize(key: "TUICallKit.Recents.calls")
|
||||
label.font = UIFont(name: "PingFangHK-Semibold", size: 34)
|
||||
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
tableHeaderView.addSubview(label)
|
||||
label.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.equalTo(tableHeaderView).offset(20)
|
||||
}
|
||||
return tableHeaderView
|
||||
}()
|
||||
|
||||
private lazy var recordCallsList: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.tableHeaderView = tableHeaderView
|
||||
tableView.backgroundColor = view.backgroundColor
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
||||
init(recordCallsUIStyle: TUICallKitRecordCallsUIStyle) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewModel.recordCallsUIStyle = recordCallsUIStyle
|
||||
|
||||
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_bg_color",
|
||||
defaultHex: "#FFFFFF")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Observe
|
||||
func registerObserve() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(windowDidChange),
|
||||
name: UIWindow.didBecomeKeyNotification, object: nil)
|
||||
|
||||
viewModel.dataSource.addObserver(dataSourceObserver) {[weak self] _, _ in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.recordCallsList.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unregisterObserve() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIWindow.didBecomeKeyNotification, object: nil)
|
||||
viewModel.dataSource.removeObserver(dataSourceObserver)
|
||||
}
|
||||
|
||||
// Register the UI data refresh callback when the page appears, close it when the page disappears, to prevent the UI refresh from running in a non-main thread.
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
registerObserve()
|
||||
viewModel.queryRecentCalls()
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
unregisterObserve()
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
viewModel.queryRecentCalls()
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
view.addSubview(containerView)
|
||||
containerView.addSubview(segmentedControl)
|
||||
containerView.addSubview(editButton)
|
||||
containerView.addSubview(clearButton)
|
||||
containerView.addSubview(doneButton)
|
||||
view.addSubview(notRecordCallsLabel)
|
||||
view.addSubview(recordCallsList)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
containerView.snp.makeConstraints { make in
|
||||
make.top.centerX.width.equalTo(view)
|
||||
make.height.equalTo(StatusBar_Height + 44)
|
||||
}
|
||||
segmentedControl.snp.makeConstraints { make in
|
||||
make.top.equalTo(containerView).offset(StatusBar_Height + 6.0)
|
||||
make.centerX.equalTo(containerView)
|
||||
make.width.equalTo(180)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
editButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(segmentedControl)
|
||||
make.trailing.equalTo(containerView).offset(-20)
|
||||
make.width.height.equalTo(32)
|
||||
}
|
||||
clearButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(segmentedControl)
|
||||
make.leading.equalTo(containerView).offset(20)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
doneButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(segmentedControl)
|
||||
make.trailing.equalTo(containerView).offset(-20)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
recordCallsList.snp.makeConstraints { make in
|
||||
make.top.equalTo(view).offset(StatusBar_Height + 44)
|
||||
make.centerX.width.bottom.equalTo(view)
|
||||
}
|
||||
notRecordCallsLabel.snp.makeConstraints { make in
|
||||
make.center.width.equalTo(view)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
recordCallsList.delegate = self
|
||||
recordCallsList.dataSource = self
|
||||
recordCallsList.register(TUICallRecordCallsCell.self, forCellReuseIdentifier: NSStringFromClass(TUICallRecordCallsCell.self))
|
||||
|
||||
clearButton.addTarget(self, action: #selector(clearButtonClick(_:)), for: .touchUpInside)
|
||||
editButton.addTarget(self, action: #selector(editButtonClick(_:)), for: .touchUpInside)
|
||||
doneButton.addTarget(self, action: #selector(doneButtonClick(_:)), for: .touchUpInside)
|
||||
segmentedControl.addTarget(self, action: #selector(segmentSelectItem(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
@objc func windowDidChange() {
|
||||
viewModel.queryRecentCalls()
|
||||
}
|
||||
|
||||
@objc func clearButtonClick(_ button: UIButton) {
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
let clearAction = UIAlertAction(title: TUICallKitLocalize(key: "TUICallKit.Recents.clear.all"),
|
||||
style: .destructive) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.recordCallsList.setEditing(false, animated: true)
|
||||
self.setShowEditButton(true)
|
||||
self.viewModel.deleteAllRecordCalls()
|
||||
}
|
||||
let cancelAction = UIAlertAction(title: TUICallKitLocalize(key: "TUICallKit.Recents.clear.cancel"), style: .cancel)
|
||||
alertController.addAction(clearAction)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func editButtonClick(_ button: UIButton) {
|
||||
recordCallsList.setEditing(true, animated: true)
|
||||
setShowEditButton(false)
|
||||
}
|
||||
|
||||
@objc func doneButtonClick(_ button: UIButton) {
|
||||
recordCallsList.setEditing(false, animated: true)
|
||||
setShowEditButton(true)
|
||||
}
|
||||
|
||||
@objc func segmentSelectItem(_ sender: UISegmentedControl) {
|
||||
var type: TUICallRecordCallsType = .all
|
||||
|
||||
if sender.selectedSegmentIndex == 1 {
|
||||
type = .missed
|
||||
}
|
||||
|
||||
viewModel.switchRecordCallsType(type)
|
||||
}
|
||||
|
||||
func setShowEditButton(_ isShow: Bool) {
|
||||
editButton.isHidden = !isShow
|
||||
clearButton.isHidden = isShow
|
||||
doneButton.isHidden = isShow
|
||||
}
|
||||
}
|
||||
|
||||
extension TUICallRecordCallsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
return .delete
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteRowAction = UIContextualAction(style: .normal,
|
||||
title: TUICallKitLocalize(key: "TUICallKit.Recents.delete"))
|
||||
{ [weak self] action, sourceView, completionHandler in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.deleteRecordCall(indexPath)
|
||||
}
|
||||
deleteRowAction.backgroundColor = .red
|
||||
let config = UISwipeActionsConfiguration(actions: [deleteRowAction])
|
||||
config.performsFirstActionWithFullSwipe = false
|
||||
self.setShowEditButton(false)
|
||||
return config
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 64.0
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.dataSource.value.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(TUICallRecordCallsCell.self),
|
||||
for: indexPath) as? TUICallRecordCallsCell else { return UITableViewCell() }
|
||||
cell.moreBtnClickedHandler = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let navigationController = self.navigationController else { return }
|
||||
self.viewModel.jumpUserInfoController(indexPath: indexPath, navigationController: navigationController)
|
||||
}
|
||||
cell.configViewModel(viewModel.dataSource.value[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
viewModel.repeatCall(indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// TimerView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICallEngine
|
||||
|
||||
class TimerView: UIView {
|
||||
|
||||
let timeCountObserver = Observer()
|
||||
|
||||
lazy var timerLabel: UILabel = {
|
||||
let timerLabel = UILabel(frame: CGRect.zero)
|
||||
timerLabel.font = UIFont.boldSystemFont(ofSize: 15.0)
|
||||
timerLabel.backgroundColor = UIColor.clear
|
||||
timerLabel.textAlignment = .center
|
||||
timerLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
|
||||
timerLabel.text = GCDTimer.secondToHMSString(second: TUICallState.instance.timeCount.value)
|
||||
return timerLabel
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.timeCount.removeObserver(timeCountObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(timerLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
timerLabel.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
|
||||
func registerObserveState() {
|
||||
callTimeChange()
|
||||
}
|
||||
|
||||
func callTimeChange() {
|
||||
TUICallState.instance.timeCount.addObserver(timeCountObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
DispatchCallKitMainAsyncSafe {
|
||||
self.timerLabel.text = GCDTimer.secondToHMSString(second: newValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// GroupBackgroundView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/8.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class GroupBackgroundView: UIView {
|
||||
|
||||
let selfUserObserver = Observer()
|
||||
|
||||
let userHeadImageView: UIImageView = {
|
||||
let userHeadImageView = UIImageView(frame: CGRect.zero)
|
||||
userHeadImageView.contentMode = .scaleAspectFill
|
||||
return userHeadImageView
|
||||
}()
|
||||
|
||||
let blurEffectView: UIVisualEffectView = {
|
||||
let blurEffect = UIBlurEffect(style: .light)
|
||||
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
blurEffectView.alpha = 0.65
|
||||
return blurEffectView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUserImage()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.selfUser.removeObserver(selfUserObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(userHeadImageView)
|
||||
addSubview(blurEffectView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.userHeadImageView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
self.blurEffectView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
selfUserChanged()
|
||||
}
|
||||
|
||||
func selfUserChanged() {
|
||||
TUICallState.instance.selfUser.addObserver(selfUserObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.setUserImage()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func setUserImage() {
|
||||
let selfUser = TUICallState.instance.selfUser.value
|
||||
userHeadImageView.sd_setImage(with: URL(string: selfUser.avatar.value),
|
||||
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// GroupCallerUserInfoView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class GroupCallerUserInfoView: UIView {
|
||||
|
||||
let remoteUserListObserver = Observer()
|
||||
|
||||
let userHeadImageView: UIImageView = {
|
||||
let userHeadImageView = UIImageView(frame: CGRect.zero)
|
||||
userHeadImageView.layer.masksToBounds = true
|
||||
userHeadImageView.layer.cornerRadius = 6.0
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
userHeadImageView.image = image
|
||||
}
|
||||
return userHeadImageView
|
||||
}()
|
||||
|
||||
let userNameLabel: UILabel = {
|
||||
let userNameLabel = UILabel(frame: CGRect.zero)
|
||||
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
|
||||
userNameLabel.font = UIFont.boldSystemFont(ofSize: 24.0)
|
||||
userNameLabel.backgroundColor = UIColor.clear
|
||||
userNameLabel.textAlignment = .center
|
||||
return userNameLabel
|
||||
}()
|
||||
|
||||
let waitingInviteLabel: UILabel = {
|
||||
let waitingInviteLabel = UILabel(frame: CGRect.zero)
|
||||
waitingInviteLabel.textColor = UIColor.t_colorWithHexString(color: "#8F9AB2")
|
||||
waitingInviteLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
|
||||
waitingInviteLabel.backgroundColor = UIColor.clear
|
||||
waitingInviteLabel.text = TUICallKitLocalize(key: "TUICallKit.Group.inviteToGroupCall") ?? ""
|
||||
waitingInviteLabel.textAlignment = .center
|
||||
return waitingInviteLabel
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUserImageAndName()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(userHeadImageView)
|
||||
addSubview(userNameLabel)
|
||||
addSubview(waitingInviteLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
userHeadImageView.snp.makeConstraints { make in
|
||||
make.top.centerX.equalTo(self)
|
||||
make.size.equalTo(CGSize(width: 100.scaleWidth(), height: 100.scaleWidth()))
|
||||
}
|
||||
userNameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(userHeadImageView.snp.bottom).offset(10.scaleHeight())
|
||||
make.centerX.equalTo(self)
|
||||
make.width.equalTo(self)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
waitingInviteLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(userNameLabel.snp.bottom).offset(20.scaleHeight())
|
||||
make.centerX.equalTo(self)
|
||||
make.width.equalTo(self)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.setUserImageAndName()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func setUserImageAndName() {
|
||||
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
|
||||
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
|
||||
|
||||
if let url = URL(string: remoteUser.avatar.value) {
|
||||
userHeadImageView.sd_setImage(with: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// InviteeAvatarCell.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class InviteeAvatarCell: UICollectionViewCell {
|
||||
|
||||
private var user: User = User()
|
||||
private let userIcon = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.cornerRadius = 2.0
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(userIcon)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
userIcon.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self.contentView)
|
||||
}
|
||||
}
|
||||
|
||||
func initCell(user: User) {
|
||||
self.user = user
|
||||
setUserIcon()
|
||||
}
|
||||
|
||||
private func setUserIcon() {
|
||||
let userImage: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
|
||||
|
||||
if user.avatar.value == "" {
|
||||
guard let image = userImage else { return }
|
||||
userIcon.image = image
|
||||
} else {
|
||||
userIcon.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: userImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// InviteeAvatarListView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let kItemWidth = 32.scaleWidth()
|
||||
private let kSpacing = 5.scaleWidth()
|
||||
|
||||
class InviteeAvatarListView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
let remoteUserListObserver = Observer()
|
||||
let dataSource: Observable<[User]> = Observable(Array())
|
||||
|
||||
lazy var describeLabel: UILabel = {
|
||||
let describeLabel = UILabel()
|
||||
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
|
||||
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
|
||||
describeLabel.textAlignment = .center
|
||||
describeLabel.isUserInteractionEnabled = false
|
||||
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.calleeTip") ?? ""
|
||||
return describeLabel
|
||||
}()
|
||||
|
||||
lazy var calleeCollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
let calleeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
|
||||
calleeCollectionView.delegate = self
|
||||
calleeCollectionView.dataSource = self
|
||||
calleeCollectionView.showsVerticalScrollIndicator = false
|
||||
calleeCollectionView.showsHorizontalScrollIndicator = false
|
||||
calleeCollectionView.backgroundColor = UIColor.clear
|
||||
return calleeCollectionView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
var dataList = TUICallState.instance.remoteUserList.value
|
||||
dataList.append(TUICallState.instance.selfUser.value)
|
||||
dataSource.value = removeCallUser(remoteUserList: dataList)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
}
|
||||
|
||||
func removeCallUser(remoteUserList: [User]) -> [User] {
|
||||
let userList = remoteUserList.filter { $0.callRole.value != .call }
|
||||
return userList
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
updateDescribeLabel()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(describeLabel)
|
||||
addSubview(calleeCollectionView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
describeLabel.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(calleeCollectionView.snp.top).offset(-5.scaleWidth())
|
||||
make.centerX.equalTo(self)
|
||||
make.width.equalTo(Screen_Width)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
calleeCollectionView.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(self)
|
||||
make.width.equalTo(Screen_Width)
|
||||
make.height.equalTo(40)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
calleeCollectionView.register(InviteeAvatarCell.self, forCellWithReuseIdentifier: "InviteeAvatarCell")
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
remoteUserChanged()
|
||||
}
|
||||
|
||||
func remoteUserChanged() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
var dataList = newValue
|
||||
dataList.append(TUICallState.instance.selfUser.value)
|
||||
self.dataSource.value = self.removeCallUser(remoteUserList: dataList)
|
||||
|
||||
self.updateDescribeLabel()
|
||||
self.calleeCollectionView.reloadData()
|
||||
self.calleeCollectionView.layoutIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
func updateDescribeLabel() {
|
||||
let count = dataSource.value.count
|
||||
|
||||
if count >= 1 {
|
||||
describeLabel.isHidden = false
|
||||
} else {
|
||||
describeLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
|
||||
extension InviteeAvatarListView {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return dataSource.value.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "InviteeAvatarCell", for: indexPath) as! InviteeAvatarCell
|
||||
cell.initCell(user: dataSource.value[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: kItemWidth, height: kItemWidth)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return kSpacing
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return kSpacing
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
let cellCount = collectionView.numberOfItems(inSection: section)
|
||||
var inset = (collectionView.bounds.size.width - ((CGFloat(cellCount)) * kItemWidth) - ((CGFloat(cellCount) - 1) * kSpacing)) * 0.5
|
||||
inset = max(inset, 0.0)
|
||||
return UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: 0.0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AvatarBackgroundView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class BackgroundView: UIView {
|
||||
|
||||
let remoteUserListObserver = Observer()
|
||||
|
||||
let userHeadImageView: UIImageView = {
|
||||
let userHeadImageView = UIImageView(frame: CGRect.zero)
|
||||
userHeadImageView.contentMode = .scaleAspectFill
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
userHeadImageView.image = image
|
||||
}
|
||||
return userHeadImageView
|
||||
}()
|
||||
|
||||
let blurEffectView: UIVisualEffectView = {
|
||||
let blurEffect = UIBlurEffect(style: .light)
|
||||
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
blurEffectView.alpha = 0.65
|
||||
return blurEffectView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUserImage()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(userHeadImageView)
|
||||
addSubview(blurEffectView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.userHeadImageView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
self.blurEffectView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
remoteUserListChanged()
|
||||
}
|
||||
|
||||
func remoteUserListChanged() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.setUserImage()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func setUserImage() {
|
||||
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
|
||||
userHeadImageView.sd_setImage(with: URL(string: remoteUser.avatar.value))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// CallStatusView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CallStatusTipView: UIView {
|
||||
|
||||
let selfCallStatusObserver = Observer()
|
||||
let networkQualityObserver = Observer()
|
||||
private var isFirstShowAccept: Bool = true
|
||||
|
||||
let callStatusLabel: UILabel = {
|
||||
let callStatusLabel = UILabel(frame: CGRect.zero)
|
||||
callStatusLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
|
||||
callStatusLabel.font = UIFont.systemFont(ofSize: 15.0)
|
||||
callStatusLabel.backgroundColor = UIColor.clear
|
||||
callStatusLabel.textAlignment = .center
|
||||
return callStatusLabel
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isFirstShowAccept = (TUICallState.instance.selfUser.value.callStatus.value == .accept) ? false : true
|
||||
updateStatusText()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfCallStatusObserver)
|
||||
TUICallState.instance.networkQualityReminder.removeObserver(networkQualityObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(callStatusLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.callStatusLabel.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
callStatusChange()
|
||||
networkQualityChange()
|
||||
}
|
||||
|
||||
func callStatusChange() {
|
||||
TUICallState.instance.selfUser.value.callStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateStatusText()
|
||||
})
|
||||
}
|
||||
|
||||
func networkQualityChange() {
|
||||
TUICallState.instance.networkQualityReminder.addObserver(networkQualityObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateNetworkQualityText()
|
||||
})
|
||||
}
|
||||
|
||||
func updateNetworkQualityText() {
|
||||
switch TUICallState.instance.networkQualityReminder.value {
|
||||
case .Local:
|
||||
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.Self.NetworkLowQuality") ?? ""
|
||||
break
|
||||
case .Remote:
|
||||
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.OtherParty.NetworkLowQuality") ?? ""
|
||||
break
|
||||
case .None:
|
||||
updateStatusText()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func updateStatusText() {
|
||||
switch TUICallState.instance.selfUser.value.callStatus.value {
|
||||
case .waiting:
|
||||
self.callStatusLabel.text = self.getCurrentWaitingText()
|
||||
break
|
||||
case .accept:
|
||||
if isFirstShowAccept {
|
||||
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.accept") ?? ""
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.isFirstShowAccept = false
|
||||
}
|
||||
} else {
|
||||
self.callStatusLabel.text = ""
|
||||
}
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func getCurrentWaitingText() -> String {
|
||||
var waitingText = String()
|
||||
switch TUICallState.instance.mediaType.value {
|
||||
case .audio:
|
||||
if TUICallState.instance.selfUser.value.callRole.value == .call {
|
||||
waitingText = TUICallKitLocalize(key: "TUICallKit.waitAccept") ?? ""
|
||||
} else {
|
||||
waitingText = TUICallKitLocalize(key: "TUICallKit.inviteToAudioCall") ?? ""
|
||||
}
|
||||
case .video:
|
||||
if TUICallState.instance.selfUser.value.callRole.value == .call {
|
||||
waitingText = TUICallKitLocalize(key: "TUICallKit.waitAccept") ?? ""
|
||||
} else {
|
||||
waitingText = TUICallKitLocalize(key: "TUICallKit.inviteToVideoCall") ?? ""
|
||||
}
|
||||
case .unknown:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return waitingText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// CallUserInfoView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class CallUserInfoView: UIView {
|
||||
|
||||
let remoteUserListObserver = Observer()
|
||||
|
||||
let userHeadImageView: UIImageView = {
|
||||
let userHeadImageView = UIImageView(frame: CGRect.zero)
|
||||
userHeadImageView.layer.masksToBounds = true
|
||||
userHeadImageView.layer.cornerRadius = 6.0
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
|
||||
userHeadImageView.image = image
|
||||
}
|
||||
return userHeadImageView
|
||||
}()
|
||||
|
||||
let userNameLabel: UILabel = {
|
||||
let userNameLabel = UILabel(frame: CGRect.zero)
|
||||
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
|
||||
userNameLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
|
||||
userNameLabel.backgroundColor = UIColor.clear
|
||||
userNameLabel.textAlignment = .center
|
||||
return userNameLabel
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setUserImageAndName()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(userHeadImageView)
|
||||
addSubview(userNameLabel)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
self.userHeadImageView.snp.makeConstraints { make in
|
||||
make.top.centerX.equalTo(self)
|
||||
make.size.equalTo(CGSize(width: 100.scaleWidth(), height: 100.scaleWidth()))
|
||||
}
|
||||
self.userNameLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(userHeadImageView.snp.bottom).offset(10.scaleHeight())
|
||||
make.centerX.equalTo(self)
|
||||
make.width.equalTo(self)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
remoteUserListChanged()
|
||||
}
|
||||
|
||||
func remoteUserListChanged() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.setUserImageAndName()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Update UI
|
||||
func setUserImageAndName() {
|
||||
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
|
||||
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
|
||||
|
||||
if let url = URL(string: remoteUser.avatar.value) {
|
||||
userHeadImageView.sd_setImage(with: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
//
|
||||
// GroupCallVideoCell.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/3/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICallEngine
|
||||
import SnapKit
|
||||
|
||||
class GroupCallVideoCell: UICollectionViewCell {
|
||||
|
||||
let selfUserStatusObserver = Observer()
|
||||
let remoteUserStatusObserver = Observer()
|
||||
let selfUserVideoAvailableObserver = Observer()
|
||||
let remoteUserVideoAvailableObserver = Observer()
|
||||
let isCameraOpenObserver = Observer()
|
||||
let isMicMuteObserver = Observer()
|
||||
let remotePlayoutVolumeObserver = Observer()
|
||||
let selfPlayoutVolumeObserver = Observer()
|
||||
let isShowLargeViewUserIdObserver = Observer()
|
||||
let enableBlurBackgroundObserver = Observer()
|
||||
let selfNetworkQualityObserver = Observer()
|
||||
let remoteNetworkQualityObserver = Observer()
|
||||
|
||||
private var viewModel = GroupCallVideoCellViewModel(remote: User())
|
||||
private var user: User = User()
|
||||
private var isSelf: Bool = false
|
||||
|
||||
private var renderView = VideoView()
|
||||
|
||||
private let titleLabel = {
|
||||
let titleLabel = UILabel(frame: CGRect.zero)
|
||||
titleLabel.textColor = UIColor.t_colorWithHexString(color: "FFFFFF")
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 12)
|
||||
titleLabel.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
|
||||
return titleLabel
|
||||
}()
|
||||
|
||||
private let loadingView = {
|
||||
let view = GroupLoadingView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
private let avatarImageView = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let micImageView = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_mic_off") {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let volumeImageView = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "icon_volume") {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let networkQualityView = {
|
||||
let imageView = UIImageView(frame: CGRect.zero)
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "group_network_low_quality") {
|
||||
imageView.image = image
|
||||
}
|
||||
return imageView
|
||||
}()
|
||||
|
||||
lazy var switchCameraBtn: GroupCallVideoCustomButton = {
|
||||
let btn = GroupCallVideoCustomButton(type: .system)
|
||||
btn.contentMode = .scaleAspectFit
|
||||
if let image = TUICallKitCommon.getBundleImage(name: "group_switch_camera") {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(switchCameraTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var virtualBackgroundBtn: GroupCallVideoCustomButton = {
|
||||
let btn = GroupCallVideoCustomButton(type: .system)
|
||||
btn.contentMode = .scaleAspectFit
|
||||
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
btn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
btn.addTarget(self, action: #selector(virtualBackgroundTouchEvent(sender: )), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
if !loadingView.isHidden {
|
||||
loadingView.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if isSelf {
|
||||
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfUserStatusObserver)
|
||||
TUICallState.instance.selfUser.value.videoAvailable.removeObserver(selfUserVideoAvailableObserver)
|
||||
TUICallState.instance.selfUser.value.playoutVolume.removeObserver(selfPlayoutVolumeObserver)
|
||||
TUICallState.instance.enableBlurBackground.removeObserver(enableBlurBackgroundObserver)
|
||||
TUICallState.instance.selfUser.value.networkQualityReminder.removeObserver(selfNetworkQualityObserver)
|
||||
} else {
|
||||
viewModel.remoteUserStatus.removeObserver(remoteUserStatusObserver)
|
||||
viewModel.remoteUserVideoAvailable.removeObserver(remoteUserVideoAvailableObserver)
|
||||
viewModel.remoteIsShowLowNetworkQuality.removeObserver(remoteNetworkQualityObserver)
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
viewModel.remoteUserVolume.removeObserver(remotePlayoutVolumeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
TUICallState.instance.showLargeViewUserId.removeObserver(isMicMuteObserver)
|
||||
|
||||
for view in contentView.subviews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
if !loadingView.isHidden {
|
||||
loadingView.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
contentView.addSubview(renderView)
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(loadingView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(micImageView)
|
||||
contentView.addSubview(volumeImageView)
|
||||
contentView.addSubview(networkQualityView)
|
||||
contentView.addSubview(switchCameraBtn)
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
contentView.addSubview(virtualBackgroundBtn)
|
||||
}
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
renderView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self.contentView)
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self.contentView)
|
||||
}
|
||||
loadingView.snp.makeConstraints { make in
|
||||
make.center.equalTo(self)
|
||||
make.width.height.equalTo(40.scaleWidth())
|
||||
}
|
||||
|
||||
if viewModel.isShowLargeViewUserId.value {
|
||||
activateLargeViewConstraints()
|
||||
} else {
|
||||
activateSmallViewConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
func activateLargeViewConstraints() {
|
||||
titleLabel.snp.remakeConstraints { make in
|
||||
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
|
||||
make.height.equalTo(24.scaleWidth())
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
micImageView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
volumeImageView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
if TUICallState.instance.showVirtualBackgroundButton {
|
||||
virtualBackgroundBtn.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
switchCameraBtn.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.centerY.equalTo(virtualBackgroundBtn)
|
||||
make.trailing.equalTo(virtualBackgroundBtn.snp.leading).offset(-10.scaleWidth())
|
||||
}
|
||||
} else {
|
||||
switchCameraBtn.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
}
|
||||
networkQualityView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.centerY.equalTo(switchCameraBtn)
|
||||
make.trailing.equalTo(switchCameraBtn.snp.leading).offset(-10.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
func activateSmallViewConstraints() {
|
||||
volumeImageView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
networkQualityView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
if self.micImageView.isHidden {
|
||||
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
} else {
|
||||
make.trailing.equalTo(self.micImageView.snp.leading).offset(-8.scaleWidth())
|
||||
}
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
micImageView.snp.remakeConstraints { make in
|
||||
make.width.height.equalTo(24.scaleWidth())
|
||||
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
|
||||
}
|
||||
}
|
||||
|
||||
func initCell(user: User) {
|
||||
self.user = user
|
||||
viewModel = GroupCallVideoCellViewModel(remote: user)
|
||||
isSelf = TUICallState.instance.selfUser.value.id.value == user.id.value ? true : false
|
||||
initWaitingUI()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
// MARK: Action Event
|
||||
@objc func switchCameraTouchEvent(sender: UIButton) {
|
||||
CallEngineManager.instance.switchCamera()
|
||||
}
|
||||
|
||||
@objc func virtualBackgroundTouchEvent(sender: UIButton ) {
|
||||
CallEngineManager.instance.setBlurBackground()
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
callStatusChanged()
|
||||
videoAvailableChanged()
|
||||
isCameraOpenChanged()
|
||||
volumeChanged()
|
||||
isShowLargeViewUserIdChanged()
|
||||
isMicMuteChanged()
|
||||
enableBlurBackgroundChanged()
|
||||
networkQualityChanged()
|
||||
}
|
||||
|
||||
func callStatusChanged() {
|
||||
if isSelf {
|
||||
TUICallState.instance.selfUser.value.callStatus.addObserver(selfUserStatusObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateSelfUserUI()
|
||||
})
|
||||
} else {
|
||||
viewModel.remoteUserStatus.addObserver(remoteUserStatusObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateRemoteUserCellUI()
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func videoAvailableChanged() {
|
||||
if isSelf {
|
||||
TUICallState.instance.selfUser.value.videoAvailable.addObserver(selfUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateSelfUserUI()
|
||||
})
|
||||
} else {
|
||||
viewModel.remoteUserVideoAvailable.addObserver(remoteUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateRemoteUserCellUI()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func volumeChanged() {
|
||||
if isSelf {
|
||||
TUICallState.instance.selfUser.value.playoutVolume.addObserver(selfPlayoutVolumeObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.volumeImageView.isHidden = newValue == 0 ? true : false
|
||||
})
|
||||
} else {
|
||||
viewModel.remoteUserVolume.addObserver(remotePlayoutVolumeObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.volumeImageView.isHidden = newValue == 0 ? true : false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isCameraOpenChanged() {
|
||||
if isSelf {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateSelfUserUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isShowLargeViewUserIdChanged() {
|
||||
viewModel.isShowLargeViewUserId.addObserver(isShowLargeViewUserIdObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if newValue {
|
||||
self.updateLargeViewUI()
|
||||
} else {
|
||||
self.updateSmallViewUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isMicMuteChanged() {
|
||||
if isSelf {
|
||||
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.micImageView.isHidden = !newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableBlurBackgroundChanged () {
|
||||
if isSelf && TUICallState.instance.showVirtualBackgroundButton {
|
||||
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
|
||||
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
|
||||
self.virtualBackgroundBtn.setBackgroundImage(image, for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func networkQualityChanged () {
|
||||
if isSelf {
|
||||
TUICallState.instance.selfUser.value.networkQualityReminder.addObserver(selfNetworkQualityObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.networkQualityView.isHidden = !newValue
|
||||
})
|
||||
} else {
|
||||
viewModel.remoteIsShowLowNetworkQuality.addObserver(remoteNetworkQualityObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.networkQualityView.isHidden = !newValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateLargeViewUI() {
|
||||
if isSelf {
|
||||
switchCameraBtn.isHidden = !TUICallState.instance.isCameraOpen.value
|
||||
virtualBackgroundBtn.isHidden = !TUICallState.instance.isCameraOpen.value || !TUICallState.instance.showVirtualBackgroundButton
|
||||
titleLabel.isHidden = false
|
||||
micImageView.isHidden = !TUICallState.instance.isMicMute.value
|
||||
} else {
|
||||
switchCameraBtn.isHidden = true
|
||||
virtualBackgroundBtn.isHidden = true
|
||||
titleLabel.isHidden = false
|
||||
micImageView.isHidden = true
|
||||
}
|
||||
activateLargeViewConstraints()
|
||||
}
|
||||
|
||||
func updateSmallViewUI() {
|
||||
switchCameraBtn.isHidden = true
|
||||
virtualBackgroundBtn.isHidden = true
|
||||
titleLabel.isHidden = true
|
||||
|
||||
if isSelf {
|
||||
micImageView.isHidden = !TUICallState.instance.isMicMute.value
|
||||
} else {
|
||||
micImageView.isHidden = true
|
||||
}
|
||||
activateSmallViewConstraints()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Update UI
|
||||
func initWaitingUI() {
|
||||
titleLabel.text = User.getUserDisplayName(user: viewModel.remoteUser)
|
||||
setUserAvatar()
|
||||
|
||||
if isSelf {
|
||||
initSelfUserUI()
|
||||
} else {
|
||||
updateRemoteUserCellUI()
|
||||
}
|
||||
}
|
||||
|
||||
func initSelfUserUI() {
|
||||
hiddenAllSubView()
|
||||
micImageView.isHidden = !TUICallState.instance.isMicMute.value
|
||||
|
||||
if !VideoFactory.instance.isExistVideoView(videoView: renderView) {
|
||||
renderView = VideoFactory.instance.createVideoView(userId: TUICallState.instance.selfUser.value.id.value, frame: CGRect.zero)
|
||||
renderView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
if TUICallState.instance.isCameraOpen.value == true {
|
||||
renderView.isHidden = false
|
||||
switchCameraBtn.isHidden = false
|
||||
CallEngineManager.instance.openCamera(videoView: renderView)
|
||||
} else {
|
||||
renderView.isHidden = true
|
||||
avatarImageView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelfUserUI() {
|
||||
hiddenAllSubView()
|
||||
micImageView.isHidden = !TUICallState.instance.isMicMute.value
|
||||
|
||||
if TUICallState.instance.isCameraOpen.value == true {
|
||||
renderView.isHidden = false
|
||||
switchCameraBtn.isHidden = false
|
||||
virtualBackgroundBtn.isHidden = !TUICallState.instance.showVirtualBackgroundButton
|
||||
} else {
|
||||
switchCameraBtn.isHidden = true
|
||||
virtualBackgroundBtn.isHidden = true
|
||||
avatarImageView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func updateRemoteUserCellUI() {
|
||||
hiddenAllSubView()
|
||||
|
||||
if viewModel.remoteUserStatus.value == .waiting {
|
||||
loadingView.isHidden = false
|
||||
loadingView.startAnimating()
|
||||
avatarImageView.isHidden = false
|
||||
} else if viewModel.remoteUserStatus.value == .accept {
|
||||
CallEngineManager.instance.startRemoteView(user: viewModel.remoteUser, videoView: renderView)
|
||||
|
||||
if viewModel.remoteUserVideoAvailable.value == true {
|
||||
renderView.isUserInteractionEnabled = false
|
||||
renderView.isHidden = false
|
||||
} else {
|
||||
avatarImageView.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hiddenAllSubView() {
|
||||
renderView.isHidden = true
|
||||
loadingView.isHidden = true
|
||||
loadingView.stopAnimating()
|
||||
avatarImageView.isHidden = true
|
||||
micImageView.isHidden = true
|
||||
volumeImageView.isHidden = true
|
||||
switchCameraBtn.isHidden = true
|
||||
virtualBackgroundBtn.isHidden = true
|
||||
networkQualityView.isHidden = true
|
||||
}
|
||||
|
||||
// MARK: Private Method
|
||||
func setUserAvatar() {
|
||||
let userIcon: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
|
||||
|
||||
if viewModel.remoteUser.avatar.value == "" {
|
||||
guard let image = userIcon else { return }
|
||||
avatarImageView.image = image
|
||||
} else {
|
||||
avatarImageView.sd_setImage(with: URL(string: viewModel.remoteUser.avatar.value), placeholderImage: userIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupCallVideoCustomButton: UIButton {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let expandedBounds = bounds.insetBy(dx: -4.scaleWidth(), dy: -4.scaleWidth())
|
||||
return expandedBounds.contains(point) ? self : nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
//
|
||||
// GroupCallVideoFlowLayout.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public var showLargeViewIndex: Int = -1
|
||||
|
||||
enum MosaicSegmentStyle {
|
||||
case fullWidth
|
||||
case fiftyFifty
|
||||
case oneThird
|
||||
case threeOneThirds
|
||||
case twoThirdsOneThirdRight
|
||||
case twoThirdsOneThirdCenter
|
||||
case oneThirdTwoThirds
|
||||
}
|
||||
|
||||
class GroupCallVideoFlowLayout: UICollectionViewFlowLayout {
|
||||
private var deletingIndexPaths = [IndexPath]()
|
||||
private var insertingIndexPaths = [IndexPath]()
|
||||
|
||||
private var contentBounds = CGRect.zero
|
||||
private var cachedAttributes = [UICollectionViewLayoutAttributes]()
|
||||
|
||||
override func prepare() {
|
||||
super.prepare()
|
||||
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
cachedAttributes.removeAll()
|
||||
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
|
||||
|
||||
let count = collectionView.numberOfItems(inSection: 0)
|
||||
var currentIndex = 0
|
||||
var segment: MosaicSegmentStyle = getSegment(count: count, currentIndex: currentIndex)
|
||||
let cvWidth = collectionView.bounds.size.width
|
||||
var lastFrame: CGRect = (count != 2 || showLargeViewIndex >= 0) ? .zero : CGRect(x: 0, y: cvWidth / 5, width: 0, height: 0)
|
||||
|
||||
while currentIndex < count {
|
||||
var segmentRects = [CGRect]()
|
||||
switch segment {
|
||||
case .fullWidth:
|
||||
segmentRects = [CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth)]
|
||||
|
||||
case .fiftyFifty:
|
||||
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 2)
|
||||
let horizontalSlices = segmentFrame.dividedIntegral(fraction: 0.5, from: .minXEdge)
|
||||
segmentRects = [horizontalSlices.first, horizontalSlices.second]
|
||||
|
||||
case .oneThird:
|
||||
segmentRects = [CGRect(x: cvWidth / 4.0, y: lastFrame.maxY + 1.0, width: cvWidth / 2.0, height: cvWidth / 2.0)]
|
||||
|
||||
case .threeOneThirds:
|
||||
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 3)
|
||||
let horizontalSlicesFirst = segmentFrame.dividedIntegral(fraction: 1.0 / 3, from: .minXEdge)
|
||||
let horizontalSlices = horizontalSlicesFirst.second.dividedIntegral(fraction: 0.5, from: .minXEdge)
|
||||
segmentRects = [horizontalSlicesFirst.first, horizontalSlices.first, horizontalSlices.second]
|
||||
|
||||
case .twoThirdsOneThirdRight:
|
||||
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
||||
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
|
||||
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
||||
segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
|
||||
|
||||
case .twoThirdsOneThirdCenter:
|
||||
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
||||
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
|
||||
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
||||
segmentRects = [verticalSlices.first, horizontalSlices.second, verticalSlices.second]
|
||||
|
||||
case .oneThirdTwoThirds:
|
||||
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
||||
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3), from: .minXEdge)
|
||||
let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
||||
segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
|
||||
}
|
||||
|
||||
// Create and cache layout attributes for calculated frames.
|
||||
for rect in segmentRects {
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
|
||||
attributes.frame = rect
|
||||
|
||||
cachedAttributes.append(attributes)
|
||||
contentBounds = contentBounds.union(lastFrame)
|
||||
|
||||
currentIndex += 1
|
||||
lastFrame = rect
|
||||
}
|
||||
|
||||
segment = getSegment(count: count, currentIndex: currentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func getSegment(count: Int, currentIndex: Int) -> MosaicSegmentStyle {
|
||||
var segment: MosaicSegmentStyle
|
||||
|
||||
// Determine first segment style.
|
||||
if currentIndex == 0 {
|
||||
var segment: MosaicSegmentStyle = .threeOneThirds
|
||||
|
||||
if count == 1 {
|
||||
segment = .fullWidth
|
||||
} else if count >= 2 && count <= 4 {
|
||||
if showLargeViewIndex >= 0 {
|
||||
segment = .fullWidth
|
||||
} else {
|
||||
segment = .fiftyFifty
|
||||
}
|
||||
} else if showLargeViewIndex == 0 {
|
||||
segment = .oneThirdTwoThirds
|
||||
} else if showLargeViewIndex == 1 {
|
||||
segment = .twoThirdsOneThirdCenter
|
||||
} else if showLargeViewIndex == 2 {
|
||||
segment = .twoThirdsOneThirdRight
|
||||
}
|
||||
|
||||
return segment
|
||||
}
|
||||
|
||||
// Determine the next segment style.
|
||||
switch count - currentIndex {
|
||||
case 1:
|
||||
if count == 3 {
|
||||
segment = .oneThird
|
||||
} else if count > 4 && showLargeViewIndex == (count - 1) {
|
||||
segment = .oneThirdTwoThirds
|
||||
} else {
|
||||
segment = .threeOneThirds
|
||||
}
|
||||
case 2:
|
||||
if count == 4 {
|
||||
segment = .fiftyFifty
|
||||
} else if count > 4 && showLargeViewIndex == currentIndex {
|
||||
segment = .oneThirdTwoThirds
|
||||
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
|
||||
segment = .twoThirdsOneThirdCenter
|
||||
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
|
||||
segment = .twoThirdsOneThirdRight
|
||||
} else {
|
||||
segment = .threeOneThirds
|
||||
}
|
||||
default:
|
||||
if count > 4 && showLargeViewIndex == currentIndex {
|
||||
segment = .oneThirdTwoThirds
|
||||
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
|
||||
segment = .twoThirdsOneThirdCenter
|
||||
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
|
||||
segment = .twoThirdsOneThirdRight
|
||||
} else {
|
||||
segment = .threeOneThirds
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return segment
|
||||
}
|
||||
|
||||
override var collectionViewContentSize: CGSize {
|
||||
return contentBounds.size
|
||||
}
|
||||
|
||||
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
guard let collectionView = collectionView else { return false }
|
||||
return !newBounds.size.equalTo(collectionView.bounds.size)
|
||||
}
|
||||
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
return cachedAttributes[indexPath.item]
|
||||
}
|
||||
|
||||
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
guard let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) else { return nil }
|
||||
|
||||
if !deletingIndexPaths.isEmpty {
|
||||
if deletingIndexPaths.contains(itemIndexPath) {
|
||||
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
attributes.alpha = 0.0
|
||||
attributes.zIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
guard let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) else { return nil }
|
||||
|
||||
if insertingIndexPaths.contains(itemIndexPath) {
|
||||
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
attributes.alpha = 0.0
|
||||
attributes.zIndex = 0
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
|
||||
super.prepare(forCollectionViewUpdates: updateItems)
|
||||
|
||||
for update in updateItems {
|
||||
switch update.updateAction {
|
||||
case .delete:
|
||||
guard let indexPath = update.indexPathBeforeUpdate else { return }
|
||||
deletingIndexPaths.append(indexPath)
|
||||
case .insert:
|
||||
guard let indexPath = update.indexPathAfterUpdate else { return }
|
||||
insertingIndexPaths.append(indexPath)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func finalizeCollectionViewUpdates() {
|
||||
super.finalizeCollectionViewUpdates()
|
||||
|
||||
deletingIndexPaths.removeAll()
|
||||
insertingIndexPaths.removeAll()
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
var attributesArray = [UICollectionViewLayoutAttributes]()
|
||||
|
||||
// Find any cell that sits within the query rect.
|
||||
guard let lastIndex = cachedAttributes.indices.last,
|
||||
let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
|
||||
|
||||
// Starting from the match, loop up and down through the array until all the attributes
|
||||
// have been added within the query rect.
|
||||
for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
|
||||
guard attributes.frame.maxY >= rect.minY else { break }
|
||||
attributesArray.append(attributes)
|
||||
}
|
||||
|
||||
for attributes in cachedAttributes[firstMatchIndex...] {
|
||||
guard attributes.frame.minY <= rect.maxY else { break }
|
||||
attributesArray.append(attributes)
|
||||
}
|
||||
|
||||
return attributesArray
|
||||
}
|
||||
|
||||
// Perform a binary search on the cached attributes array.
|
||||
func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
|
||||
if end < start { return nil }
|
||||
|
||||
let mid = (start + end) / 2
|
||||
let attributes = cachedAttributes[mid]
|
||||
|
||||
if attributes.frame.intersects(rect) {
|
||||
return mid
|
||||
} else {
|
||||
if attributes.frame.maxY < rect.minY {
|
||||
return binSearch(rect, start: (mid + 1), end: end)
|
||||
} else {
|
||||
return binSearch(rect, start: start, end: (mid - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// GroupCallVideoLayout.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class GroupCallVideoLayout: UIView, UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
let selfCallStatusObserver = Observer()
|
||||
let isCameraOpenObserver = Observer()
|
||||
let selfUser = TUICallState.instance.selfUser.value
|
||||
|
||||
var allUserList = [User]()
|
||||
|
||||
lazy var calleeCollectionView = {
|
||||
let flowLayout = GroupCallVideoFlowLayout()
|
||||
let calleeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flowLayout)
|
||||
calleeCollectionView.delegate = self
|
||||
calleeCollectionView.dataSource = self
|
||||
calleeCollectionView.showsVerticalScrollIndicator = false
|
||||
calleeCollectionView.showsHorizontalScrollIndicator = false
|
||||
calleeCollectionView.backgroundColor = UIColor.clear
|
||||
return calleeCollectionView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor.clear
|
||||
processUserList(remoteUserList: TUICallState.instance.remoteUserList.value)
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
|
||||
TUICallState.instance.remoteUserList.removeObserver(selfCallStatusObserver)
|
||||
|
||||
showLargeViewIndex = -1
|
||||
|
||||
for view in subviews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
showHistoryLargeView()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(calleeCollectionView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
calleeCollectionView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
for i in 0..<9 {
|
||||
calleeCollectionView.register(GroupCallVideoCell.self, forCellWithReuseIdentifier: "GroupCallVideoCell_\(i)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Set TUICallState showLargeViewUserId
|
||||
func setShowLargeViewUserId(userId: String) {
|
||||
TUICallState.instance.showLargeViewUserId.value = userId
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
remoteUserChanged()
|
||||
isCameraOpenChanged()
|
||||
}
|
||||
|
||||
func remoteUserChanged() {
|
||||
TUICallState.instance.remoteUserList.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
self.processUserList(remoteUserList: newValue)
|
||||
self.calleeCollectionView.reloadData()
|
||||
})
|
||||
}
|
||||
|
||||
func processUserList(remoteUserList: [User]) {
|
||||
allUserList.removeAll()
|
||||
selfUser.index = 0
|
||||
allUserList.append(selfUser)
|
||||
|
||||
for (index, value) in remoteUserList.enumerated() {
|
||||
value.index = index + 1
|
||||
allUserList.append(value)
|
||||
}
|
||||
}
|
||||
|
||||
func isCameraOpenChanged() {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if newValue {
|
||||
self.showMySelfAsLargeView()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func showMySelfAsLargeView() {
|
||||
var row = -1
|
||||
for (index, element) in allUserList.enumerated() where element.id.value == selfUser.id.value {
|
||||
row = index
|
||||
}
|
||||
if row >= 0 && selfUser.id.value != TUICallState.instance.showLargeViewUserId.value {
|
||||
let indexPath = IndexPath(row: row, section: 0)
|
||||
performUpdates(indexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func showHistoryLargeView() {
|
||||
var row = -1
|
||||
for (index, element) in allUserList.enumerated() where element.id.value == TUICallState.instance.showLargeViewUserId.value {
|
||||
row = index
|
||||
}
|
||||
if row >= 0 && row < allUserList.count {
|
||||
let indexPath = IndexPath(row: row, section: 0)
|
||||
performUpdates(indexPath: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
|
||||
extension GroupCallVideoLayout {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return allUserList.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GroupCallVideoCell_\(indexPath.row)",
|
||||
for: indexPath) as! GroupCallVideoCell
|
||||
cell.initCell(user: allUserList[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
performUpdates(indexPath: indexPath)
|
||||
}
|
||||
|
||||
func performUpdates(indexPath: IndexPath) {
|
||||
let count = allUserList.count
|
||||
let remoteUpdates = getRemoteUpdates(indexPath: indexPath)
|
||||
|
||||
var firstBigFlag = false
|
||||
if count >= 2 && count <= 4 && indexPath.row != showLargeViewIndex {
|
||||
firstBigFlag = true
|
||||
}
|
||||
|
||||
showLargeViewIndex = (showLargeViewIndex == indexPath.row) ? -1 : indexPath.row
|
||||
if firstBigFlag {
|
||||
showLargeViewIndex = 0
|
||||
}
|
||||
|
||||
setShowLargeViewUserId(userId: (showLargeViewIndex >= 0) ? allUserList[indexPath.row].id.value : " ")
|
||||
|
||||
// Animate all other update types together.
|
||||
calleeCollectionView.cancelInteractiveMovement()
|
||||
calleeCollectionView.performBatchUpdates({
|
||||
var deletes = [Int]()
|
||||
var inserts = [(user:User, index:Int)]()
|
||||
|
||||
for update in remoteUpdates {
|
||||
switch update {
|
||||
case let .delete(index):
|
||||
calleeCollectionView.deleteItems(at: [IndexPath(item: index, section: 0)])
|
||||
deletes.append(index)
|
||||
|
||||
case let .insert(user, index):
|
||||
calleeCollectionView.insertItems(at: [IndexPath(item: index, section: 0)])
|
||||
inserts.append((user, index))
|
||||
|
||||
case let .move(fromIndex, toIndex):
|
||||
calleeCollectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
|
||||
to: IndexPath(item: toIndex, section: 0))
|
||||
deletes.append(fromIndex)
|
||||
inserts.append((allUserList[fromIndex], toIndex))
|
||||
}
|
||||
}
|
||||
|
||||
for deletedIndex in deletes.sorted().reversed() {
|
||||
allUserList.remove(at: deletedIndex)
|
||||
}
|
||||
|
||||
let sortedInserts = inserts.sorted(by: { (userA, userB) -> Bool in
|
||||
return userA.index <= userB.index
|
||||
})
|
||||
|
||||
for insertion in sortedInserts {
|
||||
if insertion.index >= allUserList.startIndex && insertion.index <= allUserList.endIndex {
|
||||
allUserList.insert(insertion.user, at: insertion.index)
|
||||
}
|
||||
}
|
||||
}) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.calleeCollectionView.endInteractiveMovement()
|
||||
}
|
||||
}
|
||||
|
||||
func getRemoteUpdates(indexPath: IndexPath) -> [UserUpdate] {
|
||||
let count = allUserList.count
|
||||
|
||||
if count < 2 || count > 4 || indexPath.row >= count {
|
||||
return [UserUpdate]()
|
||||
}
|
||||
|
||||
if indexPath.row == showLargeViewIndex {
|
||||
return [
|
||||
UserUpdate.move(0, allUserList[indexPath.row].index)
|
||||
]
|
||||
}
|
||||
|
||||
if count == 2 || allUserList[0].index == 0 {
|
||||
return [
|
||||
UserUpdate.move(indexPath.row, 0)
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
UserUpdate.move(0, allUserList[0].index),
|
||||
UserUpdate.move(indexPath.row, 0)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// GroupLoadingView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by noah on 2023/11/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let kGroupLoadingViewDotSize: CGFloat = 6.scaleWidth()
|
||||
|
||||
class GroupLoadingView: UIView {
|
||||
|
||||
let dotSpacing: CGFloat = 8.scaleWidth()
|
||||
let animationDuration: TimeInterval = 0.5
|
||||
|
||||
private let dotLeft: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white
|
||||
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
|
||||
view.alpha = 0.8
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dotCenter: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white
|
||||
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
|
||||
view.alpha = 0.5
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dotRight: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white
|
||||
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
|
||||
view.alpha = 0.2
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(dotLeft)
|
||||
addSubview(dotCenter)
|
||||
addSubview(dotRight)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
dotLeft.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self)
|
||||
make.centerX.equalTo(dotCenter).offset(-(dotSpacing + kGroupLoadingViewDotSize))
|
||||
make.width.height.equalTo(kGroupLoadingViewDotSize)
|
||||
}
|
||||
dotCenter.snp.makeConstraints { make in
|
||||
make.center.equalTo(self)
|
||||
make.width.height.equalTo(kGroupLoadingViewDotSize)
|
||||
}
|
||||
dotRight.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self)
|
||||
make.centerX.equalTo(dotCenter).offset(dotSpacing + kGroupLoadingViewDotSize)
|
||||
make.width.height.equalTo(kGroupLoadingViewDotSize)
|
||||
}
|
||||
}
|
||||
|
||||
func resetDotsAlpha() {
|
||||
dotLeft.alpha = 0.8
|
||||
dotCenter.alpha = 0.5
|
||||
dotRight.alpha = 0.2
|
||||
}
|
||||
|
||||
// MARK: UI Animation
|
||||
private var isAnimation: Bool = false
|
||||
func startAnimating() {
|
||||
if isAnimation {
|
||||
return
|
||||
} else {
|
||||
isAnimation = true
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: animationDuration) {
|
||||
self.dotLeft.alpha = 0.2
|
||||
self.dotCenter.alpha = 0.8
|
||||
self.dotRight.alpha = 0.5
|
||||
} completion: { finished in
|
||||
if finished {
|
||||
self.startSecondAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startSecondAnimation() {
|
||||
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
|
||||
self.dotLeft.alpha = 0.2
|
||||
self.dotCenter.alpha = 0.5
|
||||
self.dotRight.alpha = 0.8
|
||||
}, completion: { finished in
|
||||
if finished {
|
||||
self.startThirdAnimation()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func startThirdAnimation() {
|
||||
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
|
||||
self.dotRight.alpha = 0.5
|
||||
}, completion: { finished in
|
||||
if finished {
|
||||
self.startFourthAnimation()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func startFourthAnimation() {
|
||||
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
|
||||
self.dotLeft.alpha = 0.8
|
||||
self.dotCenter.alpha = 0.5
|
||||
self.dotRight.alpha = 0.2
|
||||
}, completion: { finished in
|
||||
if finished {
|
||||
self.isAnimation = false
|
||||
self.startAnimating()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
dotLeft.layer.removeAllAnimations()
|
||||
dotCenter.layer.removeAllAnimations()
|
||||
dotRight.layer.removeAllAnimations()
|
||||
resetDotsAlpha()
|
||||
isAnimation = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// SingleLayoutView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let kCallKitSingleSmallVideoViewWidth = 100.0
|
||||
private let kCallKitSingleSmallVideoViewFrame = CGRect(x: ScreenSize.width - kCallKitSingleSmallVideoViewWidth,
|
||||
y: StatusBar_Height + 40,
|
||||
width: kCallKitSingleSmallVideoViewWidth,
|
||||
height: kCallKitSingleSmallVideoViewWidth / 9.0 * 16.0)
|
||||
private let kCallKitSingleLargeVideoViewFrame = CGRect(x: 0, y: 0, width: ScreenSize.width, height: ScreenSize.height)
|
||||
|
||||
class SingleCallVideoLayout: UIView {
|
||||
|
||||
let selfCallStatusObserver = Observer()
|
||||
let isCameraOpenObserver = Observer()
|
||||
let enableBlurBackgroundObserver = Observer()
|
||||
let remoteUserListObserver = Observer()
|
||||
|
||||
var remoteHadInit = false
|
||||
var isLocalPreViewLarge: Bool = true
|
||||
|
||||
var localPreView: VideoView {
|
||||
if VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] == nil {
|
||||
let _ = VideoFactory.instance.createVideoView(userId: TUICallState.instance.selfUser.value.id.value, frame: CGRect.zero)
|
||||
}
|
||||
return VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
|
||||
}
|
||||
|
||||
var remotePreView: VideoView {
|
||||
guard let remoteUser = TUICallState.instance.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)
|
||||
}
|
||||
return VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
|
||||
}
|
||||
var remoteUser: User?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor.t_colorWithHexString(color: "#242424")
|
||||
|
||||
if TUICallState.instance.mediaType.value != .video {
|
||||
return
|
||||
}
|
||||
|
||||
initPreView()
|
||||
registerObserveState()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfCallStatusObserver)
|
||||
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
|
||||
TUICallState.instance.enableBlurBackground.removeObserver(enableBlurBackgroundObserver)
|
||||
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
}
|
||||
|
||||
// MARK: Register TUICallState Observer && Update UI
|
||||
func registerObserveState() {
|
||||
callStatusChanged()
|
||||
cameraStateChanged()
|
||||
enableBlurBackgroundStateChange()
|
||||
remoteUserListChange()
|
||||
}
|
||||
|
||||
func callStatusChanged() {
|
||||
TUICallState.instance.selfUser.value.callStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if TUICallState.instance.mediaType.value == .video &&
|
||||
TUICallState.instance.selfUser.value.callStatus.value == .accept {
|
||||
self.initRemotePreView()
|
||||
self.setBeginAcceptPreview()
|
||||
}
|
||||
|
||||
if TUICallState.instance.mediaType.value == .unknown &&
|
||||
TUICallState.instance.selfUser.value.callStatus.value == .none {
|
||||
self.setEndPreview()
|
||||
self.deinitPreView()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func cameraStateChanged() {
|
||||
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if newValue == false && self.isLocalPreViewLarge == false {
|
||||
self.localPreView.isHidden = true
|
||||
} else {
|
||||
self.localPreView.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func enableBlurBackgroundStateChange() {
|
||||
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if newValue == true && self.isLocalPreViewLarge == false {
|
||||
switchPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remoteUserListChange() {
|
||||
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver) { [weak self] newValue, _ in
|
||||
guard let self = self else { return }
|
||||
if !remoteHadInit && TUICallState.instance.selfUser.value.callStatus.value == .accept {
|
||||
self.initRemotePreView()
|
||||
self.setBeginAcceptPreview()}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: update UI
|
||||
func switchPreview() {
|
||||
if isLocalPreViewLarge {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.localPreView.frame = kCallKitSingleSmallVideoViewFrame
|
||||
self.remotePreView.frame = kCallKitSingleLargeVideoViewFrame
|
||||
} completion: { finished in
|
||||
self.sendSubviewToBack(self.remotePreView)
|
||||
}
|
||||
isLocalPreViewLarge = false
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.localPreView.frame = kCallKitSingleLargeVideoViewFrame
|
||||
self.remotePreView.frame = kCallKitSingleSmallVideoViewFrame
|
||||
} completion: { finished in
|
||||
self.sendSubviewToBack(self.localPreView)
|
||||
}
|
||||
isLocalPreViewLarge = true
|
||||
}
|
||||
}
|
||||
|
||||
func setBeginAcceptPreview() {
|
||||
remotePreView.isHidden = false
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.localPreView.frame = kCallKitSingleSmallVideoViewFrame
|
||||
self.remotePreView.frame = kCallKitSingleLargeVideoViewFrame
|
||||
self.localPreView.isHidden = TUICallState.instance.isCameraOpen.value ? false : true
|
||||
} completion: { finished in
|
||||
self.sendSubviewToBack(self.remotePreView)
|
||||
}
|
||||
isLocalPreViewLarge = false
|
||||
}
|
||||
|
||||
func setEndPreview() {
|
||||
CallEngineManager.instance.closeCamera()
|
||||
CallEngineManager.instance.stopRemoteView(user: self.remoteUser ?? User())
|
||||
|
||||
self.remoteUser = nil
|
||||
isLocalPreViewLarge = true
|
||||
}
|
||||
|
||||
func initPreView() {
|
||||
if TUICallState.instance.selfUser.value.callStatus.value == .waiting {
|
||||
initLocalPreView()
|
||||
} else if TUICallState.instance.selfUser.value.callStatus.value == .accept {
|
||||
initLocalPreView()
|
||||
initRemotePreView()
|
||||
setBeginAcceptPreview()
|
||||
}
|
||||
}
|
||||
|
||||
func initLocalPreView() {
|
||||
localPreView.frame = kCallKitSingleLargeVideoViewFrame
|
||||
localPreView.delegate = self
|
||||
localPreView.isUserInteractionEnabled = true
|
||||
localPreView.isHidden = false
|
||||
addSubview(localPreView)
|
||||
|
||||
if TUICallState.instance.selfUser.value.callStatus.value == .waiting {
|
||||
CallEngineManager.instance.openCamera(videoView: localPreView)
|
||||
} else if TUICallState.instance.selfUser.value.callStatus.value == .accept && TUICallState.instance.isCameraOpen.value == true {
|
||||
CallEngineManager.instance.openCamera(videoView: localPreView)
|
||||
}
|
||||
|
||||
if TUICallState.instance.isCameraOpen.value == false {
|
||||
localPreView.isHidden = true
|
||||
} else {
|
||||
localPreView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func initRemotePreView() {
|
||||
guard let remoteUser = TUICallState.instance.remoteUserList.value.first else { return }
|
||||
self.remoteUser = remoteUser
|
||||
remotePreView.frame = kCallKitSingleSmallVideoViewFrame
|
||||
remotePreView.isUserInteractionEnabled = true
|
||||
remotePreView.delegate = self
|
||||
remotePreView.isHidden = true
|
||||
addSubview(self.remotePreView)
|
||||
remoteHadInit = true
|
||||
|
||||
CallEngineManager.instance.startRemoteView(user: remoteUser, videoView: remotePreView)
|
||||
}
|
||||
|
||||
func deinitPreView() {
|
||||
localPreView.removeFromSuperview()
|
||||
remotePreView.removeFromSuperview()
|
||||
VideoFactory.instance.viewMap.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleCallVideoLayout: VideoViewDelegate {
|
||||
func tapGestureAction(tapGesture: UITapGestureRecognizer) {
|
||||
if tapGesture.view?.frame.size.width == CGFloat(kCallKitSingleSmallVideoViewWidth) {
|
||||
switchPreview()
|
||||
} else {
|
||||
self.clickFullScreen()
|
||||
}
|
||||
|
||||
if TUICallState.instance.isCameraOpen.value == false && self.isLocalPreViewLarge == false {
|
||||
self.localPreView.isHidden = true
|
||||
} else {
|
||||
self.localPreView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func clickFullScreen() {
|
||||
if (TUICallState.instance.selfUser.value.callStatus.value == .accept) {
|
||||
TUICallState.instance.isShowFullScreen.value = !TUICallState.instance.isShowFullScreen.value
|
||||
}
|
||||
}
|
||||
|
||||
func panGestureAction(panGesture: UIPanGestureRecognizer) {
|
||||
if panGesture.view?.frame.size.width != CGFloat(kCallKitSingleSmallVideoViewWidth) { return }
|
||||
|
||||
let smallView = panGesture.view?.superview
|
||||
if panGesture.state == .changed {
|
||||
let translation = panGesture.translation(in: self)
|
||||
let newCenterX = translation.x + (smallView?.center.x ?? 0.0)
|
||||
let newCenterY = translation.y + (smallView?.center.y ?? 0.0)
|
||||
|
||||
if newCenterX < ((smallView?.bounds.size.width ?? 0.0) / 2.0) ||
|
||||
newCenterX > self.bounds.size.width - (smallView?.bounds.size.width ?? 0.0) / 2.0 {
|
||||
return
|
||||
}
|
||||
|
||||
if newCenterY < ((smallView?.bounds.size.height ?? 0.0) / 2.0) ||
|
||||
newCenterY > self.bounds.size.height - (smallView?.bounds.size.height ?? 0.0) / 2.0 {
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.1) {
|
||||
smallView?.center = CGPoint(x: newCenterX, y: newCenterY)
|
||||
}
|
||||
panGesture.setTranslation(CGPointZero, in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// VideoFactory.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by vincepzhang on 2023/2/15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class VideoFactory {
|
||||
|
||||
static let instance = VideoFactory()
|
||||
var viewMap: [String: UserVideoEntity] = Dictionary()
|
||||
|
||||
func createVideoView(userId: String,frame: CGRect) -> VideoView {
|
||||
let videoView = VideoView(frame: frame)
|
||||
let userVideoEntity = UserVideoEntity()
|
||||
userVideoEntity.userId = userId
|
||||
userVideoEntity.videoView = videoView
|
||||
if TUICallState.instance.selfUser.value.id.value == userId {
|
||||
userVideoEntity.user = TUICallState.instance.selfUser.value
|
||||
} else {
|
||||
for user in TUICallState.instance.remoteUserList.value where user.id.value == userId {
|
||||
userVideoEntity.user = user
|
||||
}
|
||||
}
|
||||
viewMap[userId] = userVideoEntity
|
||||
return videoView
|
||||
}
|
||||
|
||||
func isExistVideoView(videoView: VideoView) -> Bool {
|
||||
let isExist = false
|
||||
for view in viewMap where view.value.videoView == videoView {
|
||||
return true
|
||||
}
|
||||
return isExist
|
||||
}
|
||||
}
|
||||
|
||||
class UserVideoEntity {
|
||||
var userId: String = ""
|
||||
var user: User = User()
|
||||
var videoView: VideoView = VideoView(frame: CGRect.zero)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// VideoView.swift
|
||||
// TUICallKit
|
||||
//
|
||||
// Created by : on 2023/2/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import TUICallEngine
|
||||
|
||||
@objc protocol VideoViewDelegate: NSObjectProtocol {
|
||||
@objc optional func tapGestureAction(tapGesture: UITapGestureRecognizer)
|
||||
@objc optional func panGestureAction(panGesture: UIPanGestureRecognizer)
|
||||
}
|
||||
|
||||
class VideoView: TUIVideoView {
|
||||
weak var delegate: VideoViewDelegate?
|
||||
|
||||
let volumeProgress: UIProgressView = {
|
||||
let volumeProgress = UIProgressView(progressViewStyle: .default)
|
||||
volumeProgress.backgroundColor = UIColor.clear
|
||||
return volumeProgress
|
||||
}()
|
||||
|
||||
let gestureView = UIView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
backgroundColor = UIColor.t_colorWithHexString(color: "#55534F")
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: UI Specification Processing
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if isViewReady { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(gestureView)
|
||||
addSubview(volumeProgress)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
|
||||
gestureView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(self)
|
||||
}
|
||||
|
||||
volumeProgress.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalTo(self)
|
||||
make.height.equalTo(4)
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
gestureView.backgroundColor = UIColor.clear
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGesture(tapGesture: )))
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(panGesture(panGesture: )))
|
||||
gestureView.addGestureRecognizer(tap)
|
||||
pan.require(toFail: tap)
|
||||
gestureView.addGestureRecognizer(pan)
|
||||
}
|
||||
|
||||
// MARK: Gesture Action
|
||||
@objc func tapGesture(tapGesture: UITapGestureRecognizer) {
|
||||
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
|
||||
self.delegate?.tapGestureAction?(tapGesture: tapGesture)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(panGesture: UIPanGestureRecognizer) {
|
||||
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
|
||||
self.delegate?.panGestureAction?(panGesture: panGesture)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user