// // 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") } }