jsdw_ios/QuickLocation/Section/Group/GroupChat/GroupChatView.swift

1117 lines
35 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 playAnimation: LottieAnimationView = {
let v = LottieAnimationView()
if let path = Bundle.main.path(forResource: "message_voice_play", ofType: "json") {
v.animation = LottieAnimation.filepath(path)
}
v.loopMode = .loop
v.contentMode = .scaleAspectFit
return v
}()
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(playAnimation)
bubbleView.addSubview(durationLabel)
contentView.addSubview(avatarView)
let tap = UITapGestureRecognizer(target: self, action: #selector(togglePlay))
bubbleView.addGestureRecognizer(tap)
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)
playAnimation.layoutChain
.right(36)
.centerY()
.width(20).height(20)
durationLabel.layoutChain
.leftToRightOfView(playAnimation, offset: 4)
.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)''" : ""
}
@objc private func togglePlay() {
if playAnimation.isAnimationPlaying {
playAnimation.stop()
} else {
playAnimation.play()
}
}
override func prepareForReuse() {
super.prepareForReuse()
playAnimation.stop()
}
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 playAnimation: LottieAnimationView = {
let v = LottieAnimationView()
if let path = Bundle.main.path(forResource: "message_voice_play", ofType: "json") {
v.animation = LottieAnimation.filepath(path)
}
v.loopMode = .loop
v.contentMode = .scaleAspectFit
return v
}()
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(playAnimation)
bubbleView.addSubview(durationLabel)
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
let tap = UITapGestureRecognizer(target: self, action: #selector(togglePlay))
bubbleView.addGestureRecognizer(tap)
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)
playAnimation.layoutChain
.left(36)
.centerY()
.width(20).height(20)
durationLabel.layoutChain
.leftToRightOfView(playAnimation, offset: 4)
.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)''" : ""
}
@objc func togglePlay() {
if playAnimation.isAnimationPlaying {
playAnimation.stop()
} else {
playAnimation.play()
}
}
override func prepareForReuse() {
super.prepareForReuse()
playAnimation.stop()
}
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))
}
}