576 lines
18 KiB
Swift
576 lines
18 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|