| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- //
- // LNVoiceCallPanel.swift
- // Gami
- //
- // Created by OneeChan on 2026/2/1.
- //
- import Foundation
- import UIKit
- import SnapKit
- class LNVoiceCallPanel: LNPopupView {
- private let background = UIImageView()
-
- private let avatar = UIImageView()
- private let nameLabel = UILabel()
-
- private let orderView = UIView()
- private let gameIc = UIImageView()
- private let orderStateLabel = UILabel()
- private let orderTimeLabel = UILabel()
- private let gameNameLabel = UILabel()
- private let gameCountLabel = UILabel()
-
- private let stateLabel = UILabel()
-
- private let onCallView = UIView()
- private let callOutView = UIView()
-
- private let callingView = UIView()
- private let durationLabel = UILabel()
- private let muteButton = UIButton()
- private let speakerButton = UIButton()
-
- private var timer: Timer?
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- setupViews()
- LNEventDeliver.addObserver(self)
- }
-
- func toCallOut(uid: String) {
- callOutView.isHidden = false
- reloadUserInfo(uid: uid)
- }
-
- func onCallIn(uid: String) {
- onCallView.isHidden = false
- reloadUserInfo(uid: uid)
- }
-
- func resume() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- if callInfo.beginTime > 0 {
- updateCallDuration()
- callingView.isHidden = false
- } else if callInfo.isInCome {
- onCallView.isHidden = false
- } else {
- callOutView.isHidden = false
- }
-
- reloadUserInfo(uid: callInfo.uid)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNVoiceCallPanel {
- private func reloadUserInfo(uid: String) {
- LNProfileManager.shared.getUserProfile(uid: uid) { [weak self] info in
- guard let self else { return }
- guard let info else { return }
- background.sd_setImage(with: URL(string: info.avatar))
- avatar.sd_setImage(with: URL(string: info.avatar))
- nameLabel.text = info.nickname
- }
- }
-
- private func updateCallDuration() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- let duration = curTime - callInfo.beginTime
- durationLabel.text = duration.timeCountDisplay
- }
-
- private func startTimer() {
- stopTimer()
- let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
- guard let self else { return }
- guard LNIMManager.shared.curCallInfo != nil else {
- stopTimer()
- return
- }
-
- updateCallDuration()
- }
- RunLoop.main.add(timer, forMode: .common)
- self.timer = timer
- }
-
- private func stopTimer() {
- timer?.invalidate()
- timer = nil
- }
- }
- extension LNVoiceCallPanel: LNIMManagerNotify {
- func onVoiceCallBegin() {
- onCallView.isHidden = true
- callOutView.isHidden = true
- callingView.isHidden = false
-
- updateCallDuration()
- startTimer()
- }
-
- func onVoiceCallEnd() {
- dismiss()
- stopTimer()
- }
-
- func onVoiceCallInfoChanged() {
- guard let callInfo = LNIMManager.shared.curCallInfo else { return }
- muteButton.setImage(callInfo.isMute ? .icCallMute : .icCallUnmute, for: .normal)
- speakerButton.setImage(callInfo.isSpeaker ? .icCallSpeakerPhone : .icCallSpeakerEarpiece, for: .normal)
- }
- }
- extension LNVoiceCallPanel {
- private func setupViews() {
- containerHeight = .percent(1.0)
-
- let background = buildBackground()
- container.addSubview(background)
- background.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let navBar = buildNavBar()
- container.addSubview(navBar)
- navBar.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- let infoView = buildInfoView()
- container.addSubview(infoView)
- infoView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.greaterThanOrEqualTo(navBar.snp.bottom).offset(16)
- make.bottom.equalTo(container.snp.centerY).offset(-30).priority(.medium)
- }
-
- stateLabel.font = .body_xl
- stateLabel.textColor = .text_2
- container.addSubview(stateLabel)
- stateLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalTo(infoView.snp.bottom)
- }
-
- let onCallView = buildOnCallView()
- container.addSubview(onCallView)
- onCallView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
-
- let callOutView = buildCallOutView()
- container.addSubview(callOutView)
- callOutView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
-
- let callingView = buildCallingView()
- container.addSubview(callingView)
- callingView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(48)
- make.bottom.equalToSuperview().offset(-100)
- }
- }
-
- private func buildBackground() -> UIView {
- background.backgroundColor = .lightGray
- background.contentMode = .scaleAspectFill
-
- let blurEffect = UIBlurEffect(style: .light)
- let blurView = UIVisualEffectView(effect: blurEffect)
- background.addSubview(blurView)
- blurView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- // 可选:添加半透明遮罩,增强模糊层次感(毛玻璃常用搭配)
- let maskView = UIView(frame: blurView.bounds)
- maskView.backgroundColor = UIColor.black.withAlphaComponent(0.3) // 0.1~0.3为宜
- blurView.contentView.addSubview(maskView)
- maskView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- return background
- }
-
- private func buildNavBar() -> UIView {
- let navBar = LNFakeNaviBar()
-
- let minButton = UIButton()
- minButton.setImage(.icCallMin, for: .normal)
- minButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- dismiss()
- let floatingView = LNVoiceCallFloatingView()
- floatingView.show()
- }), for: .touchUpInside)
- navBar.actionView.addSubview(minButton)
- minButton.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(16)
- make.centerY.equalToSuperview()
- make.width.height.equalTo(38)
- }
-
- return navBar
- }
-
- private func buildInfoView() -> UIView {
- let container = UIView()
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.alignment = .center
- stackView.spacing = 4
- container.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- avatar.layer.cornerRadius = 75
- avatar.clipsToBounds = true
- avatar.snp.makeConstraints { make in
- make.width.height.equalTo(150)
- }
- stackView.addArrangedSubview(avatar)
-
- nameLabel.font = .heading_h1
- nameLabel.textColor = .text_1
- stackView.addArrangedSubview(nameLabel)
-
- let orderView = buildOrderView()
- stackView.addArrangedSubview(orderView)
- orderView.snp.makeConstraints { make in
- make.width.equalToSuperview()
- }
-
- return container
- }
-
- private func buildOrderView() -> UIView {
- orderView.backgroundColor = .fill.withAlphaComponent(0.5)
- orderView.layer.cornerRadius = 12
- orderView.isHidden = true
-
- orderView.addSubview(gameIc)
- gameIc.snp.makeConstraints { make in
- make.top.equalToSuperview()
- make.leading.equalToSuperview().offset(10)
- make.width.height.equalTo(50)
- }
-
- let infoView = UIView()
- orderView.addSubview(infoView)
- infoView.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalTo(gameIc.snp.trailing).offset(2)
- make.trailing.equalToSuperview().offset(-10)
- }
-
- orderStateLabel.font = .heading_h4
- orderStateLabel.textColor = .text_5
- infoView.addSubview(orderStateLabel)
- orderStateLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- orderTimeLabel.font = .body_xs
- orderTimeLabel.textColor = .text_4
- infoView.addSubview(orderTimeLabel)
- orderTimeLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- make.top.equalTo(orderStateLabel.snp.bottom).offset(2)
- }
-
- let line = UIView()
- line.backgroundColor = .fill_2
- orderView.addSubview(line)
- line.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(12)
- make.height.equalTo(0.5)
- make.bottom.equalTo(gameIc)
- }
-
- let gameInfo = UIView()
- orderView.addSubview(gameInfo)
- gameInfo.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalTo(gameIc.snp.bottom)
- make.bottom.equalToSuperview()
- make.height.equalTo(30)
- }
-
- gameNameLabel.font = .body_s
- gameNameLabel.textColor = .text_4
- gameInfo.addSubview(gameNameLabel)
- gameNameLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(16)
- }
-
- gameCountLabel.font = .body_s
- gameCountLabel.textColor = .text_4
- gameCountLabel.setContentHuggingPriority(.required, for: .horizontal)
- gameCountLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
- gameInfo.addSubview(gameCountLabel)
- gameCountLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-16)
- make.leading.greaterThanOrEqualTo(gameNameLabel.snp.trailing).offset(16)
- }
-
- return orderView
- }
-
- private func buildOnCallView() -> UIView {
- onCallView.isHidden = true
-
- let stackView = UIStackView()
- stackView.distribution = .equalSpacing
- onCallView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let rejectButton = UIButton()
- rejectButton.setImage(.icCallDecline, for: .normal)
- rejectButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.rejectVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(rejectButton)
-
- let acceptButton = UIButton()
- acceptButton.setImage(.icCallAccept, for: .normal)
- acceptButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.acceptVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(acceptButton)
-
- return onCallView
- }
-
- private func buildCallOutView() -> UIView {
- callOutView.isHidden = true
-
- let cancelButton = UIButton()
- cancelButton.setImage(.icCallDecline, for: .normal)
- cancelButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.hangupVoiceCall()
- }), for: .touchUpInside)
- callOutView.addSubview(cancelButton)
- cancelButton.snp.makeConstraints { make in
- make.verticalEdges.equalToSuperview()
- make.centerX.equalToSuperview()
- }
-
- return callOutView
- }
-
- private func buildCallingView() -> UIView {
- callingView.isHidden = true
-
- let stackView = UIStackView()
- stackView.alignment = .center
- stackView.distribution = .equalSpacing
- callingView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalToSuperview()
- }
-
- muteButton.setImage(.icCallUnmute, for: .normal)
- muteButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.switchVoiceCallMicrophone()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(muteButton)
-
- let hangupButton = UIButton()
- hangupButton.setImage(.icCallDecline, for: .normal)
- hangupButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.hangupVoiceCall()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(hangupButton)
-
- speakerButton.setImage(.icCallSpeakerEarpiece, for: .normal)
- speakerButton.addAction(UIAction(handler: { _ in
- LNIMManager.shared.switchVoiceCallSpeakerType()
- }), for: .touchUpInside)
- stackView.addArrangedSubview(speakerButton)
-
- durationLabel.font = .body_xl
- durationLabel.textColor = .text_1
- callingView.addSubview(durationLabel)
- durationLabel.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.top.equalToSuperview()
- make.bottom.equalTo(stackView.snp.top).offset(-14)
- }
-
- return callingView
- }
- }
- #if DEBUG
- import SwiftUI
- struct LNVoiceCallPanelPreview: UIViewRepresentable {
- func makeUIView(context: Context) -> some UIView {
- let container = UIView()
- container.backgroundColor = .lightGray
-
- let view = LNVoiceCallPanel()
- view.popup()
-
- return container
- }
-
- func updateUIView(_ uiView: UIViewType, context: Context) { }
- }
- #Preview(body: {
- LNVoiceCallPanelPreview()
- })
- #endif
|