878 lines
30 KiB
Swift
878 lines
30 KiB
Swift
//
|
|
// CreateGroupView.swift
|
|
// QuickLocation
|
|
//
|
|
// Created by 八条 on 2026/6/2.
|
|
//
|
|
|
|
import UIKit
|
|
import RxSwift
|
|
import RxCocoa
|
|
|
|
class CreateGroupView: UIView {
|
|
|
|
var disposeBag = DisposeBag()
|
|
|
|
private let limitCount = 50
|
|
private let tagMaxLength = 10
|
|
|
|
var onConfirmAddTag: ((String) -> Void)?
|
|
|
|
func configureGroupTemplates(_ templates: [GroupTemplateModel]) {
|
|
guard let first = templates.first else { return }
|
|
typeNameLab.text = first.name
|
|
iconCarousel.configure(templates: templates)
|
|
submitBtn.isEnabled = true
|
|
submitBtn.alpha = 1
|
|
}
|
|
|
|
private func setupRx() {
|
|
groupNameTF.rx.text.orEmpty
|
|
.subscribe(onNext: { [weak self] text in
|
|
guard let self = self else { return }
|
|
if text.count > 10 {
|
|
self.groupNameTF.text = String(text.prefix(10))
|
|
}
|
|
})
|
|
.disposed(by: disposeBag)
|
|
|
|
Observable.merge(
|
|
groupContentTV.rx.didChange.asObservable(),
|
|
groupContentTV.rx.text.map { _ in () },
|
|
groupContentTV.rx.methodInvoked(#selector(UITextView.paste(_:))).map { _ in () }
|
|
)
|
|
.throttle(.milliseconds(100), scheduler: MainScheduler.instance)
|
|
.subscribe(onNext: { [weak self] in
|
|
guard let self = self else { return }
|
|
let count = self.groupContentTV.text.count
|
|
self.placeholderLab.isHidden = count != 0
|
|
|
|
if count > self.limitCount {
|
|
self.groupContentTV.text = String(self.groupContentTV.text.prefix(self.limitCount))
|
|
self.groupContentTV.selectedRange = NSRange(location: self.limitCount, length: 0)
|
|
return
|
|
}
|
|
})
|
|
.disposed(by: disposeBag)
|
|
|
|
addTagTF.rx.text.orEmpty
|
|
.subscribe(onNext: { [weak self] text in
|
|
guard let self = self else { return }
|
|
if text.count > self.tagMaxLength {
|
|
self.addTagTF.text = String(text.prefix(self.tagMaxLength))
|
|
}
|
|
let count = self.addTagTF.text?.count ?? 0
|
|
self.addTagCountLab.text = "\(count)/\(self.tagMaxLength)"
|
|
})
|
|
.disposed(by: disposeBag)
|
|
|
|
sheetCancelBtn.rx.tap
|
|
.subscribe(onNext: { [weak self] in
|
|
self?.dismissAddTagSheet()
|
|
})
|
|
.disposed(by: disposeBag)
|
|
|
|
sheetConfirmBtn.rx.tap
|
|
.subscribe(onNext: { [weak self] in
|
|
guard let self = self else { return }
|
|
self.onConfirmAddTag?(self.addTagTF.text ?? "")
|
|
})
|
|
.disposed(by: disposeBag)
|
|
|
|
addTagClearBtn.rx.tap
|
|
.subscribe(onNext: { [weak self] in
|
|
self?.addTagTF.text = ""
|
|
self?.addTagCountLab.text = "0/\(self?.tagMaxLength ?? 10)"
|
|
})
|
|
.disposed(by: disposeBag)
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(navBgView)
|
|
addSubview(navView)
|
|
addSubview(scrollView)
|
|
addSubview(submitBtn)
|
|
|
|
scrollView.addSubview(contentView)
|
|
contentView.addSubview(iconCarousel)
|
|
contentView.addSubview(typeNamePlaceholder)
|
|
contentView.addSubview(groupNameTitleLab)
|
|
contentView.addSubview(groupNameInputView)
|
|
groupNameInputView.addSubview(groupNameTF)
|
|
contentView.addSubview(groupContentTitleLab)
|
|
contentView.addSubview(groupContentInputView)
|
|
groupContentInputView.addSubview(groupContentTV)
|
|
groupContentInputView.addSubview(placeholderLab)
|
|
contentView.addSubview(tagTitleLab)
|
|
contentView.addSubview(tagView)
|
|
|
|
navBgView.layoutChain
|
|
.edges(excludingEdge: .bottom)
|
|
.heightToWidth(160/375)
|
|
|
|
navView.layoutChain
|
|
.edges(excludingEdge: .bottom)
|
|
.height(kNaviHeight)
|
|
|
|
submitBtn.layoutChain
|
|
.bottom(kSafeBottomMargin + 24)
|
|
.centerX()
|
|
.edgesHorzontal(30)
|
|
.height(56)
|
|
|
|
scrollView.layoutChain
|
|
.topToBottomOfView(navView)
|
|
.edgesHorzontal()
|
|
.bottomToTopOfView(submitBtn, offset: -12)
|
|
|
|
contentView.layoutChain
|
|
.edges()
|
|
.widthToView(scrollView)
|
|
|
|
iconCarousel.layoutChain
|
|
.top(8)
|
|
.edgesHorzontal()
|
|
.height(130)
|
|
|
|
typeNamePlaceholder.layoutChain
|
|
.topToBottomOfView(iconCarousel, offset: 4)
|
|
.centerX()
|
|
|
|
groupNameTitleLab.layoutChain
|
|
.topToBottomOfView(typeNamePlaceholder, offset: 24)
|
|
.left(24)
|
|
|
|
groupNameInputView.layoutChain
|
|
.topToBottomOfView(groupNameTitleLab, offset: 10)
|
|
.edgesHorzontal(20)
|
|
.height(50)
|
|
|
|
groupNameTF.layoutChain
|
|
.edgesVertical()
|
|
.left(16)
|
|
.right(48)
|
|
|
|
groupContentTitleLab.layoutChain
|
|
.topToBottomOfView(groupNameInputView, offset: 18)
|
|
.left(24)
|
|
|
|
groupContentInputView.layoutChain
|
|
.topToBottomOfView(groupContentTitleLab, offset: 10)
|
|
.edgesHorzontal(20)
|
|
.height(96)
|
|
|
|
groupContentTV.layoutChain
|
|
.top(8)
|
|
.left(12)
|
|
.right(48)
|
|
.bottom(8)
|
|
|
|
placeholderLab.layoutChain
|
|
.topToView(groupContentTV, offset: 8)
|
|
.leftToView(groupContentTV, offset: 5)
|
|
|
|
tagTitleLab.layoutChain
|
|
.topToBottomOfView(groupContentInputView, offset: 18)
|
|
.left(24)
|
|
|
|
tagView.layoutChain
|
|
.topToBottomOfView(tagTitleLab, offset: 12)
|
|
.edgesHorzontal(20)
|
|
.height(28)
|
|
.bottom(24)
|
|
|
|
setupSheet()
|
|
}
|
|
|
|
func updateTagViewHeight() {
|
|
tagView.collectionViewLayout.invalidateLayout()
|
|
tagView.layoutIfNeeded()
|
|
let height = max(28, tagView.collectionViewLayout.collectionViewContentSize.height)
|
|
tagView.layoutChain.height(height)
|
|
}
|
|
|
|
func showAddTagSheet() {
|
|
addTagTF.text = ""
|
|
addTagCountLab.text = "0/\(tagMaxLength)"
|
|
sheetOverlay.isHidden = false
|
|
sheetOverlay.alpha = 0
|
|
sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
|
|
UIView.animate(withDuration: 0.28, delay: 0, options: .curveEaseOut) {
|
|
self.sheetOverlay.alpha = 1
|
|
self.sheetPanel.transform = .identity
|
|
} completion: { _ in
|
|
self.addTagTF.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
func dismissAddTagSheet() {
|
|
addTagTF.resignFirstResponder()
|
|
UIView.animate(withDuration: 0.22, delay: 0, options: .curveEaseIn) {
|
|
self.sheetOverlay.alpha = 0
|
|
self.sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
|
|
} completion: { _ in
|
|
self.sheetOverlay.isHidden = true
|
|
self.addTagTF.text = ""
|
|
self.addTagCountLab.text = "0/\(self.tagMaxLength)"
|
|
}
|
|
}
|
|
|
|
// MARK: - Sheet
|
|
|
|
private func setupSheet() {
|
|
sheetOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.45)
|
|
sheetOverlay.isHidden = true
|
|
addSubview(sheetOverlay)
|
|
sheetOverlay.layoutChain.edges()
|
|
|
|
sheetPanel.backgroundColor = .white
|
|
sheetPanel.layer.cornerRadius = 24
|
|
sheetPanel.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
sheetPanel.clipsToBounds = true
|
|
sheetOverlay.addSubview(sheetPanel)
|
|
sheetPanel.layoutChain.edgesHorzontal().bottom()
|
|
|
|
sheetHeaderView.backgroundColor = UIColor(hexStr: "#E8F7FF")
|
|
sheetPanel.addSubview(sheetHeaderView)
|
|
sheetHeaderView.layoutChain.edgesHorzontal().top().height(56)
|
|
|
|
sheetTitleLab.text = "添加标签"
|
|
sheetTitleLab.font = .systemFont(ofSize: 16, weight: .medium)
|
|
sheetTitleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
|
sheetTitleLab.textAlignment = .center
|
|
sheetHeaderView.addSubview(sheetTitleLab)
|
|
sheetTitleLab.layoutChain.center()
|
|
|
|
sheetAddIcon.backgroundColor = UIColor(hexStr: "#D6F0FF")
|
|
sheetAddIcon.cornerRadius = 8
|
|
sheetAddIcon.contentMode = .center
|
|
let plusConfig = UIImage.SymbolConfiguration(pointSize: 13, weight: .medium)
|
|
sheetAddIcon.image = UIImage(systemName: "plus", withConfiguration: plusConfig)
|
|
sheetAddIcon.tintColor = UIColor(hexStr: "#16B3FF")
|
|
sheetPanel.addSubview(sheetAddIcon)
|
|
sheetAddIcon.layoutChain
|
|
.topToBottomOfView(sheetHeaderView, offset: 24)
|
|
.left(20)
|
|
.width(32)
|
|
.height(32)
|
|
|
|
addTagInputView.backgroundColor = UIColor(hexStr: "#F7F8FA")
|
|
addTagInputView.cornerRadius = 10
|
|
sheetPanel.addSubview(addTagInputView)
|
|
|
|
addTagClearBtn.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
|
|
addTagClearBtn.tintColor = UIColor(hexStr: "#FF5B5B")
|
|
sheetPanel.addSubview(addTagClearBtn)
|
|
addTagClearBtn.layoutChain
|
|
.centerY(sheetAddIcon)
|
|
.right(20)
|
|
.width(22)
|
|
.height(22)
|
|
|
|
addTagInputView.layoutChain
|
|
.centerY(sheetAddIcon)
|
|
.leftToRightOfView(sheetAddIcon, offset: 10)
|
|
.rightToLeftOfView(addTagClearBtn, offset: -10)
|
|
.height(40)
|
|
|
|
addTagInputView.addSubview(addTagTF)
|
|
addTagInputView.addSubview(addTagCountLab)
|
|
addTagCountLab.layoutChain
|
|
.centerY()
|
|
.right(12)
|
|
addTagTF.layoutChain
|
|
.edgesVertical()
|
|
.left(12)
|
|
.rightToLeftOfView(addTagCountLab, offset: -8)
|
|
|
|
sheetPanel.addSubview(sheetCancelBtn)
|
|
sheetPanel.addSubview(sheetConfirmBtn)
|
|
|
|
sheetCancelBtn.layoutChain
|
|
.topToBottomOfView(sheetAddIcon, offset: 28)
|
|
.left(24)
|
|
.height(44)
|
|
.bottom(20 + kSafeBottomMargin)
|
|
|
|
sheetConfirmBtn.layoutChain
|
|
.leftToRightOfView(sheetCancelBtn, offset: 12)
|
|
.right(24)
|
|
.bottomToView(sheetCancelBtn)
|
|
.heightToView(sheetCancelBtn)
|
|
.widthToView(sheetCancelBtn)
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tapSheetMask))
|
|
sheetOverlay.addGestureRecognizer(tap)
|
|
}
|
|
|
|
@objc private func tapSheetMask(_ gesture: UITapGestureRecognizer) {
|
|
let point = gesture.location(in: sheetPanel)
|
|
if sheetPanel.bounds.contains(point) { return }
|
|
dismissAddTagSheet()
|
|
}
|
|
|
|
// MARK: - Views
|
|
|
|
lazy var navBgView: UIImageView = {
|
|
let iv = UIImageView()
|
|
iv.image = UIImage(named: "Common/navBar_bg_2")
|
|
iv.contentMode = .scaleAspectFill
|
|
return iv
|
|
}()
|
|
|
|
lazy var navView: BaseNavigationView = {
|
|
let nav = BaseNavigationView(title: "创建圈子")
|
|
return nav
|
|
}()
|
|
|
|
lazy var scrollView: UIScrollView = {
|
|
let view = UIScrollView()
|
|
view.backgroundColor = .clear
|
|
view.keyboardDismissMode = .onDrag
|
|
view.showsVerticalScrollIndicator = false
|
|
return view
|
|
}()
|
|
|
|
lazy var contentView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .clear
|
|
return view
|
|
}()
|
|
|
|
lazy var iconCarousel: CreateGroupIconCarouselView = {
|
|
let view = CreateGroupIconCarouselView()
|
|
return view
|
|
}()
|
|
|
|
lazy var typeNamePlaceholder: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.cornerRadius = 10
|
|
view.isUserInteractionEnabled = false
|
|
|
|
view.addSubview(typeNameLab)
|
|
typeNameLab.layoutChain
|
|
.edgesVertical(10)
|
|
.edgesHorzontal(31)
|
|
|
|
return view
|
|
}()
|
|
|
|
lazy var typeNameLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "未知"
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#3D3D3D")
|
|
return label
|
|
}()
|
|
|
|
lazy var groupNameTitleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "圈子名称"
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
return label
|
|
}()
|
|
|
|
lazy var groupNameInputView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.cornerRadius = 10
|
|
|
|
let icon = UIImageView()
|
|
icon.image = UIImage(named: "Group/edit")
|
|
icon.contentMode = .scaleAspectFit
|
|
view.addSubview(icon)
|
|
icon.layoutChain
|
|
.right(14)
|
|
.centerY()
|
|
.width(22)
|
|
.height(22)
|
|
|
|
return view
|
|
}()
|
|
|
|
lazy var groupNameTF: UITextField = {
|
|
let textField = UITextField(frame: .zero)
|
|
textField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
|
|
textField.textColor = ThemeManager.shared.color.titleAuxColor
|
|
textField.placeholderColor(placeholder: "请输入圈子名称",
|
|
color: UIColor(hexStr: "#999999", alpha: 1.0))
|
|
return textField
|
|
}()
|
|
|
|
lazy var groupContentTitleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "圈子描述"
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
return label
|
|
}()
|
|
|
|
lazy var groupContentInputView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.cornerRadius = 10
|
|
|
|
let icon = UIImageView()
|
|
icon.image = UIImage(named: "Group/edit")
|
|
icon.contentMode = .scaleAspectFit
|
|
view.addSubview(icon)
|
|
icon.layoutChain
|
|
.right(14)
|
|
.bottom(14)
|
|
.width(22)
|
|
.height(22)
|
|
|
|
return view
|
|
}()
|
|
|
|
lazy var groupContentTV: UITextView = {
|
|
let textView = UITextView()
|
|
textView.backgroundColor = .clear
|
|
textView.font = .systemFont(ofSize: 14, weight: .medium)
|
|
textView.textColor = ThemeManager.shared.color.titleAuxColor
|
|
textView.isScrollEnabled = true
|
|
textView.textContainerInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
|
|
return textView
|
|
}()
|
|
|
|
lazy var placeholderLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "请输入.."
|
|
label.textColor = ThemeManager.shared.color.contentColor
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
return label
|
|
}()
|
|
|
|
lazy var tagTitleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "选择标签"
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
return label
|
|
}()
|
|
|
|
lazy var tagView: UICollectionView = {
|
|
let layout = CreateGroupTagFlowLayout()
|
|
layout.minimumInteritemSpacing = 8
|
|
layout.minimumLineSpacing = 8
|
|
layout.sectionInset = .zero
|
|
|
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
cv.backgroundColor = .clear
|
|
cv.isScrollEnabled = false
|
|
cv.register(TagCell.self)
|
|
return cv
|
|
}()
|
|
|
|
lazy var submitBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("创建", for: .normal)
|
|
btn.setTitleColor(.white, for: .normal)
|
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
|
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
|
btn.cornerRadius = 20
|
|
btn.isEnabled = false
|
|
btn.alpha = 0.5
|
|
return btn
|
|
}()
|
|
|
|
private let sheetOverlay = UIView()
|
|
private let sheetPanel = UIView()
|
|
private let sheetHeaderView = UIView()
|
|
private let sheetTitleLab = UILabel()
|
|
private let sheetAddIcon = UIImageView()
|
|
private let addTagInputView = UIView()
|
|
|
|
lazy var addTagTF: UITextField = {
|
|
let textField = UITextField(frame: .zero)
|
|
textField.font = .systemFont(ofSize: 14, weight: .medium)
|
|
textField.textColor = ThemeManager.shared.color.titleAuxColor
|
|
textField.placeholderColor(placeholder: "请输入标签",
|
|
color: UIColor(hexStr: "#999999", alpha: 1.0),
|
|
font: .systemFont(ofSize: 14, weight: .medium))
|
|
return textField
|
|
}()
|
|
|
|
lazy var addTagCountLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "0/10"
|
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
|
label.textColor = ThemeManager.shared.color.contentColor
|
|
return label
|
|
}()
|
|
|
|
lazy var addTagClearBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
return btn
|
|
}()
|
|
|
|
lazy var sheetCancelBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("取消", for: .normal)
|
|
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
|
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
|
btn.backgroundColor = .white
|
|
btn.borderWidth = 1
|
|
btn.borderColor = UIColor(hexStr: "#16B3FF")
|
|
btn.cornerRadius = 22
|
|
return btn
|
|
}()
|
|
|
|
lazy var sheetConfirmBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("确定", for: .normal)
|
|
btn.setTitleColor(.white, for: .normal)
|
|
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
|
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
|
btn.cornerRadius = 22
|
|
btn.clipsToBounds = true
|
|
return btn
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: .zero)
|
|
setupUI()
|
|
setupRx()
|
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
// MARK: - Icon Carousel
|
|
|
|
final class CreateGroupIconCarouselView: UIView {
|
|
|
|
var onSelectTemplate: ((GroupTemplateModel) -> Void)?
|
|
|
|
private var templates: [GroupTemplateModel] = []
|
|
private let itemSize: CGFloat = 80
|
|
private var lastWidth: CGFloat = 0
|
|
private var selectedItem = 0
|
|
|
|
func configure(templates: [GroupTemplateModel]) {
|
|
self.templates = templates
|
|
selectedItem = 0
|
|
collectionView.reloadData()
|
|
collectionView.layoutIfNeeded()
|
|
scrollToItem(selectedItem, animated: false)
|
|
if let first = templates.first {
|
|
onSelectTemplate?(first)
|
|
}
|
|
applyTransforms()
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
clipsToBounds = false
|
|
addSubview(collectionView)
|
|
collectionView.layoutChain.edges()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
guard bounds.width > 0 else { return }
|
|
if bounds.width != lastWidth {
|
|
lastWidth = bounds.width
|
|
let inset = max(0, (bounds.width - itemSize) / 2)
|
|
flowLayout.sectionInset = UIEdgeInsets(top: 25, left: inset, bottom: 25, right: inset)
|
|
collectionView.layoutIfNeeded()
|
|
scrollToItem(selectedItem, animated: false)
|
|
}
|
|
applyTransforms()
|
|
}
|
|
|
|
private func scrollToItem(_ item: Int, animated: Bool) {
|
|
guard templates.indices.contains(item) else { return }
|
|
collectionView.scrollToItem(at: IndexPath(item: item, section: 0),
|
|
at: .centeredHorizontally,
|
|
animated: animated)
|
|
}
|
|
|
|
private func selectItem(_ item: Int) {
|
|
guard templates.indices.contains(item) else { return }
|
|
selectedItem = item
|
|
onSelectTemplate?(templates[item])
|
|
}
|
|
|
|
private func nearestIndex() -> Int {
|
|
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
|
|
var nearest = 0
|
|
var minDist = CGFloat.greatestFiniteMagnitude
|
|
for i in templates.indices {
|
|
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
|
|
let dist = abs(attr.center.x - centerX)
|
|
if dist < minDist {
|
|
minDist = dist
|
|
nearest = i
|
|
}
|
|
}
|
|
return nearest
|
|
}
|
|
|
|
private func applyTransforms() {
|
|
let midX = collectionView.contentOffset.x + collectionView.bounds.width / 2
|
|
for cell in collectionView.visibleCells {
|
|
guard let attr = collectionView.layoutAttributesForItem(at: collectionView.indexPath(for: cell) ?? IndexPath()) else { continue }
|
|
let distance = abs(attr.center.x - midX)
|
|
let scale = max(0.72, 1.25 - distance / 180)
|
|
let alpha = max(0.45, 1 - distance / 220)
|
|
cell.transform = CGAffineTransform(scaleX: scale, y: scale)
|
|
cell.alpha = alpha
|
|
}
|
|
}
|
|
|
|
private lazy var flowLayout: UICollectionViewFlowLayout = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.itemSize = CGSize(width: itemSize, height: itemSize)
|
|
layout.minimumLineSpacing = 18
|
|
layout.minimumInteritemSpacing = 0
|
|
return layout
|
|
}()
|
|
|
|
private lazy var collectionView: UICollectionView = {
|
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
|
cv.backgroundColor = .clear
|
|
cv.showsHorizontalScrollIndicator = false
|
|
cv.decelerationRate = .fast
|
|
cv.clipsToBounds = false
|
|
cv.register(CreateGroupIconCell.self)
|
|
cv.dataSource = self
|
|
cv.delegate = self
|
|
return cv
|
|
}()
|
|
}
|
|
|
|
extension CreateGroupIconCarouselView: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
templates.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(for: indexPath) as CreateGroupIconCell
|
|
cell.configure(templates[indexPath.item])
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
|
applyTransforms()
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
selectItem(indexPath.item)
|
|
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
applyTransforms()
|
|
}
|
|
|
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
let proposedCenter = targetContentOffset.pointee.x + scrollView.bounds.width / 2
|
|
var nearest = 0
|
|
var minDist = CGFloat.greatestFiniteMagnitude
|
|
for i in templates.indices {
|
|
guard let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0)) else { continue }
|
|
let dist = abs(attr.center.x - proposedCenter)
|
|
if dist < minDist {
|
|
minDist = dist
|
|
nearest = i
|
|
}
|
|
}
|
|
if let attr = collectionView.layoutAttributesForItem(at: IndexPath(item: nearest, section: 0)) {
|
|
targetContentOffset.pointee.x = attr.center.x - scrollView.bounds.width / 2
|
|
}
|
|
selectItem(nearest)
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
selectItem(nearestIndex())
|
|
applyTransforms()
|
|
}
|
|
|
|
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
applyTransforms()
|
|
}
|
|
}
|
|
|
|
final class CreateGroupIconCell: UICollectionViewCell {
|
|
private let imageView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.contentMode = .scaleAspectFill
|
|
view.clipsToBounds = true
|
|
view.cornerRadius = 18
|
|
return view
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
contentView.addSubview(imageView)
|
|
imageView.layoutChain.edges()
|
|
contentView.clipsToBounds = false
|
|
clipsToBounds = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(_ template: GroupTemplateModel) {
|
|
let fallback = UIImage(named: "GroupIcon/\(template.type)") ?? UIImage(named: "GroupIcon/1")
|
|
imageView.setGroupIcon(url: template.icon, fallback: fallback)
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
imageView.setGroupIcon(url: "", fallback: nil)
|
|
transform = .identity
|
|
alpha = 1
|
|
}
|
|
}
|
|
|
|
// MARK: - Tag Flow Layout
|
|
|
|
final class CreateGroupTagFlowLayout: UICollectionViewFlowLayout {
|
|
private var alignedAttrs: [IndexPath: UICollectionViewLayoutAttributes] = [:]
|
|
|
|
override func prepare() {
|
|
super.prepare()
|
|
alignedAttrs.removeAll()
|
|
let rect = CGRect(origin: .zero, size: collectionViewContentSize)
|
|
guard let original = super.layoutAttributesForElements(in: rect) else { return }
|
|
let attrs = original.map { $0.copy() as! UICollectionViewLayoutAttributes }
|
|
leftAlign(attrs)
|
|
attrs.forEach { alignedAttrs[$0.indexPath] = $0 }
|
|
}
|
|
|
|
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
|
alignedAttrs.values.filter { $0.frame.intersects(rect) }
|
|
}
|
|
|
|
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
alignedAttrs[indexPath] ?? super.layoutAttributesForItem(at: indexPath)
|
|
}
|
|
|
|
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
collectionView?.bounds.width != newBounds.width
|
|
}
|
|
|
|
private func leftAlign(_ attrs: [UICollectionViewLayoutAttributes]) {
|
|
var x = sectionInset.left
|
|
var y = sectionInset.top
|
|
var lineHeight: CGFloat = 0
|
|
let maxX = (collectionView?.bounds.width ?? 0) - sectionInset.right
|
|
for attr in attrs where attr.representedElementCategory == .cell {
|
|
if x > sectionInset.left && x + attr.frame.width > maxX {
|
|
x = sectionInset.left
|
|
y += lineHeight + minimumLineSpacing
|
|
lineHeight = 0
|
|
}
|
|
attr.frame.origin = CGPoint(x: x, y: y)
|
|
x += attr.frame.width + minimumInteritemSpacing
|
|
lineHeight = max(lineHeight, attr.frame.height)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - TagCell
|
|
|
|
final class TagCell: UICollectionViewCell {
|
|
static let reuseId = "TagCell"
|
|
|
|
private let label: UILabel = {
|
|
let l = UILabel()
|
|
l.font = .systemFont(ofSize: 12, weight: .medium)
|
|
l.textAlignment = .center
|
|
return l
|
|
}()
|
|
|
|
private let plusView: UIImageView = {
|
|
let view = UIImageView()
|
|
let config = UIImage.SymbolConfiguration(pointSize: 13, weight: .medium)
|
|
view.image = UIImage(systemName: "plus", withConfiguration: config)
|
|
view.tintColor = .white
|
|
view.contentMode = .center
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private var isTagSelected = false
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
contentView.addSubview(label)
|
|
contentView.addSubview(plusView)
|
|
label.layoutChain.edges()
|
|
plusView.layoutChain.edges()
|
|
contentView.layer.cornerRadius = 8
|
|
contentView.backgroundColor = UIColor(hexStr: "#F0F2F5")
|
|
updateStyle()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
plusView.isHidden = true
|
|
label.isHidden = false
|
|
label.text = nil
|
|
}
|
|
|
|
static func preferredSize(for item: String) -> CGSize {
|
|
if item == CreateGroupViewModel.addTagToken {
|
|
return CGSize(width: 28, height: 28)
|
|
}
|
|
let font = UIFont.systemFont(ofSize: 12, weight: .medium)
|
|
let textW = ceil((item as NSString).size(withAttributes: [.font: font]).width)
|
|
return CGSize(width: max(52, textW + 20), height: 28)
|
|
}
|
|
|
|
func configure(_ text: String, isSelected: Bool) {
|
|
plusView.isHidden = true
|
|
label.isHidden = false
|
|
label.text = text
|
|
isTagSelected = isSelected
|
|
contentView.layer.cornerRadius = 8
|
|
updateStyle()
|
|
}
|
|
|
|
func configureAsAdd() {
|
|
plusView.isHidden = false
|
|
label.isHidden = true
|
|
label.text = nil
|
|
isTagSelected = false
|
|
contentView.backgroundColor = UIColor(hexStr: "#16B3FF")
|
|
contentView.layer.borderWidth = 0
|
|
contentView.layer.cornerRadius = 8
|
|
}
|
|
|
|
func toggleSelection() {
|
|
isTagSelected.toggle()
|
|
updateStyle()
|
|
}
|
|
|
|
private func updateStyle() {
|
|
if isTagSelected {
|
|
label.textColor = UIColor(hexStr: "#16B3FF")
|
|
contentView.backgroundColor = UIColor(hexStr: "#E3F6FF")
|
|
contentView.layer.borderWidth = 1
|
|
contentView.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor
|
|
} else {
|
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
contentView.backgroundColor = UIColor(hexStr: "#F0F2F5")
|
|
contentView.layer.borderWidth = 0
|
|
}
|
|
}
|
|
}
|