// // GroupView.swift // QuickLocation // // Created by 八条 on 2026/5/29. // import UIKit import RxSwift import RxCocoa import SDCycleScrollView class GroupView: UIView { var disposeBag = DisposeBag() // MARK: - Scroll State (参考 ShopDetailNestView) enum ScrollState { case pending, scrolling, ended } var scrollState: ScrollState = .pending /// segment 所在 Y(在 contentView 中的位置) var segmentY: CGFloat { let bannerH = bounds.width * (100.0 / 375.0) return 10 + bannerH + 12 + 60 + 12 + 16 + 8 + 140 + 4 } /// 吸顶临界值(mainScrollView 达到此值后锁定) var stickThreshold: CGFloat { return segmentY - kNaviHeight } private func setupUI() { addSubview(navBgView) navBgView.addSubview(navTitleLabel) navBgView.addSubview(scanBtn) addSubview(mainScrollView) mainScrollView.addSubview(contentView) contentView.addSubview(cycleScrollView) contentView.addSubview(actionButtonsView) actionButtonsView.addSubview(createGroupBtn) actionButtonsView.addSubview(joinGroupBtn) contentView.addSubview(hotGroupTitleLabel) contentView.addSubview(hotGroupsCollectionView) contentView.addSubview(segmentView) segmentView.addSubview(createdTabLabel) segmentView.addSubview(joinedTabLabel) segmentView.addSubview(tabIndicator) contentView.addSubview(segmentScrollView) segmentScrollView.addSubview(segmentContentView) segmentContentView.addSubview(createdTableView) segmentContentView.addSubview(joinedTableView) navBgView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerX() scanBtn.layoutChain .right(15) .centerY(navTitleLabel) .width(24).height(24) mainScrollView.layoutChain .topToBottomOfView(navBgView) .edges(excludingEdge: .top) contentView.layoutChain .edges() .widthToView(mainScrollView) // 轮播图 cycleScrollView.layoutChain .top(10) .left(15).right(15) .heightToWidth(100/375) // 创建/加入圈子 let btnWidth = (UIScreen.main.bounds.width - 48) / 2 actionButtonsView.layoutChain .topToBottomOfView(cycleScrollView, offset: 12) .edgesHorzontal() .height(60) createGroupBtn.layoutChain .left(16).centerY() .width(btnWidth).height(44) joinGroupBtn.layoutChain .right(16).centerY() .width(btnWidth).height(44) // 热门酷圈 hotGroupTitleLabel.layoutChain .topToBottomOfView(actionButtonsView, offset: 12) .left(16) hotGroupsCollectionView.layoutChain .topToBottomOfView(hotGroupTitleLabel, offset: 8) .edgesHorzontal() .height(140) // Segment let labelWidth = (UIScreen.main.bounds.width - 60) / 2 segmentView.layoutChain .topToBottomOfView(hotGroupsCollectionView, offset: 4) .edgesHorzontal() .height(44) createdTabLabel.layoutChain .left(30).centerY() .width(labelWidth) joinedTabLabel.layoutChain .right(30).centerY() .width(labelWidth) tabIndicator.layoutChain .bottom(2).centerX(createdTabLabel) .width(30).height(3) // 横向 segmentScrollView segmentScrollView.layoutChain .topToBottomOfView(segmentView) .edgesHorzontal() .bottom() segmentContentView.layoutChain .edges() .heightToView(segmentScrollView) let svWidth = UIScreen.main.bounds.width createdTableView.layoutChain .top().bottom().left() .width(svWidth) joinedTableView.layoutChain .top().bottom() .leftToRightOfView(createdTableView) .width(svWidth) } override func layoutSubviews() { super.layoutSubviews() segmentContentView.layoutChain.width(bounds.width * 2) let minContentHeight = segmentY + bounds.height if contentView.frame.height < minContentHeight { contentView.layoutChain.height(minContentHeight) } } // MARK: - Nav lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_1") iv.contentMode = .scaleAspectFill return iv }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.text = "圈子" label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var scanBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Group/scan"), for: .normal) return btn }() // MARK: - Main Scroll lazy var mainScrollView: UIScrollView = { let sv = UIScrollView() sv.backgroundColor = .clear sv.delegate = self sv.showsVerticalScrollIndicator = false sv.bounces = false return sv }() lazy var contentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 轮播图 lazy var cycleScrollView: SDCycleScrollView = { let view = SDCycleScrollView(frame: .zero) view.backgroundColor = .lightGray view.scrollDirection = .horizontal view.showPageControl = true view.bannerImageViewContentMode = .scaleAspectFill view.autoScrollTimeInterval = 5 view.currentPageDotColor = UIColor(hexStr: "#16B3FF") view.pageDotColor = UIColor(hexStr: "#7AD6FF").withAlphaComponent(0.4) view.pageControlDotSize = CGSize(width: 8, height: 8) view.localizationImageNamesGroup = ["map_avatar_1", "map_avatar_2", "map_avatar_3"] view.clipsToBounds = false view.pageControlBottomOffset = -34 return view }() // MARK: - 创建 / 加入 圈子 private lazy var actionButtonsView: UIView = { let v = UIView() v.backgroundColor = .white return v }() private(set) lazy var createGroupBtn: UIView = { return makeActionButton(title: "创建圈子", icon: "Home/group_icon") }() private(set) lazy var joinGroupBtn: UIView = { return makeActionButton(title: "加入圈子", icon: "Home/group") }() private func makeActionButton(title: String, icon: String) -> UIView { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F5F7FA") v.layer.cornerRadius = 12 v.isUserInteractionEnabled = true let imgView = UIImageView(image: UIImage(named: icon)) imgView.contentMode = .scaleAspectFit v.addSubview(imgView) let label = UILabel() label.text = title label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#1A1A1A") v.addSubview(label) imgView.layoutChain .left(16).centerY() .width(24).height(24) label.layoutChain .leftToRightOfView(imgView, offset: 8) .centerY() return v } // MARK: - 热门酷圈 private lazy var hotGroupTitleLabel: UILabel = { let label = UILabel() label.text = "热门酷圈" label.font = .systemFont(ofSize: 16, weight: .bold) label.textColor = UIColor(hexStr: "#1A1A1A") return label }() private(set) lazy var hotGroupsCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 120, height: 140) layout.minimumLineSpacing = 12 layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(HotGroupCell.self, forCellWithReuseIdentifier: HotGroupCell.reuseId) return cv }() // MARK: - Segment private(set) lazy var segmentView: UIView = { let v = UIView() v.backgroundColor = .white return v }() private(set) lazy var createdTabLabel: UILabel = { let label = UILabel() label.text = "我创建的圈子" label.font = .systemFont(ofSize: 15, weight: .semibold) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() private(set) lazy var joinedTabLabel: UILabel = { let label = UILabel() label.text = "我加入的圈子" label.font = .systemFont(ofSize: 15, weight: .medium) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center label.isUserInteractionEnabled = true return label }() private lazy var tabIndicator: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#16B3FF") v.layer.cornerRadius = 2 return v }() // MARK: - 横向滚动容器 private(set) lazy var segmentScrollView: UIScrollView = { let sv = UIScrollView() sv.isPagingEnabled = true sv.showsHorizontalScrollIndicator = false sv.bounces = false sv.delegate = self return sv }() private lazy var segmentContentView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() // MARK: - 两个列表 (使用 PagerTableView 支持手势转发) private(set) lazy var createdTableView: PagerTableView = { let tv = PagerTableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.rowHeight = 72 tv.bounces = true tv.panDelegate = self return tv }() private(set) lazy var joinedTableView: PagerTableView = { let tv = PagerTableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.showsVerticalScrollIndicator = false tv.register(CircleGroupCell.self) tv.rowHeight = 72 tv.bounces = true tv.panDelegate = self return tv }() // MARK: - 当前 segment var currentSegmentIndex: Int = 0 func selectSegment(at index: Int) { currentSegmentIndex = index let isCreated = index == 0 UIView.animate(withDuration: 0.2) { self.createdTabLabel.textColor = isCreated ? UIColor(hexStr: "#1A1A1A") : UIColor(hexStr: "#999999") self.joinedTabLabel.textColor = isCreated ? UIColor(hexStr: "#999999") : UIColor(hexStr: "#1A1A1A") self.tabIndicator.center.x = isCreated ? self.createdTabLabel.center.x : self.joinedTabLabel.center.x self.layoutIfNeeded() } } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 由子 tableView 回调(GroupViewController 转发 Rx 事件) func childTableViewDidScroll(_ scrollView: UIScrollView) { self.scrollViewDidScroll(scrollView) } } // MARK: - UIScrollViewDelegate + UIGestureRecognizerDelegate extension GroupView: UIScrollViewDelegate, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // 允许 mainScrollView 和内部 tableView 同时识别手势 if otherGestureRecognizer == mainScrollView.panGestureRecognizer { return true } return false } func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == mainScrollView { let offsetY = scrollView.contentOffset.y if offsetY >= stickThreshold { // 到达吸顶位置 → 锁定 mainScrollView,让内部 tableView 接管 scrollState = .ended scrollView.contentOffset.y = stickThreshold scrollView.isScrollEnabled = false selectSegment(at: currentSegmentIndex) } else if offsetY > 0 { scrollState = .scrolling } else { scrollState = .pending scrollView.contentOffset.y = 0 } } else if scrollView == segmentScrollView { // 横向滚动同步 segment 指示器 let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width)) if page != currentSegmentIndex && page >= 0 && page < 2 { currentSegmentIndex = page selectSegment(at: page) } } else { // 内部 tableView 滚动 if scrollState == .scrolling { scrollView.contentOffset = .zero } else if scrollState == .ended { if scrollView.contentOffset.y <= 0 { scrollView.contentOffset.y = 0 scrollState = .pending mainScrollView.isScrollEnabled = true // 把 mainScrollView 降到阈值以下,避免立即重新吸顶 mainScrollView.contentOffset.y = stickThreshold - 20 } } } } } // MARK: - HotGroupCell final class HotGroupCell: UICollectionViewCell { static let reuseId = "HotGroupCell" private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 8 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#F0F0F0") return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 13, weight: .medium) label.textColor = UIColor(hexStr: "#1A1A1A") label.textAlignment = .center return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 11, weight: .regular) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center return label }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) iconView.layoutChain .top().centerX() .width(60).height(60) nameLabel.layoutChain .topToBottomOfView(iconView, offset: 6) .edgesHorzontal() countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 2) .edgesHorzontal() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ item: HotGroupItem) { iconView.image = UIImage(named: item.icon) nameLabel.text = item.name countLabel.text = "\(item.memberCount)人·\(item.onlineCount)在线" } } // MARK: - CircleGroupCell final class CircleGroupCell: UITableViewCell { private let iconView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = 24 iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#F0F0F0") return iv }() private let nameLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#1A1A1A") return label }() private let countLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor(hexStr: "#999999") return label }() private let addressLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor(hexStr: "#999999") return label }() private let dividerLine: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#F0F0F0") return v }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .white contentView.addSubview(iconView) contentView.addSubview(nameLabel) contentView.addSubview(countLabel) contentView.addSubview(addressLabel) contentView.addSubview(dividerLine) iconView.layoutChain .left(16).centerY() .width(48).height(48) nameLabel.layoutChain .top(12).leftToRightOfView(iconView, offset: 12) countLabel.layoutChain .topToBottomOfView(nameLabel, offset: 4) .leftToView(nameLabel) addressLabel.layoutChain .centerY().right(16) dividerLine.layoutChain .left(76).right().bottom().height(0.5) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(_ item: CircleGroupItem) { iconView.image = UIImage(named: item.icon) nameLabel.text = item.name countLabel.text = "\(item.memberCount)人·\(item.onlineCount)在线" addressLabel.text = item.address } }