337 lines
10 KiB
Swift
337 lines
10 KiB
Swift
//
|
|
// AvatarIconListView.swift
|
|
// QuickLocation
|
|
//
|
|
// Created by 八条 on 2026/6/11.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class AvatarIconListView: UIView {
|
|
|
|
var selectedIndex: Int = 1 {
|
|
didSet {
|
|
updatePreview()
|
|
iconCollectionView.reloadData()
|
|
}
|
|
}
|
|
|
|
private var currentHeadPic: String = ""
|
|
|
|
var selectedHeadPic: String {
|
|
if selectedIndex > 0 {
|
|
return "\(selectedIndex)"
|
|
}
|
|
return currentHeadPic
|
|
}
|
|
|
|
let saveBtn = UIButton(type: .custom)
|
|
var onBadgeTap: (() -> Void)?
|
|
var onIconSelected: (() -> Void)?
|
|
|
|
private var iconImgList: [UIImage] = []
|
|
private let collectionLayout = CollectionHFlowLayout()
|
|
private var lastLayoutWidth: CGFloat = 0
|
|
private var collectionHeightConstraint: NSLayoutConstraint?
|
|
private var didScrollToInitialPage = false
|
|
|
|
private let columns = 4
|
|
private let rows = 2
|
|
private let hSpacing: CGFloat = 12
|
|
private let vSpacing: CGFloat = 12
|
|
private let cardSideInset: CGFloat = 18
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: .zero)
|
|
backgroundColor = .white
|
|
for i in 1...16 {
|
|
if let img = UIImage(named: "UserIcon/\(i)") {
|
|
iconImgList.append(img)
|
|
}
|
|
}
|
|
setupUI()
|
|
updatePreview()
|
|
updatePageControl()
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
updateGridLayout(for: bounds.width)
|
|
scrollToSelectedPageIfNeeded()
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(navView)
|
|
navView.layoutChain.edges(excludingEdge: .bottom).height(kNaviHeight)
|
|
|
|
addSubview(previewWrap)
|
|
previewWrap.addSubview(selectedIconView)
|
|
addSubview(badgeView)
|
|
previewWrap.isUserInteractionEnabled = false
|
|
addSubview(gridCard)
|
|
gridCard.addSubview(titleLab)
|
|
gridCard.addSubview(iconCollectionView)
|
|
gridCard.addSubview(pageControl)
|
|
addSubview(saveBtn)
|
|
|
|
previewWrap.layoutChain
|
|
.topToBottomOfView(navView, offset: 36)
|
|
.centerX()
|
|
.width(120)
|
|
.height(120)
|
|
|
|
selectedIconView.layoutChain.edges(all: 6)
|
|
|
|
badgeView.layoutChain
|
|
.rightToView(previewWrap, offset: 4)
|
|
.bottomToView(previewWrap, offset: 4)
|
|
.width(40)
|
|
.height(40)
|
|
|
|
saveBtn.setTitle("保存", for: .normal)
|
|
saveBtn.setTitleColor(.white, for: .normal)
|
|
saveBtn.titleLabel?.font = FontManager.boboBold(18)
|
|
saveBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
|
saveBtn.layer.cornerRadius = 28
|
|
saveBtn.clipsToBounds = true
|
|
saveBtn.isHidden = true
|
|
saveBtn.layoutChain
|
|
.edgesHorzontal(30)
|
|
.bottom(kSafeBottomMargin + 13)
|
|
.height(56)
|
|
|
|
gridCard.layoutChain
|
|
.topToBottomOfView(previewWrap, offset: 48)
|
|
.edgesHorzontal(15)
|
|
|
|
titleLab.layoutChain
|
|
.top(20)
|
|
.centerX()
|
|
|
|
iconCollectionView.layoutChain
|
|
.topToBottomOfView(titleLab, offset: 16)
|
|
.edgesHorzontal()
|
|
collectionHeightConstraint = iconCollectionView.heightAnchor.constraint(equalToConstant: 148)
|
|
collectionHeightConstraint?.isActive = true
|
|
|
|
pageControl.layoutChain
|
|
.topToBottomOfView(iconCollectionView, offset: 8)
|
|
.centerX()
|
|
.bottom(10)
|
|
.height(16)
|
|
|
|
collectionLayout.rows = rows
|
|
collectionLayout.colums = columns
|
|
collectionLayout.hSpacing = hSpacing
|
|
collectionLayout.vSpacing = vSpacing
|
|
bringSubviewToFront(badgeView)
|
|
}
|
|
|
|
func configure(headPic: String) {
|
|
currentHeadPic = headPic
|
|
if HeadPic.isRemote(headPic) {
|
|
selectedIndex = 0
|
|
selectedIconView.setHeadPic(headPic)
|
|
} else {
|
|
selectedIndex = max(headPic.integer, 1)
|
|
}
|
|
}
|
|
|
|
func setPreviewImage(_ image: UIImage) {
|
|
selectedIndex = 0
|
|
selectedIconView.image = image
|
|
}
|
|
|
|
@objc private func handleBadgeTap() {
|
|
onBadgeTap?()
|
|
}
|
|
|
|
private func updatePreview() {
|
|
guard selectedIndex > 0 else { return }
|
|
selectedIconView.image = UIImage(named: "UserIcon/\(selectedIndex)")
|
|
}
|
|
|
|
private func updatePageControl() {
|
|
let pageSize = columns * rows
|
|
let pageCount = max(Int(ceil(Double(iconImgList.count) / Double(pageSize))), 1)
|
|
pageControl.numberOfPages = pageCount
|
|
pageControl.isHidden = pageCount <= 1
|
|
}
|
|
|
|
private func updateGridLayout(for width: CGFloat) {
|
|
let cardWidth = gridCard.bounds.width > 1
|
|
? gridCard.bounds.width
|
|
: max(width - 30, 0)
|
|
guard cardWidth > 0, abs(cardWidth - lastLayoutWidth) > 0.5 else { return }
|
|
lastLayoutWidth = cardWidth
|
|
|
|
let available = cardWidth - cardSideInset * 2
|
|
let itemW = floor((available - hSpacing * CGFloat(columns - 1)) / CGFloat(columns))
|
|
collectionLayout.itemSize = CGSize(width: itemW, height: itemW)
|
|
collectionLayout.sectionInset = UIEdgeInsets(top: 0, left: cardSideInset, bottom: 0, right: cardSideInset)
|
|
collectionLayout.invalidateLayout()
|
|
|
|
let collectionHeight = itemW * CGFloat(rows) + vSpacing * CGFloat(rows - 1)
|
|
collectionHeightConstraint?.constant = collectionHeight
|
|
}
|
|
|
|
private func scrollToSelectedPageIfNeeded() {
|
|
guard !didScrollToInitialPage,
|
|
iconCollectionView.bounds.width > 1,
|
|
collectionLayout.itemSize.width > 0 else { return }
|
|
didScrollToInitialPage = true
|
|
let pageSize = columns * rows
|
|
let page = max((selectedIndex - 1) / pageSize, 0)
|
|
pageControl.currentPage = page
|
|
iconCollectionView.setContentOffset(
|
|
CGPoint(x: CGFloat(page) * iconCollectionView.bounds.width, y: 0),
|
|
animated: false
|
|
)
|
|
}
|
|
|
|
private func updateCurrentPage() {
|
|
let width = iconCollectionView.bounds.width
|
|
guard width > 0 else { return }
|
|
pageControl.currentPage = Int(round(iconCollectionView.contentOffset.x / width))
|
|
}
|
|
|
|
lazy var navView: BaseNavigationView = {
|
|
BaseNavigationView(title: "")
|
|
}()
|
|
|
|
private lazy var previewWrap: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.layer.cornerRadius = 32
|
|
view.layer.shadowColor = UIColor.black.cgColor
|
|
view.layer.shadowOpacity = 0.08
|
|
view.layer.shadowOffset = CGSize(width: 0, height: 6)
|
|
view.layer.shadowRadius = 14
|
|
return view
|
|
}()
|
|
|
|
lazy var selectedIconView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.contentMode = .scaleAspectFill
|
|
view.clipsToBounds = true
|
|
view.layer.cornerRadius = 26
|
|
return view
|
|
}()
|
|
|
|
private lazy var badgeView: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setImage(UIImage(named: "Mine/avatar_album_badge"), for: .normal)
|
|
btn.imageView?.contentMode = .scaleAspectFit
|
|
btn.extendEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
|
btn.addTarget(self, action: #selector(handleBadgeTap), for: .touchUpInside)
|
|
return btn
|
|
}()
|
|
|
|
private lazy var gridCard: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor(hexStr: "#F5F6F8")
|
|
view.layer.cornerRadius = 40
|
|
return view
|
|
}()
|
|
|
|
private lazy var titleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "选择头像"
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
|
return label
|
|
}()
|
|
|
|
private lazy var iconCollectionView: UICollectionView = {
|
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: collectionLayout)
|
|
cv.backgroundColor = .clear
|
|
cv.isPagingEnabled = true
|
|
cv.bounces = false
|
|
cv.showsHorizontalScrollIndicator = false
|
|
cv.contentInsetAdjustmentBehavior = .never
|
|
cv.register(AvatarIconCell.self)
|
|
cv.delegate = self
|
|
cv.dataSource = self
|
|
return cv
|
|
}()
|
|
|
|
private lazy var pageControl: UIPageControl = {
|
|
let control = UIPageControl()
|
|
control.currentPage = 0
|
|
control.isUserInteractionEnabled = false
|
|
control.currentPageIndicatorTintColor = UIColor(hexStr: "#16B3FF")
|
|
control.pageIndicatorTintColor = UIColor(hexStr: "#7AD6FF", alpha: 0.4)
|
|
return control
|
|
}()
|
|
}
|
|
|
|
extension AvatarIconListView: UICollectionViewDelegate, UICollectionViewDataSource {
|
|
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
iconImgList.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(for: indexPath) as AvatarIconCell
|
|
cell.configure(img: iconImgList[indexPath.item], isSelected: selectedIndex == indexPath.item + 1)
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
selectedIndex = indexPath.item + 1
|
|
onIconSelected?()
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
updateCurrentPage()
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
updateCurrentPage()
|
|
}
|
|
}
|
|
}
|
|
|
|
final class AvatarIconCell: UICollectionViewCell {
|
|
|
|
func configure(img: UIImage, isSelected: Bool) {
|
|
iconImgView.image = img
|
|
contentView.layer.borderColor = isSelected
|
|
? UIColor(hexStr: "#16B3FF").cgColor
|
|
: UIColor.clear.cgColor
|
|
}
|
|
|
|
private func setupUI() {
|
|
contentView.backgroundColor = .clear
|
|
contentView.layer.cornerRadius = 18
|
|
contentView.layer.borderWidth = 2
|
|
contentView.clipsToBounds = true
|
|
|
|
contentView.addSubview(iconImgView)
|
|
iconImgView.layoutChain.edges(all: 2)
|
|
}
|
|
|
|
private lazy var iconImgView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.backgroundColor = .clear
|
|
view.contentMode = .scaleAspectFill
|
|
view.clipsToBounds = true
|
|
view.layer.cornerRadius = 16
|
|
return view
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|