580 lines
18 KiB
Swift
580 lines
18 KiB
Swift
//
|
||
// GroupView.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/5/29.
|
||
//
|
||
|
||
import UIKit
|
||
import RxSwift
|
||
import RxCocoa
|
||
import SDCycleScrollView
|
||
|
||
// MARK: - PanScrollView(参考 PanScrollView,允许嵌套手势同时识别)
|
||
class PanScrollView: UIScrollView {
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||
return true
|
||
}
|
||
}
|
||
|
||
class GroupView: UIView {
|
||
|
||
var disposeBag = DisposeBag()
|
||
|
||
/// 当前是否吸顶
|
||
private var isSticky = false
|
||
/// 内部 tableview 是否可滚动
|
||
private var isSubCanScroll = false
|
||
|
||
var stickThreshold: CGFloat {
|
||
return segmentView.dl.y
|
||
}
|
||
|
||
// MARK: - Setup
|
||
private func setupUI() {
|
||
addSubview(navBgView)
|
||
addSubview(navBarView)
|
||
navBarView.addSubview(navTitleLabel)
|
||
navBarView.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)
|
||
|
||
navBarView.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)
|
||
|
||
// 创建/加入圈子
|
||
actionButtonsView.layoutChain
|
||
.topToBottomOfView(cycleScrollView, offset: 38)
|
||
.edgesHorzontal()
|
||
|
||
createGroupBtn.layoutChain
|
||
.left(15)
|
||
.edgesVertical()
|
||
.widthToView(joinGroupBtn)
|
||
.height(40)
|
||
|
||
joinGroupBtn.layoutChain
|
||
.topToView(createGroupBtn)
|
||
.bottomToView(createGroupBtn)
|
||
.leftToRightOfView(createGroupBtn, offset: 21)
|
||
.right(15)
|
||
.widthToView(joinGroupBtn)
|
||
|
||
// 热门酷圈
|
||
hotGroupTitleLabel.layoutChain
|
||
.topToBottomOfView(actionButtonsView, offset: 21)
|
||
.left(16)
|
||
|
||
hotGroupsCollectionView.layoutChain
|
||
.topToBottomOfView(hotGroupTitleLabel, offset: 8)
|
||
.edgesHorzontal()
|
||
.height(88)
|
||
|
||
// Segment
|
||
let labelWidth = (UIScreen.main.bounds.width - 60) / 2
|
||
segmentView.layoutChain
|
||
.topToBottomOfView(hotGroupsCollectionView)
|
||
.edgesHorzontal()
|
||
.height(44)
|
||
|
||
createdTabLabel.layoutChain
|
||
.left(30).centerY()
|
||
.width(labelWidth)
|
||
|
||
joinedTabLabel.layoutChain
|
||
.right(30).centerY()
|
||
.width(labelWidth)
|
||
|
||
tabIndicator.layoutChain
|
||
.bottom().centerX(createdTabLabel)
|
||
.width(14).height(3)
|
||
|
||
// 横向 segmentScrollView
|
||
segmentScrollView.layoutChain
|
||
.topToBottomOfView(segmentView)
|
||
.edgesHorzontal()
|
||
.bottom()
|
||
.height(kScreenHeight - kNaviHeight - 44)
|
||
|
||
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)
|
||
|
||
}
|
||
|
||
// MARK: - Nav
|
||
lazy var navBgView: UIImageView = {
|
||
let iv = UIImageView()
|
||
iv.image = UIImage(named: "Common/navBar_bg_1")
|
||
iv.contentMode = .scaleAspectFill
|
||
return iv
|
||
}()
|
||
|
||
lazy var navBarView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .clear
|
||
return view
|
||
}()
|
||
|
||
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: PanScrollView = {
|
||
let sv = PanScrollView()
|
||
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 = ["Mask_group", "Mask_group", "Mask_group"]
|
||
view.clipsToBounds = false
|
||
view.pageControlBottomOffset = -34
|
||
return view
|
||
}()
|
||
|
||
// MARK: - 创建 / 加入 圈子
|
||
private lazy var actionButtonsView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = .clear
|
||
return v
|
||
}()
|
||
|
||
lazy var createGroupBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setTitle("创建圈子", for: .normal)
|
||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||
btn.cornerRadius = 20
|
||
return btn
|
||
}()
|
||
|
||
lazy var joinGroupBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setTitle("加入圈子", for: .normal)
|
||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||
btn.cornerRadius = 20
|
||
return btn
|
||
}()
|
||
|
||
// MARK: - 热门酷圈
|
||
private lazy var hotGroupTitleLabel: UILabel = {
|
||
let label = UILabel()
|
||
label.text = "热门酷圈"
|
||
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||
return label
|
||
}()
|
||
|
||
private(set) lazy var hotGroupsCollectionView: UICollectionView = {
|
||
let layout = UICollectionViewFlowLayout()
|
||
layout.scrollDirection = .horizontal
|
||
layout.itemSize = CGSize(width: 75, height: 88)
|
||
layout.minimumLineSpacing = 10
|
||
layout.sectionInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18)
|
||
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
|
||
|
||
let line = UIView()
|
||
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
||
v.addSubview(line)
|
||
line.layoutChain
|
||
.edgesHorzontal()
|
||
.height(1)
|
||
.bottom(4)
|
||
|
||
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: - 横向滚动容器 (PanScrollView)
|
||
private(set) lazy var segmentScrollView: PanScrollView = {
|
||
let sv = PanScrollView()
|
||
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: - 两个列表
|
||
private(set) lazy var createdTableView: UITableView = {
|
||
let tv = UITableView(frame: .zero, style: .plain)
|
||
tv.backgroundColor = .clear
|
||
tv.separatorStyle = .none
|
||
tv.showsVerticalScrollIndicator = false
|
||
tv.register(CircleGroupCell.self)
|
||
tv.rowHeight = 72
|
||
tv.bounces = true
|
||
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
||
return tv
|
||
}()
|
||
|
||
private(set) lazy var joinedTableView: UITableView = {
|
||
let tv = UITableView(frame: .zero, style: .plain)
|
||
tv.backgroundColor = .clear
|
||
tv.separatorStyle = .none
|
||
tv.showsVerticalScrollIndicator = false
|
||
tv.register(CircleGroupCell.self)
|
||
tv.rowHeight = 72
|
||
tv.bounces = true
|
||
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
||
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")
|
||
}
|
||
}
|
||
|
||
// MARK: - UIScrollViewDelegate(参考 PanView + PanSubItemView 通知模式)
|
||
extension GroupView: UIScrollViewDelegate {
|
||
|
||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||
if scrollView == mainScrollView {
|
||
handleMainScroll(scrollView)
|
||
} else if scrollView == segmentScrollView {
|
||
let page = Int(round(scrollView.contentOffset.x / scrollView.bounds.width))
|
||
if page != currentSegmentIndex, page >= 0, page < 2 {
|
||
currentSegmentIndex = page
|
||
selectSegment(at: page)
|
||
}
|
||
}
|
||
}
|
||
|
||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||
guard scrollView != mainScrollView, scrollView != segmentScrollView else { return }
|
||
if scrollView.contentOffset.y > 0 {
|
||
isSubCanScroll = true
|
||
}
|
||
}
|
||
|
||
/// 由 GroupViewController 通过 rx.didScroll 转发调用
|
||
func handleTableViewScroll(_ scrollView: UIScrollView) {
|
||
if isSubCanScroll {
|
||
if scrollView.contentOffset.y <= 0 {
|
||
isSubCanScroll = false
|
||
isSticky = false
|
||
scrollView.contentOffset.y = 0
|
||
}
|
||
} else {
|
||
scrollView.contentOffset.y = 0
|
||
}
|
||
}
|
||
|
||
// MARK: - 主 scroll 逻辑
|
||
private func handleMainScroll(_ scrollView: UIScrollView) {
|
||
if isSubCanScroll {
|
||
scrollView.contentOffset.y = stickThreshold
|
||
isSticky = true
|
||
selectSegment(at: currentSegmentIndex)
|
||
} else {
|
||
let offsetY = scrollView.contentOffset.y
|
||
if offsetY >= stickThreshold {
|
||
scrollView.contentOffset.y = stickThreshold
|
||
isSticky = true
|
||
isSubCanScroll = true
|
||
selectSegment(at: currentSegmentIndex)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - HotGroupCell
|
||
final class HotGroupCell: UICollectionViewCell {
|
||
static let reuseId = "HotGroupCell"
|
||
|
||
private let iconView: UIImageView = {
|
||
let iv = UIImageView()
|
||
iv.contentMode = .scaleAspectFill
|
||
iv.layer.cornerRadius = 10
|
||
iv.clipsToBounds = true
|
||
iv.backgroundColor = .clear
|
||
return iv
|
||
}()
|
||
|
||
private let nameLabel: UILabel = {
|
||
let label = UILabel()
|
||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||
label.textAlignment = .center
|
||
return label
|
||
}()
|
||
|
||
private let countLabel: UILabel = {
|
||
let label = UILabel()
|
||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||
label.textColor = ThemeManager.shared.color.contentColor
|
||
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(50).height(50)
|
||
|
||
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
|
||
}
|
||
}
|