LNVoiceCallPanel.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. //
  2. // LNVoiceCallPanel.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/2/1.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. class LNVoiceCallPanel: LNPopupView {
  11. private let background = UIImageView()
  12. private let avatar = UIImageView()
  13. private let nameLabel = UILabel()
  14. private let orderView = UIView()
  15. private let gameIc = UIImageView()
  16. private let orderStateLabel = UILabel()
  17. private let orderTimeLabel = UILabel()
  18. private let gameNameLabel = UILabel()
  19. private let gameCountLabel = UILabel()
  20. private let stateLabel = UILabel()
  21. private let onCallView = UIView()
  22. private let callOutView = UIView()
  23. private let callingView = UIView()
  24. private let durationLabel = UILabel()
  25. private let muteButton = UIButton()
  26. private let speakerButton = UIButton()
  27. private var timer: Timer?
  28. override init(frame: CGRect) {
  29. super.init(frame: frame)
  30. setupViews()
  31. LNEventDeliver.addObserver(self)
  32. }
  33. func toCallOut(uid: String) {
  34. callOutView.isHidden = false
  35. reloadUserInfo(uid: uid)
  36. }
  37. func onCallIn(uid: String) {
  38. onCallView.isHidden = false
  39. reloadUserInfo(uid: uid)
  40. }
  41. func resume() {
  42. guard let callInfo = LNIMManager.shared.curCallInfo else { return }
  43. if callInfo.beginTime > 0 {
  44. updateCallDuration()
  45. callingView.isHidden = false
  46. } else if callInfo.isInCome {
  47. onCallView.isHidden = false
  48. } else {
  49. callOutView.isHidden = false
  50. }
  51. reloadUserInfo(uid: callInfo.uid)
  52. }
  53. required init?(coder: NSCoder) {
  54. fatalError("init(coder:) has not been implemented")
  55. }
  56. }
  57. extension LNVoiceCallPanel {
  58. private func reloadUserInfo(uid: String) {
  59. LNProfileManager.shared.getUserProfile(uid: uid) { [weak self] info in
  60. guard let self else { return }
  61. guard let info else { return }
  62. background.sd_setImage(with: URL(string: info.avatar))
  63. avatar.sd_setImage(with: URL(string: info.avatar))
  64. nameLabel.text = info.nickname
  65. }
  66. }
  67. private func updateCallDuration() {
  68. guard let callInfo = LNIMManager.shared.curCallInfo else { return }
  69. let duration = curTime - callInfo.beginTime
  70. durationLabel.text = duration.timeCountDisplay
  71. }
  72. private func startTimer() {
  73. stopTimer()
  74. let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
  75. guard let self else { return }
  76. guard LNIMManager.shared.curCallInfo != nil else {
  77. stopTimer()
  78. return
  79. }
  80. updateCallDuration()
  81. }
  82. RunLoop.main.add(timer, forMode: .common)
  83. self.timer = timer
  84. }
  85. private func stopTimer() {
  86. timer?.invalidate()
  87. timer = nil
  88. }
  89. }
  90. extension LNVoiceCallPanel: LNIMManagerNotify {
  91. func onVoiceCallBegin() {
  92. onCallView.isHidden = true
  93. callOutView.isHidden = true
  94. callingView.isHidden = false
  95. updateCallDuration()
  96. startTimer()
  97. }
  98. func onVoiceCallEnd() {
  99. dismiss()
  100. stopTimer()
  101. }
  102. func onVoiceCallInfoChanged() {
  103. guard let callInfo = LNIMManager.shared.curCallInfo else { return }
  104. muteButton.setImage(callInfo.isMute ? .icCallMute : .icCallUnmute, for: .normal)
  105. speakerButton.setImage(callInfo.isSpeaker ? .icCallSpeakerPhone : .icCallSpeakerEarpiece, for: .normal)
  106. }
  107. }
  108. extension LNVoiceCallPanel {
  109. private func setupViews() {
  110. containerHeight = .percent(1.0)
  111. let background = buildBackground()
  112. container.addSubview(background)
  113. background.snp.makeConstraints { make in
  114. make.edges.equalToSuperview()
  115. }
  116. let navBar = buildNavBar()
  117. container.addSubview(navBar)
  118. navBar.snp.makeConstraints { make in
  119. make.horizontalEdges.equalToSuperview()
  120. make.top.equalToSuperview()
  121. }
  122. let infoView = buildInfoView()
  123. container.addSubview(infoView)
  124. infoView.snp.makeConstraints { make in
  125. make.horizontalEdges.equalToSuperview().inset(16)
  126. make.top.greaterThanOrEqualTo(navBar.snp.bottom).offset(16)
  127. make.bottom.equalTo(container.snp.centerY).offset(-30).priority(.medium)
  128. }
  129. stateLabel.font = .body_xl
  130. stateLabel.textColor = .text_2
  131. container.addSubview(stateLabel)
  132. stateLabel.snp.makeConstraints { make in
  133. make.centerX.equalToSuperview()
  134. make.top.equalTo(infoView.snp.bottom)
  135. }
  136. let onCallView = buildOnCallView()
  137. container.addSubview(onCallView)
  138. onCallView.snp.makeConstraints { make in
  139. make.horizontalEdges.equalToSuperview().inset(48)
  140. make.bottom.equalToSuperview().offset(-100)
  141. }
  142. let callOutView = buildCallOutView()
  143. container.addSubview(callOutView)
  144. callOutView.snp.makeConstraints { make in
  145. make.horizontalEdges.equalToSuperview().inset(48)
  146. make.bottom.equalToSuperview().offset(-100)
  147. }
  148. let callingView = buildCallingView()
  149. container.addSubview(callingView)
  150. callingView.snp.makeConstraints { make in
  151. make.horizontalEdges.equalToSuperview().inset(48)
  152. make.bottom.equalToSuperview().offset(-100)
  153. }
  154. }
  155. private func buildBackground() -> UIView {
  156. background.backgroundColor = .lightGray
  157. background.contentMode = .scaleAspectFill
  158. let blurEffect = UIBlurEffect(style: .light)
  159. let blurView = UIVisualEffectView(effect: blurEffect)
  160. background.addSubview(blurView)
  161. blurView.snp.makeConstraints { make in
  162. make.edges.equalToSuperview()
  163. }
  164. // 可选:添加半透明遮罩,增强模糊层次感(毛玻璃常用搭配)
  165. let maskView = UIView(frame: blurView.bounds)
  166. maskView.backgroundColor = UIColor.black.withAlphaComponent(0.3) // 0.1~0.3为宜
  167. blurView.contentView.addSubview(maskView)
  168. maskView.snp.makeConstraints { make in
  169. make.edges.equalToSuperview()
  170. }
  171. return background
  172. }
  173. private func buildNavBar() -> UIView {
  174. let navBar = LNFakeNaviBar()
  175. let minButton = UIButton()
  176. minButton.setImage(.icCallMin, for: .normal)
  177. minButton.addAction(UIAction(handler: { [weak self] _ in
  178. guard let self else { return }
  179. dismiss()
  180. let floatingView = LNVoiceCallFloatingView()
  181. floatingView.show()
  182. }), for: .touchUpInside)
  183. navBar.actionView.addSubview(minButton)
  184. minButton.snp.makeConstraints { make in
  185. make.leading.equalToSuperview().offset(16)
  186. make.centerY.equalToSuperview()
  187. make.width.height.equalTo(38)
  188. }
  189. return navBar
  190. }
  191. private func buildInfoView() -> UIView {
  192. let container = UIView()
  193. let stackView = UIStackView()
  194. stackView.axis = .vertical
  195. stackView.alignment = .center
  196. stackView.spacing = 4
  197. container.addSubview(stackView)
  198. stackView.snp.makeConstraints { make in
  199. make.edges.equalToSuperview()
  200. }
  201. avatar.layer.cornerRadius = 75
  202. avatar.clipsToBounds = true
  203. avatar.snp.makeConstraints { make in
  204. make.width.height.equalTo(150)
  205. }
  206. stackView.addArrangedSubview(avatar)
  207. nameLabel.font = .heading_h1
  208. nameLabel.textColor = .text_1
  209. stackView.addArrangedSubview(nameLabel)
  210. let orderView = buildOrderView()
  211. stackView.addArrangedSubview(orderView)
  212. orderView.snp.makeConstraints { make in
  213. make.width.equalToSuperview()
  214. }
  215. return container
  216. }
  217. private func buildOrderView() -> UIView {
  218. orderView.backgroundColor = .fill.withAlphaComponent(0.5)
  219. orderView.layer.cornerRadius = 12
  220. orderView.isHidden = true
  221. orderView.addSubview(gameIc)
  222. gameIc.snp.makeConstraints { make in
  223. make.top.equalToSuperview()
  224. make.leading.equalToSuperview().offset(10)
  225. make.width.height.equalTo(50)
  226. }
  227. let infoView = UIView()
  228. orderView.addSubview(infoView)
  229. infoView.snp.makeConstraints { make in
  230. make.centerY.equalToSuperview()
  231. make.leading.equalTo(gameIc.snp.trailing).offset(2)
  232. make.trailing.equalToSuperview().offset(-10)
  233. }
  234. orderStateLabel.font = .heading_h4
  235. orderStateLabel.textColor = .text_5
  236. infoView.addSubview(orderStateLabel)
  237. orderStateLabel.snp.makeConstraints { make in
  238. make.horizontalEdges.equalToSuperview()
  239. make.top.equalToSuperview()
  240. }
  241. orderTimeLabel.font = .body_xs
  242. orderTimeLabel.textColor = .text_4
  243. infoView.addSubview(orderTimeLabel)
  244. orderTimeLabel.snp.makeConstraints { make in
  245. make.horizontalEdges.equalToSuperview()
  246. make.bottom.equalToSuperview()
  247. make.top.equalTo(orderStateLabel.snp.bottom).offset(2)
  248. }
  249. let line = UIView()
  250. line.backgroundColor = .fill_2
  251. orderView.addSubview(line)
  252. line.snp.makeConstraints { make in
  253. make.horizontalEdges.equalToSuperview().inset(12)
  254. make.height.equalTo(0.5)
  255. make.bottom.equalTo(gameIc)
  256. }
  257. let gameInfo = UIView()
  258. orderView.addSubview(gameInfo)
  259. gameInfo.snp.makeConstraints { make in
  260. make.horizontalEdges.equalToSuperview()
  261. make.top.equalTo(gameIc.snp.bottom)
  262. make.bottom.equalToSuperview()
  263. make.height.equalTo(30)
  264. }
  265. gameNameLabel.font = .body_s
  266. gameNameLabel.textColor = .text_4
  267. gameInfo.addSubview(gameNameLabel)
  268. gameNameLabel.snp.makeConstraints { make in
  269. make.centerY.equalToSuperview()
  270. make.leading.equalToSuperview().offset(16)
  271. }
  272. gameCountLabel.font = .body_s
  273. gameCountLabel.textColor = .text_4
  274. gameCountLabel.setContentHuggingPriority(.required, for: .horizontal)
  275. gameCountLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
  276. gameInfo.addSubview(gameCountLabel)
  277. gameCountLabel.snp.makeConstraints { make in
  278. make.centerY.equalToSuperview()
  279. make.trailing.equalToSuperview().offset(-16)
  280. make.leading.greaterThanOrEqualTo(gameNameLabel.snp.trailing).offset(16)
  281. }
  282. return orderView
  283. }
  284. private func buildOnCallView() -> UIView {
  285. onCallView.isHidden = true
  286. let stackView = UIStackView()
  287. stackView.distribution = .equalSpacing
  288. onCallView.addSubview(stackView)
  289. stackView.snp.makeConstraints { make in
  290. make.edges.equalToSuperview()
  291. }
  292. let rejectButton = UIButton()
  293. rejectButton.setImage(.icCallDecline, for: .normal)
  294. rejectButton.addAction(UIAction(handler: { _ in
  295. LNIMManager.shared.rejectVoiceCall()
  296. }), for: .touchUpInside)
  297. stackView.addArrangedSubview(rejectButton)
  298. let acceptButton = UIButton()
  299. acceptButton.setImage(.icCallAccept, for: .normal)
  300. acceptButton.addAction(UIAction(handler: { _ in
  301. LNIMManager.shared.acceptVoiceCall()
  302. }), for: .touchUpInside)
  303. stackView.addArrangedSubview(acceptButton)
  304. return onCallView
  305. }
  306. private func buildCallOutView() -> UIView {
  307. callOutView.isHidden = true
  308. let cancelButton = UIButton()
  309. cancelButton.setImage(.icCallDecline, for: .normal)
  310. cancelButton.addAction(UIAction(handler: { _ in
  311. LNIMManager.shared.hangupVoiceCall()
  312. }), for: .touchUpInside)
  313. callOutView.addSubview(cancelButton)
  314. cancelButton.snp.makeConstraints { make in
  315. make.verticalEdges.equalToSuperview()
  316. make.centerX.equalToSuperview()
  317. }
  318. return callOutView
  319. }
  320. private func buildCallingView() -> UIView {
  321. callingView.isHidden = true
  322. let stackView = UIStackView()
  323. stackView.alignment = .center
  324. stackView.distribution = .equalSpacing
  325. callingView.addSubview(stackView)
  326. stackView.snp.makeConstraints { make in
  327. make.horizontalEdges.equalToSuperview()
  328. make.bottom.equalToSuperview()
  329. }
  330. muteButton.setImage(.icCallUnmute, for: .normal)
  331. muteButton.addAction(UIAction(handler: { _ in
  332. LNIMManager.shared.switchVoiceCallMicrophone()
  333. }), for: .touchUpInside)
  334. stackView.addArrangedSubview(muteButton)
  335. let hangupButton = UIButton()
  336. hangupButton.setImage(.icCallDecline, for: .normal)
  337. hangupButton.addAction(UIAction(handler: { _ in
  338. LNIMManager.shared.hangupVoiceCall()
  339. }), for: .touchUpInside)
  340. stackView.addArrangedSubview(hangupButton)
  341. speakerButton.setImage(.icCallSpeakerEarpiece, for: .normal)
  342. speakerButton.addAction(UIAction(handler: { _ in
  343. LNIMManager.shared.switchVoiceCallSpeakerType()
  344. }), for: .touchUpInside)
  345. stackView.addArrangedSubview(speakerButton)
  346. durationLabel.font = .body_xl
  347. durationLabel.textColor = .text_1
  348. callingView.addSubview(durationLabel)
  349. durationLabel.snp.makeConstraints { make in
  350. make.centerX.equalToSuperview()
  351. make.top.equalToSuperview()
  352. make.bottom.equalTo(stackView.snp.top).offset(-14)
  353. }
  354. return callingView
  355. }
  356. }
  357. #if DEBUG
  358. import SwiftUI
  359. struct LNVoiceCallPanelPreview: UIViewRepresentable {
  360. func makeUIView(context: Context) -> some UIView {
  361. let container = UIView()
  362. container.backgroundColor = .lightGray
  363. let view = LNVoiceCallPanel()
  364. view.popup()
  365. return container
  366. }
  367. func updateUIView(_ uiView: UIViewType, context: Context) { }
  368. }
  369. #Preview(body: {
  370. LNVoiceCallPanelPreview()
  371. })
  372. #endif