// // LNAudioCallFloatingView.swift // Gami // // Created by OneeChan on 2026/2/3. // import Foundation import UIKit import SnapKit class LNAudioCallFloatingView: UIView { private let size: CGFloat = 67 private let stackView = UIStackView() private let stateIc = UIImageView() private let durationLabel = UILabel() private var timer: Timer? override init(frame: CGRect) { super.init(frame: frame) setupViews() setupGesture() LNEventDeliver.addObserver(self) if let callInfo = LNIMManager.shared.curCallInfo, callInfo.beginTime > 0 { updateCallDuration() startTimer() } } func show() { guard let window = UIView.appKeyWindow else { return } window.addSubview(self) frame = .init(x: window.bounds.width - 16 - size, y: window.bounds.height - 100.0 - size, width: size, height: size) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNAudioCallFloatingView { func dismiss() { removeFromSuperview() } private func updateCallDuration() { guard let callInfo = LNIMManager.shared.curCallInfo else { return } let duration = curTime - callInfo.beginTime durationLabel.text = duration.timeCountDisplay durationLabel.isHidden = false } 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 LNAudioCallFloatingView { @objc private func handlePan(_ ges: UIPanGestureRecognizer) { let location = ges.location(in: superview) switch ges.state { case .began: break case .changed: center = location break default: updatePosition(animated: true) break } } } extension LNAudioCallFloatingView { private func updatePosition(animated: Bool) { guard let superview else { return } let movement = { [weak self] in guard let self else { return } let y = center.y.bounded(min: 160, max: superview.bounds.height - 160) // if center.x > superview.bounds.width * 0.5 { center = .init(x: superview.bounds.width - bounds.width * 0.5 - 16, y: y) // } else { // center = .init(x: bounds.width * 0.5 + 16, y: y) // } } if animated { UIView.animate(withDuration: 0.25, animations: movement) } else { movement() } } private func setupViews() { layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor layer.shadowOffset = .init(width: 0, height: 4) layer.shadowRadius = 6 layer.shadowOpacity = 0.5 layer.masksToBounds = false backgroundColor = .fill layer.cornerRadius = 12 stackView.axis = .vertical stackView.spacing = 8 stackView.alignment = .center addSubview(stackView) stackView.snp.makeConstraints { make in make.center.equalToSuperview() } stateIc.image = .icCalling stackView.addArrangedSubview(stateIc) durationLabel.isHidden = true durationLabel.font = .heading_h5 durationLabel.textColor = .text_6 stackView.addArrangedSubview(durationLabel) } private func setupGesture() { let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) addGestureRecognizer(pan) onTap { let panel = LNAudioCallPanel() panel.resume() panel.popup() } } } extension LNAudioCallFloatingView: LNIMManagerNotify { func onVoiceCallEnd() { dismiss() } func onVoiceCallBegin() { updateCallDuration() startTimer() } }