945 lines
36 KiB
Swift
945 lines
36 KiB
Swift
//
|
|
// PigeonMessageView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
|
|
enum PigeonMessageMode {
|
|
case image
|
|
case voice
|
|
}
|
|
|
|
final class PigeonMessageView: UIView, UITextFieldDelegate {
|
|
|
|
private static let designWidth: CGFloat = 375
|
|
|
|
var onModeChanged: ((PigeonMessageMode) -> Void)?
|
|
var mode: PigeonMessageMode = .image {
|
|
didSet {
|
|
updateMode()
|
|
onModeChanged?(mode)
|
|
}
|
|
}
|
|
|
|
lazy var navView: BaseNavigationView = {
|
|
BaseNavigationView(title: "")
|
|
}()
|
|
|
|
let historyButton = UIButton(type: .custom)
|
|
let changeRecipientButton = UIButton(type: .custom)
|
|
let addImageButton = UIButton(type: .custom)
|
|
let deleteImageButton = UIButton(type: .custom)
|
|
let recordButton = UIButton(type: .custom)
|
|
let playPauseButton = UIButton(type: .custom)
|
|
let sendButton = UIButton(type: .custom)
|
|
|
|
private let recipientAvatarView = UIImageView()
|
|
private let recipientNameLabel = UILabel()
|
|
private let recipientIdLabel = UILabel()
|
|
private let recipientOnlineDotView = UIView()
|
|
private let recipientOnlineLabel = UILabel()
|
|
private let recipientStatusDividerView = UIView()
|
|
private let recipientLocationIconView = UIImageView()
|
|
private let recipientDistancePrefixLabel = UILabel()
|
|
private let recipientDistanceLabel = UILabel()
|
|
private let imageModeButton = UIButton(type: .custom)
|
|
private let voiceModeButton = UIButton(type: .custom)
|
|
private let imageEditorView = UIView()
|
|
private let voiceEditorView = UIView()
|
|
private let selectedImageView = UIImageView()
|
|
private let imagePlaceholderLabel = UILabel()
|
|
private let captionField = UITextField()
|
|
private let captionCountLabel = UILabel()
|
|
private let imageThumbnailView = UIImageView()
|
|
private let imageThumbnailButton = UIButton(type: .custom)
|
|
private let addTile = UIView()
|
|
private let voiceThumbnailView = UIView()
|
|
private let recordOuterHaloView = UIView()
|
|
private let recordInnerHaloView = UIView()
|
|
private let recordingHintLabel = UILabel()
|
|
private let voiceTimeLabel = UILabel()
|
|
private let waveformView = PigeonWaveformView()
|
|
private var addTileLeadingWithImage: NSLayoutConstraint?
|
|
private var addTileLeadingWithoutImage: NSLayoutConstraint?
|
|
private var isRecording = false
|
|
|
|
var captionText: String {
|
|
captionField.text ?? ""
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
setupUI()
|
|
configureRecipient(nil, distanceText: "与您相距 --")
|
|
setSelectedImage(nil)
|
|
clearVoiceDraft()
|
|
updateMode()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private var designScale: CGFloat {
|
|
min(1.104, max(0.853, bounds.width / Self.designWidth))
|
|
}
|
|
|
|
private func scaled(_ value: CGFloat) -> CGFloat {
|
|
value * designScale
|
|
}
|
|
|
|
func configureRecipient(_ member: GroupMemberModel?, distanceText: String) {
|
|
recipientAvatarView.image = member?.userIcon ?? UIImage(named: "Common/default_avatar")
|
|
recipientNameLabel.text = member?.nick_name ?? "暂无圈子成员"
|
|
recipientIdLabel.text = member.map { "ID:\($0.user_id)" } ?? "ID:--"
|
|
let isOnline = member?.is_online == true
|
|
recipientOnlineLabel.text = isOnline ? "在线" : "离线"
|
|
recipientOnlineDotView.backgroundColor = isOnline
|
|
? UIColor(hexStr: "#54D34D")
|
|
: UIColor(hexStr: "#C7CBD1")
|
|
recipientDistanceLabel.text = distanceText
|
|
.replacingOccurrences(of: "与您相距 ", with: "")
|
|
changeRecipientButton.isEnabled = member != nil
|
|
changeRecipientButton.alpha = member == nil ? 0.55 : 1
|
|
}
|
|
|
|
func setSelectedImage(_ image: UIImage?) {
|
|
selectedImageView.image = image
|
|
selectedImageView.isHidden = image == nil
|
|
imagePlaceholderLabel.isHidden = image != nil
|
|
imageThumbnailView.image = image
|
|
imageThumbnailButton.isHidden = image == nil
|
|
deleteImageButton.isHidden = image == nil
|
|
addTile.alpha = 1
|
|
updateAddTilePosition(hasImage: image != nil)
|
|
}
|
|
|
|
func setRecording(isRecording: Bool) {
|
|
self.isRecording = isRecording
|
|
recordingHintLabel.text = isRecording ? "松开发送" : "按住录音 松开发送"
|
|
recordOuterHaloView.backgroundColor = UIColor(
|
|
hexStr: isRecording ? "#DDF5FF" : "#F2FBFF"
|
|
)
|
|
recordInnerHaloView.backgroundColor = UIColor(
|
|
hexStr: isRecording ? "#CDEFFF" : "#E6F7FF"
|
|
)
|
|
recordButton.alpha = isRecording ? 0.86 : 1
|
|
playPauseButton.isEnabled = !isRecording
|
|
}
|
|
|
|
func setVoiceDuration(_ seconds: TimeInterval, hasRecording: Bool) {
|
|
let value = min(60, max(0, Int(seconds.rounded(.down))))
|
|
let currentTime = String(format: "%02d″", value)
|
|
let timeText = "\(currentTime)/60″"
|
|
let attributedTime = NSMutableAttributedString(
|
|
string: timeText,
|
|
attributes: [.foregroundColor: UIColor(hexStr: "#AAAAAA")]
|
|
)
|
|
attributedTime.addAttribute(
|
|
.foregroundColor,
|
|
value: UIColor(hexStr: "#13B6F1"),
|
|
range: NSRange(location: 0, length: currentTime.utf16.count)
|
|
)
|
|
voiceTimeLabel.attributedText = attributedTime
|
|
voiceThumbnailView.alpha = 1
|
|
playPauseButton.isEnabled = hasRecording
|
|
if !hasRecording, !isRecording {
|
|
waveformView.reset()
|
|
}
|
|
}
|
|
|
|
func setPlaying(_ isPlaying: Bool) {
|
|
let image: UIImage?
|
|
if isPlaying {
|
|
image = UIImage(named: "PigeonMessage/pause")
|
|
} else {
|
|
image = UIImage(
|
|
systemName: "play.circle.fill",
|
|
withConfiguration: UIImage.SymbolConfiguration(
|
|
pointSize: scaled(26),
|
|
weight: .semibold
|
|
)
|
|
)?.withTintColor(UIColor(hexStr: "#11B8F4"), renderingMode: .alwaysOriginal)
|
|
}
|
|
playPauseButton.setImage(image, for: .normal)
|
|
}
|
|
|
|
func pushMeterLevel(_ level: CGFloat) {
|
|
waveformView.push(level: level)
|
|
}
|
|
|
|
func clearImageDraft() {
|
|
captionField.text = nil
|
|
captionCountLabel.text = "0/20"
|
|
setSelectedImage(nil)
|
|
}
|
|
|
|
func clearVoiceDraft() {
|
|
setRecording(isRecording: false)
|
|
setVoiceDuration(0, hasRecording: false)
|
|
setPlaying(false)
|
|
}
|
|
|
|
private func setupUI() {
|
|
let headerView = UIImageView(image: UIImage(named: "PigeonMessage/bg"))
|
|
headerView.contentMode = .scaleToFill
|
|
headerView.clipsToBounds = true
|
|
addSubview(headerView)
|
|
headerView.layoutChain.top().edgesHorzontal().height(scaled(210))
|
|
|
|
historyButton.backgroundColor = .white.withAlphaComponent(0.8)
|
|
historyButton.setTitle(" 传书记录 ", for: .normal)
|
|
historyButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
historyButton.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
|
|
historyButton.cornerRadius = 8
|
|
|
|
addSubview(navView)
|
|
navView.addRightButton(historyButton)
|
|
navView.layoutChain.edges(excludingEdge: .bottom).height(kNaviHeight)
|
|
|
|
sendButton.setTitle("发送", for: .normal)
|
|
sendButton.setTitleColor(.white, for: .normal)
|
|
sendButton.titleLabel?.font = FontManager.boboBold(scaled(18))
|
|
sendButton.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
|
sendButton.layer.cornerRadius = 20
|
|
sendButton.clipsToBounds = true
|
|
addSubview(sendButton)
|
|
sendButton.layoutChain
|
|
.bottom(kSafeBottomMargin + 10)
|
|
.edgesHorzontal(30)
|
|
.height(56)
|
|
|
|
let scrollView = UIScrollView()
|
|
scrollView.backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
scrollView.layer.cornerRadius = scaled(30)
|
|
scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
// scrollView.alwaysBounceVertical = true
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
addSubview(scrollView)
|
|
scrollView.layoutChain
|
|
.top(scaled(174))
|
|
.edgesHorzontal()
|
|
.bottomToTopOfView(sendButton, offset: -scaled(12))
|
|
|
|
let contentView = UIView()
|
|
contentView.backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
scrollView.addSubview(contentView)
|
|
contentView.layoutChain.edges().widthToView(scrollView)
|
|
|
|
let sectionSparkle = UIImageView(image: UIImage(named: "PigeonMessage/sparkle"))
|
|
sectionSparkle.contentMode = .scaleAspectFit
|
|
contentView.addSubview(sectionSparkle)
|
|
sectionSparkle.layoutChain
|
|
.top(scaled(21))
|
|
.left(scaled(28))
|
|
.width(scaled(11))
|
|
.height(scaled(11))
|
|
|
|
let sectionTitle = UILabel()
|
|
sectionTitle.text = "选择收件人"
|
|
sectionTitle.font = FontManager.boboBold(scaled(18))
|
|
sectionTitle.textColor = UIColor(hexStr: "#293445")
|
|
contentView.addSubview(sectionTitle)
|
|
sectionTitle.layoutChain
|
|
.top(scaled(16))
|
|
.leftToRightOfView(sectionSparkle, offset: scaled(8))
|
|
|
|
let recipientCard = UIView()
|
|
recipientCard.backgroundColor = .white
|
|
recipientCard.layer.cornerRadius = scaled(20)
|
|
contentView.addSubview(recipientCard)
|
|
recipientCard.layoutChain
|
|
.topToBottomOfView(sectionTitle, offset: scaled(8))
|
|
.edgesHorzontal(scaled(15))
|
|
.height(scaled(100))
|
|
setupRecipientCard(recipientCard)
|
|
|
|
let modeContainer = UIView()
|
|
modeContainer.backgroundColor = .white
|
|
modeContainer.layer.cornerRadius = scaled(22)
|
|
contentView.addSubview(modeContainer)
|
|
modeContainer.layoutChain
|
|
.topToBottomOfView(recipientCard, offset: scaled(12))
|
|
.edgesHorzontal(scaled(15))
|
|
.height(scaled(44))
|
|
setupModeButtons(modeContainer)
|
|
|
|
contentView.addSubview(imageEditorView)
|
|
contentView.addSubview(voiceEditorView)
|
|
imageEditorView.layoutChain
|
|
.topToBottomOfView(modeContainer, offset: scaled(12))
|
|
.edgesHorzontal(scaled(15))
|
|
.height(scaled(190))
|
|
voiceEditorView.layoutChain
|
|
.topToBottomOfView(modeContainer, offset: scaled(12))
|
|
.edgesHorzontal(scaled(15))
|
|
.height(scaled(190))
|
|
setupImageEditor()
|
|
setupVoiceEditor()
|
|
|
|
let thumbnailRow = UIView()
|
|
contentView.addSubview(thumbnailRow)
|
|
thumbnailRow.layoutChain
|
|
.topToBottomOfView(imageEditorView, offset: scaled(14))
|
|
.edgesHorzontal(scaled(17))
|
|
.height(scaled(60))
|
|
.bottom(scaled(20))
|
|
setupThumbnailRow(thumbnailRow)
|
|
}
|
|
|
|
private func setupRecipientCard(_ card: UIView) {
|
|
recipientAvatarView.contentMode = .scaleAspectFill
|
|
recipientAvatarView.clipsToBounds = true
|
|
recipientAvatarView.layer.cornerRadius = scaled(29)
|
|
card.addSubview(recipientAvatarView)
|
|
recipientAvatarView.layoutChain
|
|
.left(scaled(18))
|
|
.centerY()
|
|
.width(scaled(58))
|
|
.height(scaled(58))
|
|
|
|
recipientNameLabel.font = .systemFont(ofSize: scaled(17), weight: .bold)
|
|
recipientNameLabel.textColor = UIColor(hexStr: "#293445")
|
|
recipientNameLabel.lineBreakMode = .byTruncatingTail
|
|
card.addSubview(recipientNameLabel)
|
|
recipientNameLabel.layoutChain
|
|
.top(scaled(18))
|
|
.leftToRightOfView(recipientAvatarView, offset: scaled(16))
|
|
|
|
recipientIdLabel.font = .systemFont(ofSize: scaled(12))
|
|
recipientIdLabel.textColor = UIColor(hexStr: "#767676")
|
|
recipientIdLabel.lineBreakMode = .byTruncatingMiddle
|
|
card.addSubview(recipientIdLabel)
|
|
recipientIdLabel.layoutChain
|
|
.topToBottomOfView(recipientNameLabel, offset: scaled(4))
|
|
.leftToView(recipientNameLabel)
|
|
|
|
recipientOnlineDotView.layer.cornerRadius = scaled(3)
|
|
card.addSubview(recipientOnlineDotView)
|
|
recipientOnlineDotView.layoutChain
|
|
.topToBottomOfView(recipientIdLabel, offset: scaled(10))
|
|
.leftToView(recipientNameLabel)
|
|
.width(scaled(6))
|
|
.height(scaled(6))
|
|
|
|
recipientOnlineLabel.font = .systemFont(ofSize: scaled(12), weight: .medium)
|
|
recipientOnlineLabel.textColor = UIColor(hexStr: "#747A84")
|
|
card.addSubview(recipientOnlineLabel)
|
|
recipientOnlineLabel.layoutChain
|
|
.centerY(recipientOnlineDotView)
|
|
.leftToRightOfView(recipientOnlineDotView, offset: scaled(6))
|
|
|
|
recipientStatusDividerView.backgroundColor = UIColor(hexStr: "#D8DBDF")
|
|
card.addSubview(recipientStatusDividerView)
|
|
recipientStatusDividerView.layoutChain
|
|
.centerY(recipientOnlineDotView)
|
|
.leftToRightOfView(recipientOnlineLabel, offset: scaled(10))
|
|
.width(scaled(1))
|
|
.height(scaled(14))
|
|
|
|
recipientLocationIconView.image = UIImage(
|
|
systemName: "mappin.circle",
|
|
withConfiguration: UIImage.SymbolConfiguration(
|
|
pointSize: scaled(13),
|
|
weight: .regular
|
|
)
|
|
)
|
|
recipientLocationIconView.tintColor = UIColor(hexStr: "#858A92")
|
|
recipientLocationIconView.contentMode = .scaleAspectFit
|
|
card.addSubview(recipientLocationIconView)
|
|
recipientLocationIconView.layoutChain
|
|
.centerY(recipientOnlineDotView)
|
|
.leftToRightOfView(recipientStatusDividerView, offset: scaled(9))
|
|
.width(scaled(14))
|
|
.height(scaled(14))
|
|
|
|
recipientDistancePrefixLabel.text = "与您相距"
|
|
recipientDistancePrefixLabel.font = .systemFont(ofSize: scaled(12))
|
|
recipientDistancePrefixLabel.textColor = UIColor(hexStr: "#747A84")
|
|
card.addSubview(recipientDistancePrefixLabel)
|
|
recipientDistancePrefixLabel.layoutChain
|
|
.centerY(recipientOnlineDotView)
|
|
.leftToRightOfView(recipientLocationIconView, offset: scaled(4))
|
|
|
|
recipientDistanceLabel.font = .systemFont(ofSize: scaled(12))
|
|
recipientDistanceLabel.textColor = UIColor(hexStr: "#00ADFE")
|
|
card.addSubview(recipientDistanceLabel)
|
|
recipientDistanceLabel.layoutChain
|
|
.centerY(recipientOnlineDotView)
|
|
.leftToRightOfView(recipientDistancePrefixLabel, offset: scaled(4))
|
|
|
|
changeRecipientButton.setTitle("更换收件人", for: .normal)
|
|
changeRecipientButton.setTitleColor(UIColor(hexStr: "#00ADFE"), for: .normal)
|
|
changeRecipientButton.titleLabel?.font = .systemFont(ofSize: scaled(12), weight: .medium)
|
|
changeRecipientButton.backgroundColor = UIColor(hexStr: "#F6F6F6")
|
|
changeRecipientButton.layer.cornerRadius = scaled(15)
|
|
card.addSubview(changeRecipientButton)
|
|
changeRecipientButton.layoutChain
|
|
.right(scaled(12))
|
|
.centerY()
|
|
.width(scaled(70))
|
|
.height(scaled(30))
|
|
recipientNameLabel.layoutChain.rightToLeftOfView(
|
|
changeRecipientButton,
|
|
offset: -scaled(8),
|
|
relation: .lessThanOrEqual
|
|
)
|
|
recipientIdLabel.layoutChain.rightToLeftOfView(
|
|
changeRecipientButton,
|
|
offset: -scaled(8),
|
|
relation: .lessThanOrEqual
|
|
)
|
|
}
|
|
|
|
private func setupModeButtons(_ container: UIView) {
|
|
configureModeButton(
|
|
imageModeButton,
|
|
title: "图片",
|
|
imageName: "PigeonMessage/gallery"
|
|
)
|
|
configureModeButton(
|
|
voiceModeButton,
|
|
title: "语音",
|
|
imageName: "PigeonMessage/microphone"
|
|
)
|
|
container.addSubview(imageModeButton)
|
|
container.addSubview(voiceModeButton)
|
|
imageModeButton.layoutChain
|
|
.edgesVertical(scaled(4))
|
|
.left(scaled(4))
|
|
.widthToView(container, offset: -scaled(6), multiplier: 0.5)
|
|
voiceModeButton.layoutChain
|
|
.edgesVertical(scaled(4))
|
|
.right(scaled(4))
|
|
.widthToView(container, offset: -scaled(6), multiplier: 0.5)
|
|
imageModeButton.addTarget(self, action: #selector(selectImageMode), for: .touchUpInside)
|
|
voiceModeButton.addTarget(self, action: #selector(selectVoiceMode), for: .touchUpInside)
|
|
}
|
|
|
|
private func configureModeButton(_ button: UIButton, title: String, imageName: String) {
|
|
var titleAttributes = AttributeContainer()
|
|
titleAttributes.font = FontManager.boboBold(scaled(16))
|
|
var configuration = UIButton.Configuration.plain()
|
|
configuration.attributedTitle = AttributedString(title, attributes: titleAttributes)
|
|
configuration.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate)
|
|
configuration.imagePadding = scaled(6)
|
|
configuration.contentInsets = .zero
|
|
button.configuration = configuration
|
|
button.layer.cornerRadius = scaled(18)
|
|
}
|
|
|
|
private func setupImageEditor() {
|
|
imageEditorView.backgroundColor = UIColor(hexStr: "#DDE1E5")
|
|
imageEditorView.layer.cornerRadius = scaled(22)
|
|
imageEditorView.clipsToBounds = true
|
|
|
|
selectedImageView.contentMode = .scaleAspectFill
|
|
selectedImageView.clipsToBounds = true
|
|
imageEditorView.addSubview(selectedImageView)
|
|
selectedImageView.layoutChain.edges()
|
|
|
|
imagePlaceholderLabel.text = "点击下方添加一张图片"
|
|
imagePlaceholderLabel.font = .systemFont(ofSize: scaled(14), weight: .medium)
|
|
imagePlaceholderLabel.textColor = UIColor(hexStr: "#9298A1")
|
|
imagePlaceholderLabel.textAlignment = .center
|
|
imageEditorView.addSubview(imagePlaceholderLabel)
|
|
imagePlaceholderLabel.layoutChain.centerX().centerY()
|
|
|
|
let captionBackground = UIView()
|
|
captionBackground.backgroundColor = UIColor.white.withAlphaComponent(0.9)
|
|
captionBackground.layer.cornerRadius = scaled(12)
|
|
imageEditorView.addSubview(captionBackground)
|
|
captionBackground.layoutChain
|
|
.left(scaled(10))
|
|
.right(scaled(60))
|
|
.bottom(scaled(10))
|
|
.height(scaled(40))
|
|
|
|
captionField.attributedPlaceholder = NSAttributedString(
|
|
string: "随便说点什么吧~",
|
|
attributes: [.foregroundColor: UIColor(hexStr: "#A7ABB2")]
|
|
)
|
|
captionField.font = .systemFont(ofSize: scaled(14))
|
|
captionField.textColor = UIColor(hexStr: "#293445")
|
|
captionField.delegate = self
|
|
captionField.addTarget(self, action: #selector(captionChanged), for: .editingChanged)
|
|
captionBackground.addSubview(captionField)
|
|
captionBackground.addSubview(captionCountLabel)
|
|
captionField.layoutChain
|
|
.left(scaled(14))
|
|
.centerY()
|
|
.right(scaled(48))
|
|
.height(scaled(34))
|
|
captionCountLabel.text = "0/20"
|
|
captionCountLabel.font = .systemFont(ofSize: scaled(14))
|
|
captionCountLabel.textColor = UIColor(hexStr: "#A7ABB2")
|
|
captionCountLabel.layoutChain.right(scaled(10)).centerY()
|
|
|
|
deleteImageButton.setImage(UIImage(named: "PigeonMessage/trash"), for: .normal)
|
|
deleteImageButton.backgroundColor = UIColor.white.withAlphaComponent(0.92)
|
|
deleteImageButton.layer.cornerRadius = scaled(12)
|
|
imageEditorView.addSubview(deleteImageButton)
|
|
deleteImageButton.layoutChain
|
|
.right(scaled(10))
|
|
.bottom(scaled(10))
|
|
.width(scaled(40))
|
|
.height(scaled(40))
|
|
}
|
|
|
|
private func setupVoiceEditor() {
|
|
voiceEditorView.backgroundColor = .white
|
|
voiceEditorView.layer.cornerRadius = scaled(22)
|
|
|
|
recordOuterHaloView.backgroundColor = UIColor(hexStr: "#F2FBFF")
|
|
recordOuterHaloView.layer.cornerRadius = scaled(46)
|
|
voiceEditorView.addSubview(recordOuterHaloView)
|
|
recordOuterHaloView.layoutChain
|
|
.top(scaled(15))
|
|
.centerX()
|
|
.width(scaled(92))
|
|
.height(scaled(92))
|
|
|
|
recordInnerHaloView.backgroundColor = UIColor(hexStr: "#E6F7FF")
|
|
recordInnerHaloView.layer.cornerRadius = scaled(37)
|
|
voiceEditorView.addSubview(recordInnerHaloView)
|
|
recordInnerHaloView.layoutChain
|
|
.centerX(recordOuterHaloView)
|
|
.centerY(recordOuterHaloView)
|
|
.width(scaled(74))
|
|
.height(scaled(74))
|
|
|
|
recordButton.setImage(UIImage(named: "PigeonMessage/record_microphone"), for: .normal)
|
|
voiceEditorView.addSubview(recordButton)
|
|
recordButton.layoutChain
|
|
.centerX(recordOuterHaloView)
|
|
.centerY(recordOuterHaloView)
|
|
.width(scaled(92))
|
|
.height(scaled(92))
|
|
|
|
recordingHintLabel.text = "按住录音 松开发送"
|
|
recordingHintLabel.font = .systemFont(ofSize: scaled(14))
|
|
recordingHintLabel.textColor = UIColor(hexStr: "#A7ABB2")
|
|
voiceEditorView.addSubview(recordingHintLabel)
|
|
recordingHintLabel.layoutChain
|
|
.topToBottomOfView(recordOuterHaloView, offset: scaled(5))
|
|
.centerX()
|
|
|
|
let playbackBar = UIView()
|
|
playbackBar.backgroundColor = UIColor(hexStr: "#F5F5F6")
|
|
playbackBar.layer.cornerRadius = scaled(12)
|
|
voiceEditorView.addSubview(playbackBar)
|
|
playbackBar.layoutChain
|
|
.left(scaled(10))
|
|
.right(scaled(10))
|
|
.bottom(scaled(10))
|
|
.height(scaled(40))
|
|
|
|
setPlaying(false)
|
|
playPauseButton.isEnabled = false
|
|
playbackBar.addSubview(playPauseButton)
|
|
playPauseButton.layoutChain
|
|
.left(scaled(8))
|
|
.centerY()
|
|
.width(scaled(28))
|
|
.height(scaled(28))
|
|
|
|
playbackBar.addSubview(waveformView)
|
|
waveformView.layoutChain
|
|
.leftToRightOfView(playPauseButton, offset: scaled(8))
|
|
.centerY()
|
|
.right(scaled(66))
|
|
.height(scaled(24))
|
|
|
|
voiceTimeLabel.text = "00″/60″"
|
|
voiceTimeLabel.font = .systemFont(ofSize: scaled(16))
|
|
voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1")
|
|
playbackBar.addSubview(voiceTimeLabel)
|
|
voiceTimeLabel.layoutChain.right(scaled(10)).centerY()
|
|
}
|
|
|
|
private func setupThumbnailRow(_ row: UIView) {
|
|
imageThumbnailButton.layer.cornerRadius = scaled(20)
|
|
imageThumbnailButton.layer.borderWidth = scaled(2)
|
|
imageThumbnailButton.layer.borderColor = UIColor(hexStr: "#09B6F4").cgColor
|
|
imageThumbnailButton.clipsToBounds = true
|
|
row.addSubview(imageThumbnailButton)
|
|
imageThumbnailButton.layoutChain
|
|
.left()
|
|
.top()
|
|
.width(scaled(60))
|
|
.height(scaled(60))
|
|
|
|
imageThumbnailView.contentMode = .scaleAspectFill
|
|
imageThumbnailView.isUserInteractionEnabled = false
|
|
imageThumbnailButton.addSubview(imageThumbnailView)
|
|
imageThumbnailView.layoutChain.edges()
|
|
|
|
let tick = UIImageView(image: UIImage(named: "PigeonMessage/tick_circle"))
|
|
imageThumbnailButton.addSubview(tick)
|
|
tick.layoutChain
|
|
.right(scaled(6))
|
|
.bottom(scaled(5))
|
|
.width(scaled(16))
|
|
.height(scaled(16))
|
|
|
|
addTile.backgroundColor = UIColor(hexStr: "#EBECEE")
|
|
addTile.layer.cornerRadius = scaled(20)
|
|
row.addSubview(addTile)
|
|
addTile.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
addTile.topAnchor.constraint(equalTo: row.topAnchor),
|
|
addTile.widthAnchor.constraint(equalToConstant: scaled(60)),
|
|
addTile.heightAnchor.constraint(equalToConstant: scaled(60))
|
|
])
|
|
addTileLeadingWithImage = addTile.leadingAnchor.constraint(
|
|
equalTo: imageThumbnailButton.trailingAnchor,
|
|
constant: scaled(10)
|
|
)
|
|
addTileLeadingWithoutImage = addTile.leadingAnchor.constraint(equalTo: row.leadingAnchor)
|
|
addTileLeadingWithoutImage?.isActive = true
|
|
addTile.addSubview(addImageButton)
|
|
addImageButton.layoutChain.edges()
|
|
let addIcon = UIImageView(image: UIImage(named: "PigeonMessage/add"))
|
|
addIcon.contentMode = .scaleAspectFit
|
|
let addLabel = UILabel()
|
|
addLabel.text = "添加"
|
|
addLabel.font = .systemFont(ofSize: scaled(13))
|
|
addLabel.textColor = UIColor(hexStr: "#ADB1B8")
|
|
addTile.addSubview(addIcon)
|
|
addTile.addSubview(addLabel)
|
|
addIcon.layoutChain
|
|
.top(scaled(8))
|
|
.centerX()
|
|
.width(scaled(24))
|
|
.height(scaled(24))
|
|
addLabel.layoutChain
|
|
.topToBottomOfView(addIcon, offset: scaled(2))
|
|
.centerX()
|
|
|
|
voiceThumbnailView.backgroundColor = UIColor(hexStr: "#F2F3F4")
|
|
voiceThumbnailView.layer.cornerRadius = scaled(20)
|
|
voiceThumbnailView.layer.borderWidth = scaled(2)
|
|
voiceThumbnailView.layer.borderColor = UIColor(hexStr: "#09B6F4").cgColor
|
|
row.addSubview(voiceThumbnailView)
|
|
voiceThumbnailView.layoutChain
|
|
.left()
|
|
.top()
|
|
.width(scaled(60))
|
|
.height(scaled(60))
|
|
let mic = UIImageView(
|
|
image: UIImage(named: "PigeonMessage/microphone_thumb")?.withRenderingMode(.alwaysTemplate)
|
|
)
|
|
mic.tintColor = UIColor(hexStr: "#AEB3BA")
|
|
mic.contentMode = .scaleAspectFit
|
|
let voiceLabel = UILabel()
|
|
voiceLabel.text = "录音"
|
|
voiceLabel.font = .systemFont(ofSize: scaled(13))
|
|
voiceLabel.textColor = UIColor(hexStr: "#969CA5")
|
|
voiceThumbnailView.addSubview(mic)
|
|
voiceThumbnailView.addSubview(voiceLabel)
|
|
mic.layoutChain
|
|
.top(scaled(7))
|
|
.centerX()
|
|
.width(scaled(24))
|
|
.height(scaled(24))
|
|
voiceLabel.layoutChain
|
|
.topToBottomOfView(mic, offset: scaled(2))
|
|
.centerX()
|
|
}
|
|
|
|
@objc private func selectImageMode() {
|
|
mode = .image
|
|
}
|
|
|
|
@objc private func selectVoiceMode() {
|
|
mode = .voice
|
|
}
|
|
|
|
@objc private func captionChanged() {
|
|
let text = captionField.text ?? ""
|
|
if text.count > 20 {
|
|
captionField.text = String(text.prefix(20))
|
|
}
|
|
captionCountLabel.text = "\((captionField.text ?? "").count)/20"
|
|
}
|
|
|
|
func textField(
|
|
_ textField: UITextField,
|
|
shouldChangeCharactersIn range: NSRange,
|
|
replacementString string: String
|
|
) -> Bool {
|
|
guard let current = textField.text,
|
|
let stringRange = Range(range, in: current) else { return true }
|
|
return current.replacingCharacters(in: stringRange, with: string).count <= 20
|
|
}
|
|
|
|
private func updateAddTilePosition(hasImage: Bool) {
|
|
addTileLeadingWithImage?.isActive = false
|
|
addTileLeadingWithoutImage?.isActive = false
|
|
if hasImage {
|
|
addTileLeadingWithImage?.isActive = true
|
|
} else {
|
|
addTileLeadingWithoutImage?.isActive = true
|
|
}
|
|
}
|
|
|
|
private func updateMode() {
|
|
let isImage = mode == .image
|
|
imageEditorView.isHidden = !isImage
|
|
voiceEditorView.isHidden = isImage
|
|
imageThumbnailButton.isHidden = !isImage || selectedImageView.image == nil
|
|
addTile.isHidden = !isImage
|
|
voiceThumbnailView.isHidden = isImage
|
|
styleModeButton(imageModeButton, selected: isImage)
|
|
styleModeButton(voiceModeButton, selected: !isImage)
|
|
}
|
|
|
|
private func styleModeButton(_ button: UIButton, selected: Bool) {
|
|
button.backgroundColor = selected ? UIColor(hexStr: "#45C2FF") : .clear
|
|
let foregroundColor = selected ? UIColor.white : UIColor(hexStr: "#A5A5A5")
|
|
var configuration = button.configuration
|
|
configuration?.baseForegroundColor = foregroundColor
|
|
button.configuration = configuration
|
|
button.tintColor = foregroundColor
|
|
}
|
|
}
|
|
|
|
final class PigeonGradientButton: UIButton {
|
|
private let gradient = CAGradientLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
gradient.colors = [
|
|
UIColor(hexStr: "#00ADFE").cgColor,
|
|
UIColor(hexStr: "#45C2FF").cgColor
|
|
]
|
|
gradient.startPoint = CGPoint(x: 0, y: 0.5)
|
|
gradient.endPoint = CGPoint(x: 1, y: 0.5)
|
|
layer.insertSublayer(gradient, at: 0)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
gradient.frame = bounds
|
|
layer.cornerRadius = bounds.height / 2
|
|
gradient.cornerRadius = bounds.height / 2
|
|
}
|
|
}
|
|
|
|
final class PigeonWaveformView: UIView {
|
|
private static let idleLevels: [CGFloat] = [
|
|
0.12, 0.12, 0.14, 0.14, 0.16, 0.14, 0.18,
|
|
0.22, 0.18, 0.30, 0.42, 0.22, 0.34, 0.92,
|
|
0.46, 0.28, 0.22, 0.18, 0.24, 0.18, 0.16,
|
|
0.14, 0.14, 0.12, 0.12, 0.12
|
|
]
|
|
|
|
private var levels = PigeonWaveformView.idleLevels
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func push(level: CGFloat) {
|
|
levels.removeFirst()
|
|
levels.append(min(1, max(0.12, level)))
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
func reset() {
|
|
levels = Self.idleLevels
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
guard let context = UIGraphicsGetCurrentContext(), !levels.isEmpty else { return }
|
|
let activeColor = UIColor(hexStr: "#00ADFE")
|
|
let inactiveColor = UIColor(hexStr: "#86D9F8")
|
|
let spacing = rect.width / CGFloat(levels.count)
|
|
for (index, level) in levels.enumerated() {
|
|
let edgeDistance = min(index, levels.count - 1 - index)
|
|
let color = edgeDistance < 4 ? inactiveColor : activeColor
|
|
let x = CGFloat(index) * spacing + spacing / 2
|
|
if level <= 0.18 {
|
|
let radius = min(2, spacing * 0.3)
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(
|
|
in: CGRect(
|
|
x: x - radius,
|
|
y: rect.midY - radius,
|
|
width: radius * 2,
|
|
height: radius * 2
|
|
)
|
|
)
|
|
continue
|
|
}
|
|
|
|
let height = max(4, rect.height * level)
|
|
context.setStrokeColor(color.cgColor)
|
|
context.setLineWidth(min(3, spacing * 0.48))
|
|
context.setLineCap(.round)
|
|
context.move(to: CGPoint(x: x, y: rect.midY - height / 2))
|
|
context.addLine(to: CGPoint(x: x, y: rect.midY + height / 2))
|
|
context.strokePath()
|
|
}
|
|
}
|
|
}
|
|
|
|
final class PigeonRecipientPickerView: UIView {
|
|
private let members: [GroupMemberModel]
|
|
private let selectedId: String
|
|
private let onSelect: (GroupMemberModel) -> Void
|
|
private let tableView = UITableView(frame: .zero, style: .plain)
|
|
private let sheet = UIView()
|
|
|
|
static func show(
|
|
members: [GroupMemberModel],
|
|
selectedId: String,
|
|
onSelect: @escaping (GroupMemberModel) -> Void
|
|
) {
|
|
guard let window = kKeyWindow, !members.isEmpty else { return }
|
|
let picker = PigeonRecipientPickerView(
|
|
frame: CGRect(origin: .zero, size: kScreenSize),
|
|
members: members,
|
|
selectedId: selectedId,
|
|
onSelect: onSelect
|
|
)
|
|
window.addSubview(picker)
|
|
picker.alpha = 0
|
|
UIView.animate(withDuration: 0.2) {
|
|
picker.alpha = 1
|
|
}
|
|
}
|
|
|
|
private init(
|
|
frame: CGRect,
|
|
members: [GroupMemberModel],
|
|
selectedId: String,
|
|
onSelect: @escaping (GroupMemberModel) -> Void
|
|
) {
|
|
self.members = members
|
|
self.selectedId = selectedId
|
|
self.onSelect = onSelect
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundColor = UIColor.black.withAlphaComponent(0.48)
|
|
let backgroundTap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
|
|
backgroundTap.delegate = self
|
|
addGestureRecognizer(backgroundTap)
|
|
|
|
sheet.backgroundColor = .white
|
|
sheet.layer.cornerRadius = 26
|
|
sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
addSubview(sheet)
|
|
sheet.layoutChain.bottom().edgesHorzontal().height(min(460, CGFloat(members.count * 72 + 86)))
|
|
|
|
let title = UILabel()
|
|
title.text = "选择收件人"
|
|
title.font = .systemFont(ofSize: 20, weight: .bold)
|
|
title.textColor = UIColor(hexStr: "#293445")
|
|
sheet.addSubview(title)
|
|
title.layoutChain.top(20).left(20)
|
|
|
|
tableView.backgroundColor = .clear
|
|
tableView.separatorStyle = .none
|
|
tableView.rowHeight = 68
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
tableView.register(PigeonRecipientCell.self, forCellReuseIdentifier: "PigeonRecipientCell")
|
|
sheet.addSubview(tableView)
|
|
tableView.layoutChain.top(58).edgesHorzontal().bottom(kSafeBottomMargin)
|
|
|
|
}
|
|
|
|
@objc private func dismiss() {
|
|
UIView.animate(withDuration: 0.18, animations: {
|
|
self.alpha = 0
|
|
}) { _ in
|
|
self.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PigeonRecipientPickerView: UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate {
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
!(touch.view?.isDescendant(of: sheet) ?? false)
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
members.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
guard let cell = tableView.dequeueReusableCell(
|
|
withIdentifier: "PigeonRecipientCell",
|
|
for: indexPath
|
|
) as? PigeonRecipientCell else {
|
|
return UITableViewCell()
|
|
}
|
|
let member = members[indexPath.row]
|
|
cell.configure(member: member, selected: member.user_id == selectedId)
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
onSelect(members[indexPath.row])
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
private final class PigeonRecipientCell: UITableViewCell {
|
|
private let avatarView = UIImageView()
|
|
private let nameLabel = UILabel()
|
|
private let statusLabel = UILabel()
|
|
private let tickView = UIImageView(image: UIImage(named: "PigeonMessage/tick_circle"))
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
selectionStyle = .none
|
|
avatarView.contentMode = .scaleAspectFill
|
|
avatarView.clipsToBounds = true
|
|
avatarView.layer.cornerRadius = 20
|
|
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
nameLabel.textColor = UIColor(hexStr: "#293445")
|
|
statusLabel.font = .systemFont(ofSize: 12)
|
|
statusLabel.textColor = UIColor(hexStr: "#9EA3AA")
|
|
contentView.addSubview(avatarView)
|
|
contentView.addSubview(nameLabel)
|
|
contentView.addSubview(statusLabel)
|
|
contentView.addSubview(tickView)
|
|
avatarView.layoutChain.left(20).centerY().width(48).height(48)
|
|
nameLabel.layoutChain.top(13).leftToRightOfView(avatarView, offset: 14)
|
|
statusLabel.layoutChain.topToBottomOfView(nameLabel, offset: 4).leftToView(nameLabel)
|
|
tickView.layoutChain.right(20).centerY().width(16).height(16)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(member: GroupMemberModel, selected: Bool) {
|
|
avatarView.image = member.userIcon
|
|
nameLabel.text = member.nick_name
|
|
statusLabel.text = member.is_online ? "在线" : "离线"
|
|
tickView.isHidden = !selected
|
|
}
|
|
}
|