// // VoiceRecordView.swift // QuickLocation // // Created by 八条 on 2026/6/6. // import UIKit import Lottie final class VoiceRecordView: UIView { enum State { case recording case canceling } var state: State = .recording { didSet { updateUI() } } private func updateUI() { switch state { case .recording: tipsLab.text = "松开发送" startRotating() case .canceling: tipsLab.text = "松开取消" } } lazy var bgView: UIView = { let v = UIView() v.backgroundColor = .clear v.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.1).cgColor v.layer.shadowOffset = CGSize(width: 0, height: 0) v.layer.shadowOpacity = 1 v.layer.shadowRadius = 8 return v }() lazy var topContainer: UIView = { let v = UIView() v.backgroundColor = .white return v }() lazy var keyboardBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/keyboard"), for: .normal) return btn }() lazy var titleView: UIView = { let v = UIView() v.layer.shadowColor = UIColor(red: 0.06, green: 0.16, blue: 0.27, alpha: 0.1).cgColor v.layer.shadowOffset = CGSize(width: 0, height: 0) v.layer.shadowOpacity = 1 v.layer.shadowRadius = 9 let cornerView = UIView() cornerView.backgroundColor = .white cornerView.cornerRadius = 19 let label = UILabel() label.text = "正在录入语音..." label.font = .systemFont(ofSize: 12) label.textColor = ThemeManager.shared.color.subTitleColor label.textAlignment = .center v.addSubview(cornerView) v.addSubview(label) cornerView.layoutChain.edges() label.layoutChain.centerX().centerY() return v }() lazy var emojiBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/emoji_off"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 0) return btn }() lazy var addBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/add_off"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 15) return btn }() lazy var containerView: UIView = { let view = UIView() view.backgroundColor = .white return view }() lazy var tipsLab: UILabel = { let label = UILabel() label.text = " " label.font = .systemFont(ofSize: 12, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var speakingImgView: UIImageView = { let view = UIImageView(image: UIImage(named: "IM/speaking")) view.isHidden = true return view }() lazy var speakBtn: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#E7F7FF") view.cornerRadius = 48 let icon = UIImageView(image: UIImage(named: "IM/speak")) view.addSubview(icon) icon.layoutChain .centerX() .centerY() return view }() lazy var cancelLab: UILabel = { let label = UILabel() label.text = "左滑取消" label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = ThemeManager.shared.color.contentColor return label }() lazy var cancelBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/cancel_off"), for: .normal) btn.setImage(UIImage(named: "IM/cancel"), for: .selected) btn.isUserInteractionEnabled = false return btn }() // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear addSubview(bgView) bgView.addSubview(topContainer) topContainer.addSubview(keyboardBtn) topContainer.addSubview(titleView) topContainer.addSubview(emojiBtn) topContainer.addSubview(addBtn) bgView.addSubview(containerView) containerView.addSubview(tipsLab) containerView.addSubview(speakBtn) containerView.addSubview(speakingImgView) containerView.addSubview(cancelLab) containerView.addSubview(cancelBtn) bgView.layoutChain.edges() topContainer.layoutChain .edges(excludingEdge: .bottom) .height(58) keyboardBtn.layoutChain .left(22) .centerY() .width(36) .height(36) addBtn.layoutChain .right(22) .centerY() .width(24) .height(24) emojiBtn.layoutChain .rightToLeftOfView(addBtn, offset: -8) .centerY() .width(24) .height(24) titleView.layoutChain .top(10) .leftToRightOfView(keyboardBtn, offset: 7) .rightToLeftOfView(emojiBtn, offset: -12) .bottom(10) containerView.layoutChain .topToBottomOfView(topContainer, offset: 0) .edges(excludingEdge: .top) tipsLab.layoutChain .top(18) .centerX() speakBtn.layoutChain .height(96) .widthToHeight(1) .centerY() .centerX() speakingImgView.layoutChain .topToView(speakBtn, offset: -6) .leftToView(speakBtn, offset: -6) .rightToView(speakBtn, offset: 6) .bottomToView(speakBtn, offset: 6) cancelBtn.layoutChain .rightToLeftOfView(speakBtn, offset: -47) .centerY(speakBtn) .width(41) .heightToWidth(1) cancelLab.layoutChain .bottomToTopOfView(cancelBtn, offset: -7) .centerX(cancelBtn) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() topContainer.setNeedsLayout() topContainer.layoutIfNeeded() topContainer.setCornerRadius(corners: [.topRight, .topLeft], withCornerRadii: CGSize(width: 20, height: 20)) } } extension VoiceRecordView { func startRotating() { guard speakingImgView.isHidden == true else { return } speakingImgView.isHidden = false // 避免重复添加动画 speakingImgView.layer.removeAnimation(forKey: "rotationAnimation") let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotationAnimation.fromValue = 0 rotationAnimation.toValue = Double.pi * 2 // 360° rotationAnimation.duration = 1.0 rotationAnimation.repeatCount = .infinity // 无限循环 rotationAnimation.isRemovedOnCompletion = false rotationAnimation.fillMode = .forwards speakingImgView.layer.add(rotationAnimation, forKey: "rotationAnimation") } func stopRotating() { tipsLab.text = " " speakingImgView.isHidden = true speakingImgView.layer.removeAnimation(forKey: "rotationAnimation") } }