1151 lines
42 KiB
Swift
1151 lines
42 KiB
Swift
//
|
|
// PigeonMessageView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
|
|
enum PigeonMessageMode {
|
|
case image
|
|
case voice
|
|
}
|
|
|
|
final class PigeonMessageView: UIView, UITextFieldDelegate {
|
|
|
|
static let maxVoiceDuration: TimeInterval = 10
|
|
private static let designWidth: CGFloat = 375
|
|
|
|
var onModeChanged: ((PigeonMessageMode) -> Void)?
|
|
var onSelectionChanged: (([GroupMemberModel]) -> Void)?
|
|
var onUploadTapped: (() -> Void)?
|
|
var onTemplateSelected: ((PigeonLocalTemplate) -> Void)?
|
|
var onVoiceTemplateSelected: ((PigeonVoiceTemplate?) -> Void)?
|
|
var mode: PigeonMessageMode = .image {
|
|
didSet {
|
|
updateMode()
|
|
onModeChanged?(mode)
|
|
}
|
|
}
|
|
|
|
lazy var navView: BaseNavigationView = {
|
|
BaseNavigationView(title: "")
|
|
}()
|
|
|
|
let historyButton = UIButton(type: .custom)
|
|
let switchGroupButton = UIButton(type: .custom)
|
|
let deleteImageButton = UIButton(type: .custom)
|
|
let recordButton = UIButton(type: .custom)
|
|
let playPauseButton = UIButton(type: .custom)
|
|
let sendButton = UIButton(type: .custom)
|
|
|
|
private var members: [GroupMemberModel] = []
|
|
private var selectedMemberIds: Set<String> = []
|
|
|
|
private lazy var memberLayout: UICollectionViewFlowLayout = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.minimumLineSpacing = 0
|
|
layout.minimumInteritemSpacing = 0
|
|
layout.itemSize = CGSize(width: 68, height: 76)
|
|
return layout
|
|
}()
|
|
|
|
private lazy var memberCollectionView: UICollectionView = {
|
|
let view = UICollectionView(frame: .zero, collectionViewLayout: memberLayout)
|
|
view.backgroundColor = .clear
|
|
view.showsHorizontalScrollIndicator = false
|
|
view.dataSource = self
|
|
view.delegate = self
|
|
view.register(PigeonMemberCell.self, forCellWithReuseIdentifier: PigeonMemberCell.reuseId)
|
|
return view
|
|
}()
|
|
private let recipientFadeView = PigeonRecipientFadeView()
|
|
|
|
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 selectedImageMaskView = UIView()
|
|
private let imagePlaceholderLabel = UILabel()
|
|
private let overlayCaptionBar = UIView()
|
|
private let overlayCaptionLabel = UILabel()
|
|
private let copyFooter = UIView()
|
|
private let captionField = UITextField()
|
|
private let captionCountLabel = UILabel()
|
|
private let templatePickerView = PigeonTemplatePickerView()
|
|
private let voiceChipPicker = PigeonVoiceChipPickerView()
|
|
private let playbackBar = 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 selectedImageHeightConstraint: NSLayoutConstraint?
|
|
private var isRecording = false
|
|
|
|
var captionText: String {
|
|
captionField.text ?? ""
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
setupUI()
|
|
configureMembers([], selectedIds: [])
|
|
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 configureMembers(_ members: [GroupMemberModel], selectedIds: Set<String>) {
|
|
self.members = members
|
|
self.selectedMemberIds = selectedIds
|
|
memberCollectionView.reloadData()
|
|
setNeedsLayout()
|
|
}
|
|
|
|
func setCurrentGroupName(_ name: String) {
|
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
var titleAttributes = AttributeContainer()
|
|
titleAttributes.font = .systemFont(ofSize: scaled(12), weight: .medium)
|
|
|
|
var configuration = switchGroupButton.configuration ?? UIButton.Configuration.plain()
|
|
configuration.attributedTitle = AttributedString(
|
|
trimmedName.isEmpty ? "当前圈子" : trimmedName,
|
|
attributes: titleAttributes
|
|
)
|
|
configuration.image = UIImage(named: "Home/group_name_icon")
|
|
configuration.imagePadding = scaled(3)
|
|
configuration.contentInsets = NSDirectionalEdgeInsets(
|
|
top: 0,
|
|
leading: scaled(6),
|
|
bottom: 0,
|
|
trailing: scaled(6)
|
|
)
|
|
configuration.baseForegroundColor = UIColor(hexStr: "#293445")
|
|
switchGroupButton.configuration = configuration
|
|
switchGroupButton.titleLabel?.lineBreakMode = .byTruncatingTail
|
|
switchGroupButton.sizeToFit()
|
|
}
|
|
|
|
func setSelectedImage(_ image: UIImage?, selectedTemplateID: String? = nil) {
|
|
selectedImageView.image = image
|
|
selectedImageView.isHidden = image == nil
|
|
selectedImageMaskView.isHidden = image == nil
|
|
imagePlaceholderLabel.isHidden = image != nil
|
|
// deleteImageButton.isHidden = image == nil
|
|
templatePickerView.setSelection(
|
|
templateID: selectedTemplateID,
|
|
customImageSelected: image != nil && selectedTemplateID == nil
|
|
)
|
|
setNeedsLayout()
|
|
}
|
|
|
|
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 maxDuration = Self.maxVoiceDuration
|
|
let value: Int
|
|
if seconds + 0.05 >= maxDuration {
|
|
value = Int(maxDuration)
|
|
} else {
|
|
value = min(Int(maxDuration), max(0, Int(seconds.rounded(.down))))
|
|
}
|
|
let currentTime = String(format: "%02d″", value)
|
|
let timeText = "\(currentTime)/\(Int(maxDuration))″"
|
|
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()
|
|
}
|
|
updatePreviewOverlays()
|
|
}
|
|
|
|
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 clearCaption() {
|
|
captionField.text = nil
|
|
captionCountLabel.text = "0/20"
|
|
updatePreviewOverlays()
|
|
}
|
|
|
|
func clearImageDraft() {
|
|
clearCaption()
|
|
setSelectedImage(nil)
|
|
}
|
|
|
|
func clearVoiceDraft() {
|
|
setRecording(isRecording: false)
|
|
setVoiceDuration(0, hasRecording: false)
|
|
setPlaying(false)
|
|
}
|
|
|
|
private func setupUI() {
|
|
let headerView = PigeonHeaderGradientView()
|
|
addSubview(headerView)
|
|
headerView.layoutChain.top().edgesHorzontal().heightToWidth(135/375)
|
|
|
|
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 = .clear
|
|
scrollView.layer.cornerRadius = scaled(30)
|
|
scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
// scrollView.alwaysBounceVertical = true
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
addSubview(scrollView)
|
|
scrollView.layoutChain
|
|
.topToBottomOfView(navView)
|
|
.edgesHorzontal()
|
|
.bottomToTopOfView(sendButton, offset: -scaled(12))
|
|
|
|
let contentView = UIView()
|
|
contentView.backgroundColor = .clear
|
|
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(12)
|
|
.left(27)
|
|
.width(11)
|
|
.height(11)
|
|
|
|
let sectionTitle = UILabel()
|
|
sectionTitle.text = "选择收件人"
|
|
sectionTitle.font = FontManager.boboBold(scaled(18))
|
|
sectionTitle.textColor = UIColor(hexStr: "#293445")
|
|
contentView.addSubview(sectionTitle)
|
|
sectionTitle.layoutChain
|
|
.centerY(sectionSparkle)
|
|
.leftToRightOfView(sectionSparkle, offset: 8)
|
|
|
|
switchGroupButton.backgroundColor = .white
|
|
switchGroupButton.cornerRadius = 8
|
|
switchGroupButton.clipsToBounds = true
|
|
setCurrentGroupName("")
|
|
contentView.addSubview(switchGroupButton)
|
|
switchGroupButton.layoutChain
|
|
.centerY(sectionTitle)
|
|
.right(15)
|
|
.width(68, relation: .greaterThanOrEqual)
|
|
.height(24)
|
|
|
|
let recipientCard = UIView()
|
|
recipientCard.backgroundColor = .white
|
|
recipientCard.layer.cornerRadius = 20
|
|
contentView.addSubview(recipientCard)
|
|
recipientCard.layoutChain
|
|
.topToBottomOfView(sectionTitle, offset: 14)
|
|
.edgesHorzontal(15)
|
|
.height(86)
|
|
setupRecipientCard(recipientCard)
|
|
|
|
contentView.addSubview(imageEditorView)
|
|
imageEditorView.layoutChain
|
|
.topToBottomOfView(recipientCard, offset: 14)
|
|
.edgesHorzontal(15)
|
|
.heightToWidth(200.0 / 345.0)
|
|
setupImageEditor()
|
|
|
|
let thumbnailRow = UIView()
|
|
contentView.addSubview(thumbnailRow)
|
|
thumbnailRow.layoutChain
|
|
.topToBottomOfView(imageEditorView, offset: 12)
|
|
.edgesHorzontal()
|
|
.height(72)
|
|
setupThumbnailRow(thumbnailRow)
|
|
|
|
let modeContainer = UIView()
|
|
modeContainer.backgroundColor = .white
|
|
modeContainer.layer.cornerRadius = 10
|
|
contentView.addSubview(modeContainer)
|
|
modeContainer.layoutChain
|
|
.topToBottomOfView(thumbnailRow, offset: 14)
|
|
.centerX()
|
|
.width(152)
|
|
.height(36)
|
|
setupModeButtons(modeContainer)
|
|
|
|
setupCopyFooter()
|
|
voiceChipPicker.onSelectRecord = { [weak self] in
|
|
self?.onVoiceTemplateSelected?(nil)
|
|
self?.updateMode()
|
|
}
|
|
voiceChipPicker.onSelectTemplate = { [weak self] template in
|
|
self?.onVoiceTemplateSelected?(template)
|
|
self?.updateMode()
|
|
}
|
|
setupVoiceEditor()
|
|
|
|
let bottomStack = UIStackView(arrangedSubviews: [copyFooter, voiceChipPicker, voiceEditorView])
|
|
bottomStack.axis = .vertical
|
|
bottomStack.spacing = 12
|
|
bottomStack.alignment = .fill
|
|
contentView.addSubview(bottomStack)
|
|
copyFooter.layoutChain.height(40)
|
|
voiceChipPicker.layoutChain.height(56)
|
|
voiceEditorView.layoutChain.height(110)
|
|
bottomStack.layoutChain
|
|
.topToBottomOfView(modeContainer, offset: 12)
|
|
.edgesHorzontal(15)
|
|
.bottom(16)
|
|
}
|
|
|
|
private func setupRecipientCard(_ card: UIView) {
|
|
card.addSubview(memberCollectionView)
|
|
memberCollectionView.layoutChain
|
|
.edgesHorzontal(8)
|
|
.centerY()
|
|
.height(76)
|
|
|
|
card.addSubview(recipientFadeView)
|
|
recipientFadeView.isUserInteractionEnabled = false
|
|
recipientFadeView.layoutChain
|
|
.right()
|
|
.edgesVertical()
|
|
.width(30)
|
|
}
|
|
|
|
private func setupModeButtons(_ container: UIView) {
|
|
configureModeButton(
|
|
imageModeButton,
|
|
title: "文案",
|
|
imageName: "PigeonMessage/copy"
|
|
)
|
|
configureModeButton(
|
|
voiceModeButton,
|
|
title: "语音",
|
|
imageName: "PigeonMessage/mic_tab"
|
|
)
|
|
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 = .systemFont(ofSize: 14, weight: .bold)
|
|
var configuration = UIButton.Configuration.plain()
|
|
configuration.attributedTitle = AttributedString(title, attributes: titleAttributes)
|
|
configuration.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate)
|
|
configuration.imagePadding = 4
|
|
configuration.contentInsets = .zero
|
|
configuration.cornerStyle = .fixed
|
|
configuration.background.cornerRadius = 10
|
|
button.configuration = configuration
|
|
button.layer.cornerRadius = 10
|
|
button.clipsToBounds = true
|
|
}
|
|
|
|
private func setupImageEditor() {
|
|
imageEditorView.backgroundColor = UIColor(hexStr: "#DDE1E5")
|
|
imageEditorView.layer.cornerRadius = scaled(22)
|
|
imageEditorView.clipsToBounds = true
|
|
|
|
selectedImageMaskView.backgroundColor = .black.withAlphaComponent(0.2)
|
|
selectedImageMaskView.isHidden = true
|
|
selectedImageView.contentMode = .scaleAspectFill
|
|
selectedImageView.clipsToBounds = false
|
|
imageEditorView.addSubview(selectedImageView)
|
|
selectedImageView.layoutChain.edges()
|
|
// let imageHeight = selectedImageView.heightAnchor.constraint(equalTo: imageEditorView.heightAnchor)
|
|
// imageHeight.isActive = true
|
|
// selectedImageHeightConstraint = imageHeight
|
|
|
|
imageEditorView.addSubview(selectedImageMaskView)
|
|
selectedImageMaskView.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()
|
|
|
|
overlayCaptionBar.backgroundColor = UIColor.white.withAlphaComponent(0.9)
|
|
overlayCaptionBar.layer.cornerRadius = 12
|
|
overlayCaptionBar.isHidden = true
|
|
imageEditorView.addSubview(overlayCaptionBar)
|
|
overlayCaptionBar.layoutChain
|
|
.left(10)
|
|
.right(10)
|
|
.bottom(10)
|
|
.height(40)
|
|
overlayCaptionLabel.font = .systemFont(ofSize: 14)
|
|
overlayCaptionLabel.textColor = UIColor(hexStr: "#293445")
|
|
overlayCaptionBar.addSubview(overlayCaptionLabel)
|
|
overlayCaptionLabel.layoutChain.edgesHorzontal(14).centerY()
|
|
|
|
playbackBar.backgroundColor = UIColor.white.withAlphaComponent(0.92)
|
|
playbackBar.layer.cornerRadius = 12
|
|
playbackBar.isHidden = true
|
|
imageEditorView.addSubview(playbackBar)
|
|
playbackBar.layoutChain
|
|
.left(10)
|
|
.right(10)
|
|
.bottom(10)
|
|
.height(40)
|
|
setPlaying(false)
|
|
playPauseButton.isEnabled = false
|
|
playbackBar.addSubview(playPauseButton)
|
|
playPauseButton.layoutChain.left(8).centerY().width(28).height(28)
|
|
playbackBar.addSubview(waveformView)
|
|
voiceTimeLabel.text = "00″/10″"
|
|
voiceTimeLabel.font = .systemFont(ofSize: 16)
|
|
voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1")
|
|
playbackBar.addSubview(voiceTimeLabel)
|
|
voiceTimeLabel.layoutChain.right(10).centerY()
|
|
waveformView.layoutChain
|
|
.leftToRightOfView(playPauseButton, offset: 8)
|
|
.centerY()
|
|
.rightToLeftOfView(voiceTimeLabel, offset: -2)
|
|
.height(24)
|
|
}
|
|
|
|
private func setupCopyFooter() {
|
|
captionField.attributedPlaceholder = NSAttributedString(
|
|
string: "随便说点什么吧~",
|
|
attributes: [.foregroundColor: UIColor(hexStr: "#A7ABB2")]
|
|
)
|
|
captionField.font = .systemFont(ofSize: 14)
|
|
captionField.textColor = UIColor(hexStr: "#293445")
|
|
captionField.delegate = self
|
|
captionField.returnKeyType = .done
|
|
captionField.addTarget(self, action: #selector(captionChanged), for: .editingChanged)
|
|
let fieldBg = UIView()
|
|
fieldBg.backgroundColor = .white
|
|
fieldBg.layer.cornerRadius = 12
|
|
fieldBg.layer.borderWidth = 1
|
|
fieldBg.layer.borderColor = UIColor(hexStr: "#EEEEEE").cgColor
|
|
copyFooter.addSubview(fieldBg)
|
|
fieldBg.layoutChain.left().right(52).edgesVertical()
|
|
fieldBg.addSubview(captionField)
|
|
fieldBg.addSubview(captionCountLabel)
|
|
captionField.layoutChain.left(14).centerY().right(48).height(34)
|
|
captionCountLabel.text = "0/20"
|
|
captionCountLabel.font = .systemFont(ofSize: 14)
|
|
captionCountLabel.textColor = UIColor(hexStr: "#A7ABB2")
|
|
captionCountLabel.layoutChain.right(10).centerY()
|
|
|
|
deleteImageButton.setImage(UIImage(named: "PigeonMessage/trash"), for: .normal)
|
|
deleteImageButton.backgroundColor = .white
|
|
deleteImageButton.layer.cornerRadius = 12
|
|
copyFooter.addSubview(deleteImageButton)
|
|
deleteImageButton.layoutChain.right().centerY().width(40).height(40)
|
|
}
|
|
|
|
private func setupVoiceEditor() {
|
|
voiceEditorView.backgroundColor = .clear
|
|
|
|
recordButton.setImage(UIImage(named: "PigeonMessage/record_btn"), for: .normal)
|
|
voiceEditorView.addSubview(recordButton)
|
|
recordButton.layoutChain
|
|
.top()
|
|
.centerX()
|
|
.width(55)
|
|
.height(55)
|
|
|
|
recordingHintLabel.text = "按住录音 松开发送"
|
|
recordingHintLabel.font = .systemFont(ofSize: 13)
|
|
recordingHintLabel.textColor = UIColor(hexStr: "#A7ABB2")
|
|
voiceEditorView.addSubview(recordingHintLabel)
|
|
recordingHintLabel.layoutChain
|
|
.topToBottomOfView(recordButton, offset: 6)
|
|
.centerX()
|
|
}
|
|
|
|
private func setupThumbnailRow(_ row: UIView) {
|
|
row.addSubview(templatePickerView)
|
|
templatePickerView.layoutChain.edgesHorzontal(15).edgesVertical()
|
|
templatePickerView.onUploadTapped = { [weak self] in
|
|
self?.onUploadTapped?()
|
|
}
|
|
templatePickerView.onTemplateSelected = { [weak self] template in
|
|
self?.onTemplateSelected?(template)
|
|
}
|
|
}
|
|
|
|
@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"
|
|
updatePreviewOverlays()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
textField.resignFirstResponder()
|
|
return true
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
let contentWidth = CGFloat(members.count) * memberLayout.itemSize.width
|
|
recipientFadeView.isHidden = contentWidth <= memberCollectionView.bounds.width
|
|
// updateSelectedImageTopFill()
|
|
}
|
|
|
|
private func updateSelectedImageTopFill() {
|
|
guard let oldConstraint = selectedImageHeightConstraint else { return }
|
|
let editorWidth = imageEditorView.bounds.width
|
|
let editorHeight = imageEditorView.bounds.height
|
|
guard editorWidth > 1, editorHeight > 1 else { return }
|
|
let height: CGFloat
|
|
if let image = selectedImageView.image, image.size.width > 1 {
|
|
height = max(editorHeight, editorWidth * image.size.height / image.size.width)
|
|
} else {
|
|
height = editorHeight
|
|
}
|
|
if oldConstraint.secondItem == nil, abs(oldConstraint.constant - height) < 0.5 {
|
|
return
|
|
}
|
|
oldConstraint.isActive = false
|
|
let constraint = selectedImageView.heightAnchor.constraint(equalToConstant: height)
|
|
constraint.isActive = true
|
|
selectedImageHeightConstraint = constraint
|
|
}
|
|
|
|
private func updateMode() {
|
|
let isCopy = mode == .image
|
|
copyFooter.isHidden = !isCopy
|
|
voiceChipPicker.isHidden = isCopy
|
|
let usingVoiceTemplate = voiceChipPicker.selectedTemplate != nil
|
|
voiceEditorView.isHidden = isCopy || usingVoiceTemplate
|
|
styleModeButton(imageModeButton, selected: isCopy)
|
|
styleModeButton(voiceModeButton, selected: !isCopy)
|
|
updatePreviewOverlays()
|
|
}
|
|
|
|
private func updatePreviewOverlays() {
|
|
let caption = captionText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
overlayCaptionLabel.text = caption
|
|
overlayCaptionBar.isHidden = mode != .image || caption.isEmpty
|
|
let hasVoiceContent = playPauseButton.isEnabled || voiceChipPicker.selectedTemplate != nil
|
|
playbackBar.isHidden = mode != .voice || !hasVoiceContent
|
|
}
|
|
|
|
private func styleModeButton(_ button: UIButton, selected: Bool) {
|
|
let foregroundColor = selected ? UIColor.white : UIColor(hexStr: "#A5A5A5")
|
|
var configuration = button.configuration ?? .plain()
|
|
configuration.baseForegroundColor = foregroundColor
|
|
configuration.cornerStyle = .fixed
|
|
configuration.background.backgroundColor = selected ? UIColor(hexStr: "#16B3FF") : .clear
|
|
configuration.background.cornerRadius = 10
|
|
button.configuration = configuration
|
|
button.backgroundColor = .clear
|
|
button.tintColor = foregroundColor
|
|
button.layer.cornerRadius = 10
|
|
button.clipsToBounds = true
|
|
}
|
|
}
|
|
|
|
extension PigeonMessageView: UICollectionViewDataSource, UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
members.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(
|
|
withReuseIdentifier: PigeonMemberCell.reuseId,
|
|
for: indexPath
|
|
) as! PigeonMemberCell
|
|
let member = members[indexPath.item]
|
|
cell.configure(member: member, selected: selectedMemberIds.contains(member.user_id))
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
toggleMember(at: indexPath.item)
|
|
}
|
|
}
|
|
|
|
private extension PigeonMessageView {
|
|
func toggleMember(at index: Int) {
|
|
guard members.indices.contains(index) else { return }
|
|
let id = members[index].user_id
|
|
if selectedMemberIds.contains(id) {
|
|
selectedMemberIds.remove(id)
|
|
} else {
|
|
selectedMemberIds.insert(id)
|
|
}
|
|
memberCollectionView.reloadData()
|
|
onSelectionChanged?(members.filter { selectedMemberIds.contains($0.user_id) })
|
|
}
|
|
}
|
|
|
|
private final class PigeonMemberCell: UICollectionViewCell {
|
|
static let reuseId = "PigeonMemberCell"
|
|
|
|
private let avatarContainer = UIView()
|
|
private let avatarView = UIImageView()
|
|
private let onlineDot = UIView()
|
|
private let nameLabel = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
avatarContainer.layer.cornerRadius = 12
|
|
avatarContainer.clipsToBounds = false
|
|
contentView.addSubview(avatarContainer)
|
|
avatarContainer.layoutChain.top().centerX().width(42).height(42)
|
|
|
|
avatarView.contentMode = .scaleAspectFill
|
|
avatarView.clipsToBounds = true
|
|
avatarView.layer.cornerRadius = 10
|
|
avatarContainer.addSubview(avatarView)
|
|
avatarView.layoutChain.edges(all: 2)
|
|
|
|
onlineDot.layer.cornerRadius = 4.5
|
|
onlineDot.layer.borderWidth = 1.5
|
|
onlineDot.layer.borderColor = UIColor.white.cgColor
|
|
onlineDot.isUserInteractionEnabled = false
|
|
contentView.addSubview(onlineDot)
|
|
onlineDot.layoutChain
|
|
.topToView(avatarContainer, offset: -1)
|
|
.rightToView(avatarContainer, offset: 1)
|
|
.width(9)
|
|
.height(9)
|
|
|
|
nameLabel.font = .systemFont(ofSize: 10, weight: .medium)
|
|
nameLabel.textColor = UIColor(hexStr: "#293445")
|
|
nameLabel.textAlignment = .center
|
|
nameLabel.lineBreakMode = .byTruncatingTail
|
|
nameLabel.backgroundColor = UIColor(hexStr: "#F7F7F8")
|
|
nameLabel.layer.cornerRadius = 10
|
|
nameLabel.clipsToBounds = true
|
|
contentView.addSubview(nameLabel)
|
|
nameLabel.layoutChain
|
|
.topToBottomOfView(avatarContainer, offset: 5)
|
|
.centerX()
|
|
.width(58)
|
|
.height(20)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func configure(member: GroupMemberModel, selected: Bool) {
|
|
avatarView.image = member.userIcon.size.width > 0
|
|
? member.userIcon
|
|
: UIImage(named: "Common/default_avatar")
|
|
nameLabel.text = member.nick_name
|
|
onlineDot.backgroundColor = UIColor(hexStr: member.is_online ? "#67E151" : "#AAAAAA")
|
|
avatarContainer.layer.borderWidth = selected ? 2 : 0
|
|
avatarContainer.layer.borderColor = UIColor(hexStr: "#00ADFE").cgColor
|
|
}
|
|
}
|
|
|
|
private final class PigeonRecipientFadeView: UIView {
|
|
private let gradientLayer = CAGradientLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
gradientLayer.colors = [
|
|
UIColor.white.withAlphaComponent(0).cgColor,
|
|
UIColor.white.cgColor
|
|
]
|
|
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
layer.addSublayer(gradientLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
gradientLayer.frame = bounds
|
|
}
|
|
}
|
|
|
|
private final class PigeonHeaderGradientView: UIView {
|
|
private let gradientLayer = CAGradientLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
gradientLayer.colors = [
|
|
UIColor(hexStr: "#BAF4FF").cgColor,
|
|
UIColor(hexStr: "#BAF4FF").withAlphaComponent(0).cgColor
|
|
]
|
|
gradientLayer.locations = [0, 1]
|
|
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
|
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
|
layer.addSublayer(gradientLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
gradientLayer.frame = bounds
|
|
}
|
|
}
|
|
|
|
private final class PigeonVoiceChipPickerView: UIView,
|
|
UICollectionViewDataSource,
|
|
UICollectionViewDelegateFlowLayout {
|
|
var onSelectRecord: (() -> Void)?
|
|
var onSelectTemplate: ((PigeonVoiceTemplate) -> Void)?
|
|
private(set) var selectedTemplate: PigeonVoiceTemplate?
|
|
private let templates = PigeonVoiceTemplate.templates
|
|
|
|
private lazy var collectionView: UICollectionView = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.minimumLineSpacing = 8
|
|
layout.itemSize = CGSize(width: 92, height: 52)
|
|
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
view.backgroundColor = .clear
|
|
view.showsHorizontalScrollIndicator = false
|
|
view.dataSource = self
|
|
view.delegate = self
|
|
view.register(PigeonVoiceChipCell.self, forCellWithReuseIdentifier: PigeonVoiceChipCell.reuseID)
|
|
view.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
|
|
return view
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
addSubview(collectionView)
|
|
collectionView.layoutChain.edges()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
templates.count + 1
|
|
}
|
|
|
|
func collectionView(
|
|
_ collectionView: UICollectionView,
|
|
cellForItemAt indexPath: IndexPath
|
|
) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(
|
|
withReuseIdentifier: PigeonVoiceChipCell.reuseID,
|
|
for: indexPath
|
|
) as! PigeonVoiceChipCell
|
|
if indexPath.item == 0 {
|
|
cell.configureRecord(selected: selectedTemplate == nil)
|
|
} else {
|
|
let template = templates[indexPath.item - 1]
|
|
cell.configure(template: template, selected: selectedTemplate?.id == template.id)
|
|
}
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
if indexPath.item == 0 {
|
|
selectedTemplate = nil
|
|
collectionView.reloadData()
|
|
onSelectRecord?()
|
|
return
|
|
}
|
|
let template = templates[indexPath.item - 1]
|
|
selectedTemplate = template
|
|
collectionView.reloadData()
|
|
onSelectTemplate?(template)
|
|
}
|
|
}
|
|
|
|
private final class PigeonVoiceChipCell: UICollectionViewCell {
|
|
static let reuseID = "PigeonVoiceChipCell"
|
|
private let iconView = UIImageView()
|
|
private let titleLab = UILabel()
|
|
private let subLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
contentView.backgroundColor = .white
|
|
contentView.layer.cornerRadius = 14
|
|
contentView.layer.borderWidth = 1
|
|
contentView.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor
|
|
contentView.addSubview(iconView)
|
|
contentView.addSubview(titleLab)
|
|
contentView.addSubview(subLab)
|
|
iconView.contentMode = .scaleAspectFit
|
|
iconView.layoutChain.left(8).centerY().width(18).height(18)
|
|
titleLab.font = .systemFont(ofSize: 13, weight: .medium)
|
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
|
titleLab.layoutChain.leftToRightOfView(iconView, offset: 6).top(8).right(8)
|
|
subLab.font = .systemFont(ofSize: 11)
|
|
subLab.textColor = UIColor(hexStr: "#A7ABB2")
|
|
subLab.layoutChain.leftToView(titleLab).topToBottomOfView(titleLab, offset: 2)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configureRecord(selected: Bool) {
|
|
iconView.image = UIImage(named: "PigeonMessage/voice_chip_mic")
|
|
titleLab.text = "录音"
|
|
subLab.text = "说点什么吧"
|
|
applySelected(selected)
|
|
}
|
|
|
|
func configure(template: PigeonVoiceTemplate, selected: Bool) {
|
|
iconView.image = UIImage(named: "PigeonMessage/voice_chip_play")
|
|
titleLab.text = template.title
|
|
subLab.text = String(format: "%02d:%02d", Int(template.duration) / 60, Int(template.duration) % 60)
|
|
applySelected(selected)
|
|
}
|
|
|
|
private func applySelected(_ selected: Bool) {
|
|
contentView.layer.borderColor = UIColor(hexStr: selected ? "#00ADFE" : "#E8E8E8").cgColor
|
|
contentView.layer.borderWidth = selected ? 1.5 : 1
|
|
}
|
|
}
|
|
|
|
private final class PigeonTemplatePickerView: UIView,
|
|
UICollectionViewDataSource,
|
|
UICollectionViewDelegateFlowLayout {
|
|
var onUploadTapped: (() -> Void)?
|
|
var onTemplateSelected: ((PigeonLocalTemplate) -> Void)?
|
|
|
|
private let templates = PigeonLocalTemplate.imageTemplates
|
|
private var selectedTemplateID: String?
|
|
private var isCustomImageSelected = false
|
|
|
|
private lazy var collectionView: UICollectionView = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.minimumLineSpacing = 10
|
|
layout.minimumInteritemSpacing = 10
|
|
layout.itemSize = CGSize(width: 48, height: 72)
|
|
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
view.backgroundColor = .clear
|
|
view.showsHorizontalScrollIndicator = false
|
|
view.alwaysBounceHorizontal = true
|
|
view.contentInset = .zero
|
|
view.dataSource = self
|
|
view.delegate = self
|
|
view.register(
|
|
PigeonTemplateCell.self,
|
|
forCellWithReuseIdentifier: PigeonTemplateCell.reuseID
|
|
)
|
|
return view
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
addSubview(collectionView)
|
|
collectionView.layoutChain.edges()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func setSelection(templateID: String?, customImageSelected: Bool) {
|
|
selectedTemplateID = templateID
|
|
isCustomImageSelected = customImageSelected
|
|
collectionView.reloadData()
|
|
guard let templateID,
|
|
let index = templates.firstIndex(where: { $0.id == templateID }) else {
|
|
return
|
|
}
|
|
collectionView.scrollToItem(
|
|
at: IndexPath(item: index + 1, section: 0),
|
|
at: .centeredHorizontally,
|
|
animated: true
|
|
)
|
|
}
|
|
|
|
func collectionView(
|
|
_ collectionView: UICollectionView,
|
|
numberOfItemsInSection section: Int
|
|
) -> Int {
|
|
templates.count + 1
|
|
}
|
|
|
|
func collectionView(
|
|
_ collectionView: UICollectionView,
|
|
cellForItemAt indexPath: IndexPath
|
|
) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(
|
|
withReuseIdentifier: PigeonTemplateCell.reuseID,
|
|
for: indexPath
|
|
) as! PigeonTemplateCell
|
|
if indexPath.item == 0 {
|
|
cell.configureAsUpload(selected: isCustomImageSelected)
|
|
} else {
|
|
let template = templates[indexPath.item - 1]
|
|
cell.configure(
|
|
template: template,
|
|
selected: selectedTemplateID == template.id
|
|
)
|
|
}
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
if indexPath.item == 0 {
|
|
onUploadTapped?()
|
|
return
|
|
}
|
|
let template = templates[indexPath.item - 1]
|
|
selectedTemplateID = template.id
|
|
isCustomImageSelected = false
|
|
collectionView.reloadData()
|
|
onTemplateSelected?(template)
|
|
}
|
|
}
|
|
|
|
private final class PigeonTemplateCell: UICollectionViewCell {
|
|
static let reuseID = "PigeonTemplateCell"
|
|
|
|
private let imageContainer = UIView()
|
|
private let imageView = UIImageView()
|
|
private let addImageView = UIImageView(image: UIImage(named: "PigeonMessage/add"))
|
|
private let titleLabel = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
imageContainer.backgroundColor = UIColor(hexStr: "#EBECEE")
|
|
imageContainer.layer.cornerRadius = 18
|
|
imageContainer.clipsToBounds = true
|
|
contentView.addSubview(imageContainer)
|
|
imageContainer.layoutChain.top().left().right().height(48)
|
|
|
|
imageView.contentMode = .scaleAspectFill
|
|
imageView.clipsToBounds = true
|
|
imageContainer.addSubview(imageView)
|
|
imageView.layoutChain.edges()
|
|
|
|
addImageView.contentMode = .scaleAspectFit
|
|
imageContainer.addSubview(addImageView)
|
|
addImageView.layoutChain.centerX().centerY().width(28).height(28)
|
|
|
|
titleLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
|
titleLabel.textColor = UIColor(hexStr: "#293445")
|
|
titleLabel.textAlignment = .center
|
|
titleLabel.lineBreakMode = .byTruncatingTail
|
|
contentView.addSubview(titleLabel)
|
|
titleLabel.layoutChain.topToBottomOfView(imageContainer, offset: 4).left().right().height(20)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configureAsUpload(selected: Bool) {
|
|
imageView.image = nil
|
|
imageView.isHidden = true
|
|
addImageView.isHidden = false
|
|
titleLabel.text = "上传图片"
|
|
applySelection(selected)
|
|
}
|
|
|
|
func configure(template: PigeonLocalTemplate, selected: Bool) {
|
|
imageView.image = template.coverImage
|
|
imageView.isHidden = false
|
|
addImageView.isHidden = true
|
|
titleLabel.text = template.title
|
|
applySelection(selected)
|
|
}
|
|
|
|
private func applySelection(_ selected: Bool) {
|
|
imageContainer.layer.borderWidth = selected ? 2 : 0
|
|
imageContainer.layer.borderColor = UIColor(hexStr: "#00ADFE").cgColor
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|