// // GroupChatView.swift // QuickLocation // // Created by 八条 on 2026/6/4. // import UIKit import RxSwift import RxCocoa import Lottie // MARK: - Message Model struct ChatMessage { let id: String let isSelf: Bool let avatar: UIImage let senderName: String let content: String let timestamp: TimeInterval var showTime: Bool = false } class GroupChatView: UIView { var disposeBag = DisposeBag() /// For scrolling control from VC var scrollToBottom: (() -> Void)? /// bottomBar 底部约束 var bottomBarBottomConstraint: NSLayoutConstraint? // MARK: - Setup private func setupUI() { backgroundColor = UIColor(hexStr: "#F5FBFB") addSubview(navBgView) addSubview(navBarView) navBarView.addSubview(backBtn) navBarView.addSubview(groupAvatarView) navBarView.addSubview(groupNameLabel) navBarView.addSubview(onlineStatusLabel) navBarView.addSubview(rightIconsView) rightIconsView.addSubview(reviewBtn) rightIconsView.addSubview(memberBtn) rightIconsView.addSubview(settingBtn) addSubview(tableView) addSubview(bottomBar) bottomBar.addSubview(bottomBarCornerView) addSubview(emojiPanelView) emojiPanelView.addSubview(emojiCollectionView) emojiPanelView.addSubview(emojiPageControl) bottomBar.addSubview(voiceBtn) bottomBar.addSubview(textField) bottomBar.addSubview(emojiBtn) bottomBar.addSubview(addBtn) bottomBar.addSubview(sendBtn) navBgView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) backBtn.layoutChain .top(kStatusBarHeight + 12) .left(7) .width(24).height(24) groupAvatarView.layoutChain .centerY(backBtn) .leftToRightOfView(backBtn, offset: 5) .width(32).height(32) groupNameLabel.layoutChain .topToView(groupAvatarView) .leftToRightOfView(groupAvatarView, offset: 8) onlineStatusLabel.layoutChain .topToBottomOfView(groupNameLabel, offset: 2) .leftToView(groupNameLabel) rightIconsView.layoutChain .centerY(backBtn) .right(15) .height(24) reviewBtn.layoutChain .left().centerY() .width(24).height(24) memberBtn.layoutChain .leftToRightOfView(reviewBtn, offset: 20) .centerY() .width(24).height(24) settingBtn.layoutChain .leftToRightOfView(memberBtn, offset: 20) .centerY() .width(24).height(24) .right() tableView.layoutChain .topToBottomOfView(navBarView) .edgesHorzontal() .bottomToTopOfView(bottomBar) bottomBar.layoutChain .edgesHorzontal(15) .height(50) .bottom(kSafeBottomMargin + 20) bottomBarBottomConstraint = bottomBar.jh_constraint(.bottom, toAttribute: .bottom, otherView: bottomBar.superview, relation: .equal) bottomBarCornerView.layoutChain.edges() voiceBtn.layoutChain .left(13).centerY() .width(28).height(28) sendBtn.layoutChain .right(19).centerY() .width(76).height(34) addBtn.layoutChain .rightToLeftOfView(sendBtn, offset: -10) .centerY() .width(28).height(28) emojiBtn.layoutChain .rightToLeftOfView(addBtn, offset: -10) .centerY() .width(28).height(28) textField.layoutChain .leftToRightOfView(voiceBtn, offset: 10) .rightToLeftOfView(emojiBtn, offset: -10) .centerY() .height(36) emojiPanelView.layoutChain .edgesHorzontal() .bottom() .height(220) emojiCollectionView.layoutChain .edges(excludingEdge: .bottom) emojiPageControl.layoutChain .topToBottomOfView(emojiCollectionView) .centerX() .height(20) .bottom() } // MARK: - Nav lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill return iv }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 40) return btn }() lazy var groupAvatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 16 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") return iv }() lazy var groupNameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#333333") return label }() lazy var onlineStatusLabel: UILabel = { let label = UILabel() label.text = "今日活跃" label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor(hexStr: "#999999") return label }() lazy var rightIconsView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var reviewBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/review"), for: .normal) return btn }() lazy var memberBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/member"), for: .normal) return btn }() lazy var settingBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/setting"), for: .normal) return btn }() // MARK: - Message List lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = true tv.register(TextSendMsgCell.self) tv.register(TextReceivedMsgCell.self) tv.register(EmojiSendMsgCell.self) tv.register(EmojiReceivedMsgCell.self) tv.register(VoiceSendMsgCell.self) tv.register(VoiceReceivedMsgCell.self) tv.register(NotificationMsgCell.self) tv.rowHeight = UITableView.automaticDimension tv.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) return tv }() // MARK: - Bottom Bar lazy var bottomBar: UIView = { let view = UIView() view.backgroundColor = .clear view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor view.layer.shadowOffset = CGSize(width: 0, height: 0) view.layer.shadowOpacity = 1 view.layer.shadowRadius = 9 return view }() lazy var bottomBarCornerView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 25 return view }() lazy var voiceBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/voice"), for: .normal) return btn }() lazy var textField: UITextField = { let tf = UITextField() tf.font = .systemFont(ofSize: 14) tf.backgroundColor = .white tf.cornerRadius = 18 tf.placeholder = "输入消息..." tf.returnKeyType = .send tf.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 36)) tf.leftViewMode = .always return tf }() lazy var emojiBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/emoji"), for: .normal) return btn }() lazy var addBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "IM/add"), for: .normal) return btn }() lazy var sendBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("发送", for: .normal) btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) btn.backgroundColor = .white btn.borderWidth = 1 btn.borderColor = UIColor(hexStr: "#16B3FF") btn.cornerRadius = 17 return btn }() // MARK: - 表情面板 lazy var emojiPanelView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F5F6F8") v.isHidden = true return v }() private static let emojiCols = 4 private static let emojiRows = 3 private static let emojiPerPage = emojiCols * emojiRows // 12 lazy var emojiCollectionView: UICollectionView = { let layout = CollectionHFlowLayout() let hSpacing: CGFloat = (kScreenWidth - CGFloat(Self.emojiCols) * 50) / CGFloat(Self.emojiCols + 1) let vSpacing: CGFloat = (180 - CGFloat(Self.emojiRows) * 50) / CGFloat(Self.emojiRows + 1) layout.rows = Self.emojiRows layout.colums = Self.emojiCols layout.itemSize = CGSize(width: 50, height: 50) layout.hSpacing = hSpacing layout.vSpacing = vSpacing layout.sectionInset = UIEdgeInsets(top: vSpacing, left: hSpacing, bottom: vSpacing, right: hSpacing) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.isPagingEnabled = true cv.showsHorizontalScrollIndicator = false cv.register(EmojiPanelCell.self, forCellWithReuseIdentifier: "EmojiPanelCell") cv.delegate = self return cv }() lazy var emojiPageControl: UIPageControl = { let pc = UIPageControl() pc.numberOfPages = (UITableViewCell.emojiFileNames.count + Self.emojiPerPage - 1) / Self.emojiPerPage pc.currentPageIndicatorTintColor = UIColor(hexStr: "#16B3FF") pc.pageIndicatorTintColor = UIColor(hexStr: "#D0D0D0") return pc }() override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .clear setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - 发送的消息cell class TextSendMsgCell: UITableViewCell { func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar contentLabel.text = msg.content } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let bubbleView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#16B3FF") return v }() private let contentLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14) label.numberOfLines = 0 return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(bubbleView) bubbleView.addSubview(contentLabel) contentView.addSubview(timeLabel) contentView.addSubview(avatarView) timeLabel.layoutChain .top() .centerX() avatarView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .right(12) .width(30).height(30) bubbleView.layoutChain .topToBottomOfView(avatarView, offset: -15) .rightToView(avatarView, offset: -13) .left(60, relation: .greaterThanOrEqual) .width(100, relation: .greaterThanOrEqual) .height(30, relation: .greaterThanOrEqual) .bottom(10) contentLabel.layoutChain .edgesVertical(10) .edgesHorzontal(20) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() bubbleView.setNeedsLayout() bubbleView.layoutIfNeeded() bubbleView.setCornerRadius(corners: [.topLeft ,.bottomLeft, .bottomRight], withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2)) } } //MARK: - 收到的消息cell class TextReceivedMsgCell: UITableViewCell { func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar nameLabel.text = msg.senderName contentLabel.text = msg.content } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .regular) label.textColor = UIColor(hexStr: "#666666") return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let bubbleView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#CCEAFF") return v }() private let contentLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14) label.numberOfLines = 0 return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(timeLabel) contentView.addSubview(bubbleView) bubbleView.addSubview(contentLabel) contentView.addSubview(avatarView) contentView.addSubview(nameLabel) timeLabel.layoutChain .top() .centerX() bubbleView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .left(30) .right(60, relation: .greaterThanOrEqual) .width(100, relation: .greaterThanOrEqual) .height(30, relation: .greaterThanOrEqual) avatarView.layoutChain .topToBottomOfView(bubbleView, offset: -15) .left(12) .width(30).height(30) .bottom(10) nameLabel.layoutChain .leftToRightOfView(avatarView, offset: 5) .bottomToView(avatarView) contentLabel.layoutChain .edgesVertical(10) .edgesHorzontal(20) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() bubbleView.setNeedsLayout() bubbleView.layoutIfNeeded() bubbleView.setCornerRadius(corners: [.topLeft , .topRight, .bottomRight], withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2)) } } // MARK: - 通知消息cell final class NotificationMsgCell: UITableViewCell { private let contentLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center label.numberOfLines = 0 return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(contentLabel) contentLabel.layoutChain .edges(UIEdgeInsets(top: 10, left: 40, bottom: 10, right: 40)) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ text: String) { contentLabel.text = text } } // MARK: - 表情文件列表(共用) extension UITableViewCell { static var emojiFileNames: [String] = { let paths = Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil) return paths .compactMap { $0.components(separatedBy: "/").last } .filter { $0.hasPrefix("normal_") && $0.hasSuffix(".json") } .map { $0.replacingOccurrences(of: ".json", with: "") } .sorted() }() } // MARK: - 发送的表情消息 final class EmojiSendMsgCell: UITableViewCell { private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let lottieView: LottieAnimationView = { let v = LottieAnimationView() v.contentMode = .scaleAspectFit v.loopMode = .loop return v }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(timeLabel) contentView.addSubview(avatarView) contentView.addSubview(lottieView) timeLabel.layoutChain.top().centerX() avatarView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .right(12) .width(30).height(30) lottieView.layoutChain .topToBottomOfView(avatarView, offset: -15) .right(60) .width(60).height(60) .bottom(10) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar let index = Int(msg.content.replacingOccurrences(of: "js_emoji:", with: "")) ?? 0 if index < Self.emojiFileNames.count, let path = Bundle.main.path(forResource: Self.emojiFileNames[index], ofType: "json") { lottieView.animation = LottieAnimation.filepath(path) lottieView.play() } } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } } // MARK: - 收到的表情消息 final class EmojiReceivedMsgCell: UITableViewCell { private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .regular) label.textColor = UIColor(hexStr: "#666666") return label }() private let lottieView: LottieAnimationView = { let v = LottieAnimationView() v.contentMode = .scaleAspectFit v.loopMode = .loop return v }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(timeLabel) contentView.addSubview(avatarView) contentView.addSubview(nameLabel) contentView.addSubview(lottieView) timeLabel.layoutChain.top().centerX() lottieView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .left(60) .width(60).height(60) avatarView.layoutChain .topToBottomOfView(lottieView, offset: -15) .left(12) .width(30).height(30) .bottom(10) nameLabel.layoutChain .leftToRightOfView(avatarView, offset: 5) .bottomToView(avatarView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar nameLabel.text = msg.senderName let index = Int(msg.content.replacingOccurrences(of: "js_emoji:", with: "")) ?? 0 if index < Self.emojiFileNames.count, let path = Bundle.main.path(forResource: Self.emojiFileNames[index], ofType: "json") { lottieView.animation = LottieAnimation.filepath(path) lottieView.play() } } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } } // MARK: - UICollectionViewDelegate (page control) extension GroupChatView: UICollectionViewDelegate { func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { guard scrollView == emojiCollectionView else { return } let page = Int(scrollView.contentOffset.x / scrollView.bounds.width) emojiPageControl.currentPage = page } } // MARK: - EmojiPanelCell final class EmojiPanelCell: UICollectionViewCell { private static var animationCache: [String: LottieAnimation] = [:] static func preloadAnimations() { DispatchQueue.global(qos: .userInitiated).async { for name in UITableViewCell.emojiFileNames { guard animationCache[name] == nil, let path = Bundle.main.path(forResource: name, ofType: "json"), let anim = LottieAnimation.filepath(path) else { continue } animationCache[name] = anim } } } private let lottieView: LottieAnimationView = { let v = LottieAnimationView() v.contentMode = .scaleAspectFit v.loopMode = .loop return v }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(lottieView) lottieView.layoutChain.edges() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(path: String) { lottieView.stop() if let cached = Self.animationCache[path] { lottieView.animation = cached } else { lottieView.animation = LottieAnimation.filepath(path) } lottieView.currentProgress = 0 lottieView.play() } override func prepareForReuse() { super.prepareForReuse() lottieView.stop() lottieView.animation = nil } } // MARK: - 发送的语音消息 final class VoiceSendMsgCell: UITableViewCell { private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let bubbleView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#16B3FF") return v }() private let playIcon: UIImageView = { let iv = UIImageView(image: UIImage(named: "IM/video_send")) return iv }() private let durationLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14) label.textColor = .white return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(timeLabel) contentView.addSubview(bubbleView) bubbleView.addSubview(playIcon) bubbleView.addSubview(durationLabel) contentView.addSubview(avatarView) timeLabel.layoutChain.top().centerX() avatarView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .right(12).width(30).height(30) bubbleView.layoutChain .topToView(avatarView, offset: -15) .rightToView(avatarView, offset: -13) .width(105).height(39).bottom(10) playIcon.layoutChain .right(36) .centerY() .width(11).height(15) durationLabel.layoutChain .leftToRightOfView(playIcon, offset: 8) .centerY() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar let dur = msg.content.int / 1000 durationLabel.text = dur > 0 ? "\(dur)''" : "" } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } override func layoutSubviews() { super.layoutSubviews() bubbleView.setNeedsLayout() bubbleView.layoutIfNeeded() bubbleView.setCornerRadius(corners: [.topLeft , .bottomRight, .bottomLeft], withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2)) } } // MARK: - 收到的语音消息 final class VoiceReceivedMsgCell: UITableViewCell { private let timeLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() private let avatarView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.cornerRadius = 15 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") iv.borderWidth = 2 iv.borderColor = .white return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .regular) label.textColor = UIColor(hexStr: "#666666") return label }() private let bubbleView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#CCEAFF") v.cornerRadius = 8 return v }() private let playIcon: UIImageView = { let iv = UIImageView(image: UIImage(named: "IM/video_received")) return iv }() private let durationLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14) label.textColor = UIColor(hexStr: "#1A1A1A") return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.addSubview(timeLabel) contentView.addSubview(bubbleView) bubbleView.addSubview(playIcon) bubbleView.addSubview(durationLabel) contentView.addSubview(avatarView) contentView.addSubview(nameLabel) timeLabel.layoutChain.top().centerX() bubbleView.layoutChain .topToBottomOfView(timeLabel, offset: 14) .width(105) .height(39) avatarView.layoutChain .topToBottomOfView(bubbleView, offset: -15) .left(12) .width(30).height(30) .bottom(10) bubbleView.layoutChain.leftToView(avatarView, offset: 13) nameLabel.layoutChain .leftToRightOfView(avatarView, offset: 5) .bottomToView(avatarView) playIcon.layoutChain .left(36) .centerY() .width(11).height(15) durationLabel.layoutChain .leftToRightOfView(playIcon, offset: 8) .centerY() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ msg: ChatMessage) { timeLabel.isHidden = !msg.showTime timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil avatarView.image = msg.avatar nameLabel.text = msg.senderName let dur = msg.content.int / 1000 durationLabel.text = dur > 0 ? "\(dur)''" : "" } private func formatTime(_ t: TimeInterval) -> String { let date = Date(timeIntervalSince1970: t) let now = Date() let calendar = Calendar.current let f = DateFormatter() if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" } else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" } else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" } else { f.dateFormat = "yyyy-M-d HH:mm" } return f.string(from: date) } override func layoutSubviews() { super.layoutSubviews() bubbleView.setNeedsLayout() bubbleView.layoutIfNeeded() bubbleView.setCornerRadius(corners: [.topLeft , .topRight, .bottomRight], withCornerRadii: CGSize(width: bubbleView.dl.height / 2, height: bubbleView.dl.height / 2)) } }