373 lines
14 KiB
Swift
373 lines
14 KiB
Swift
//
|
|
// GroupMemberListView.swift
|
|
// QuickLocation
|
|
//
|
|
// Created by 八条 on 2026/6/29.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
final class GroupMemberListView: UIView {
|
|
|
|
private var sectionTitles: [String] = []
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#F7F7F8")
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func updateSectionTitles(_ titles: [String]) {
|
|
sectionTitles = titles
|
|
}
|
|
|
|
func updateSearchPlaceholder(isHidden: Bool) {
|
|
searchPlaceholderStack.isHidden = isHidden
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(navView)
|
|
addSubview(searchField)
|
|
addSubview(tableView)
|
|
searchField.addSubview(searchPlaceholderStack)
|
|
|
|
[navView, searchField, tableView, searchPlaceholderStack].forEach {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
NSLayoutConstraint.activate([
|
|
navView.topAnchor.constraint(equalTo: topAnchor),
|
|
navView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
navView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
navView.heightAnchor.constraint(equalToConstant: kNaviHeight),
|
|
|
|
searchField.topAnchor.constraint(equalTo: navView.bottomAnchor, constant: 20),
|
|
searchField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
|
|
searchField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -15),
|
|
searchField.heightAnchor.constraint(equalToConstant: 38),
|
|
|
|
searchPlaceholderStack.centerXAnchor.constraint(equalTo: searchField.centerXAnchor),
|
|
searchPlaceholderStack.centerYAnchor.constraint(equalTo: searchField.centerYAnchor),
|
|
|
|
tableView.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 20),
|
|
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
])
|
|
}
|
|
|
|
lazy var navView: BaseNavigationView = BaseNavigationView(title: "圈子成员")
|
|
|
|
lazy var searchField: UISearchTextField = {
|
|
let textField = UISearchTextField()
|
|
textField.backgroundColor = .white
|
|
textField.borderStyle = .none
|
|
textField.layer.cornerRadius = 12
|
|
textField.clipsToBounds = true
|
|
textField.font = .systemFont(ofSize: 15, weight: .regular)
|
|
textField.textColor = UIColor(hexStr: "#293445")
|
|
textField.tintColor = UIColor(hexStr: "#293445")
|
|
textField.returnKeyType = .search
|
|
textField.clearButtonMode = .whileEditing
|
|
textField.leftView = UIView(frame: .zero)
|
|
textField.leftViewMode = .never
|
|
return textField
|
|
}()
|
|
|
|
private lazy var searchPlaceholderStack: UIStackView = {
|
|
let imageView = UIImageView(image: UIImage(named: "Group/remove_member_search"))
|
|
imageView.contentMode = .scaleAspectFit
|
|
imageView.widthAnchor.constraint(equalToConstant: 16).isActive = true
|
|
imageView.heightAnchor.constraint(equalToConstant: 16).isActive = true
|
|
|
|
let label = UILabel()
|
|
label.text = "搜索"
|
|
label.textColor = UIColor(hexStr: "#B0B0B5")
|
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
|
|
|
let stack = UIStackView(arrangedSubviews: [imageView, label])
|
|
stack.axis = .horizontal
|
|
stack.alignment = .center
|
|
stack.spacing = 6
|
|
stack.isUserInteractionEnabled = false
|
|
return stack
|
|
}()
|
|
|
|
lazy var tableView: UITableView = {
|
|
let view = UITableView(frame: .zero, style: .plain)
|
|
view.backgroundColor = .clear
|
|
view.separatorStyle = .none
|
|
view.rowHeight = GroupMemberDirectoryCell.rowHeight
|
|
view.estimatedRowHeight = GroupMemberDirectoryCell.rowHeight
|
|
view.sectionHeaderHeight = 42
|
|
view.estimatedSectionHeaderHeight = 42
|
|
view.showsVerticalScrollIndicator = false
|
|
view.keyboardDismissMode = .onDrag
|
|
view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin + 24, right: 0)
|
|
if #available(iOS 15.0, *) {
|
|
view.sectionHeaderTopPadding = 0
|
|
}
|
|
view.register(GroupMemberDirectoryCell.self)
|
|
return view
|
|
}()
|
|
}
|
|
|
|
extension GroupMemberListView: UITableViewDelegate {
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
guard section < sectionTitles.count else { return nil }
|
|
|
|
let container = UIView()
|
|
let label = UILabel()
|
|
label.text = sectionTitles[section]
|
|
label.textColor = UIColor(hexStr: "#777A80")
|
|
label.font = .systemFont(ofSize: 14, weight: .regular)
|
|
container.addSubview(label)
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8)
|
|
])
|
|
return container
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
42
|
|
}
|
|
}
|
|
|
|
final class GroupMemberDirectoryCell: UITableViewCell {
|
|
|
|
static let rowHeight: CGFloat = 52
|
|
|
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
selectionStyle = .none
|
|
backgroundColor = .clear
|
|
contentView.backgroundColor = .clear
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(_ member: GroupMemberModel,
|
|
showsSeparator: Bool,
|
|
roundsTopCorners: Bool,
|
|
roundsBottomCorners: Bool) {
|
|
avatarView.image = member.userIcon
|
|
nicknameLabel.text = member.nick_name.isEmpty ? "未知昵称" : member.nick_name
|
|
separatorView.isHidden = !showsSeparator
|
|
|
|
var corners: CACornerMask = []
|
|
if roundsTopCorners {
|
|
corners.formUnion([.layerMinXMinYCorner, .layerMaxXMinYCorner])
|
|
}
|
|
if roundsBottomCorners {
|
|
corners.formUnion([.layerMinXMaxYCorner, .layerMaxXMaxYCorner])
|
|
}
|
|
cardView.layer.maskedCorners = corners
|
|
}
|
|
|
|
private func setupUI() {
|
|
contentView.addSubview(cardView)
|
|
cardView.addSubview(avatarView)
|
|
cardView.addSubview(nicknameLabel)
|
|
cardView.addSubview(separatorView)
|
|
|
|
[cardView, avatarView, nicknameLabel, separatorView].forEach {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
NSLayoutConstraint.activate([
|
|
cardView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
|
|
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
|
|
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
|
|
avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 16),
|
|
avatarView.centerYAnchor.constraint(equalTo: cardView.centerYAnchor),
|
|
avatarView.widthAnchor.constraint(equalToConstant: 38),
|
|
avatarView.heightAnchor.constraint(equalToConstant: 38),
|
|
|
|
nicknameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
|
|
nicknameLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -16),
|
|
nicknameLabel.centerYAnchor.constraint(equalTo: cardView.centerYAnchor),
|
|
|
|
separatorView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 20),
|
|
separatorView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -13),
|
|
separatorView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor),
|
|
separatorView.heightAnchor.constraint(equalToConstant: 0.5)
|
|
])
|
|
}
|
|
|
|
private lazy var cardView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.layer.cornerRadius = 12
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var avatarView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
|
view.contentMode = .scaleAspectFill
|
|
view.layer.cornerRadius = 10
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var nicknameLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.textColor = UIColor(hexStr: "#111111")
|
|
label.font = .systemFont(ofSize: 16, weight: .regular)
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}()
|
|
|
|
private lazy var separatorView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor(hexStr: "#EDEEF0")
|
|
return view
|
|
}()
|
|
}
|
|
|
|
// This compact cell is shared by the map and member information pages.
|
|
final class GroupMemberListCell: UICollectionViewCell {
|
|
|
|
private static let sosBlinkKey = "sosBlink"
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
super.prepareForReuse()
|
|
updateSOSBlink(false)
|
|
bubbleImageView.isHidden = true
|
|
}
|
|
|
|
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
|
|
avatarImageView.image = model.userIcon
|
|
nameLabel.text = model.nick_name
|
|
nameLabel.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
|
selectionImageView.isHidden = !isSelected
|
|
vipBackgroundView.isHidden = model.level == 1
|
|
|
|
let isSOS = model.status == 1
|
|
updateSOSBlink(isSOS)
|
|
bubbleImageView.isHidden = isSOS || model.status != 2
|
|
}
|
|
|
|
private func updateSOSBlink(_ blinking: Bool) {
|
|
sosImageView.layer.removeAnimation(forKey: Self.sosBlinkKey)
|
|
sosImageView.isHidden = !blinking
|
|
sosImageView.alpha = 1
|
|
guard blinking else { return }
|
|
|
|
let animation = CABasicAnimation(keyPath: "opacity")
|
|
animation.fromValue = 1
|
|
animation.toValue = 0.2
|
|
animation.duration = 0.6
|
|
animation.autoreverses = true
|
|
animation.repeatCount = .infinity
|
|
sosImageView.layer.add(animation, forKey: Self.sosBlinkKey)
|
|
}
|
|
|
|
private func setupUI() {
|
|
contentView.addSubview(selectionImageView)
|
|
contentView.addSubview(avatarImageView)
|
|
contentView.addSubview(bubbleImageView)
|
|
contentView.addSubview(sosImageView)
|
|
contentView.addSubview(vipBackgroundView)
|
|
contentView.addSubview(nameLabel)
|
|
|
|
[selectionImageView, avatarImageView, bubbleImageView, sosImageView, vipBackgroundView, nameLabel].forEach {
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
NSLayoutConstraint.activate([
|
|
selectionImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
selectionImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
selectionImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
selectionImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
|
|
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 9),
|
|
avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
|
avatarImageView.widthAnchor.constraint(equalToConstant: 50),
|
|
avatarImageView.heightAnchor.constraint(equalToConstant: 50),
|
|
|
|
bubbleImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
|
bubbleImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
|
|
bubbleImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
|
bubbleImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
|
|
|
sosImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
|
|
sosImageView.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
|
|
sosImageView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
|
|
sosImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
|
|
|
|
vipBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7),
|
|
vipBackgroundView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
|
|
|
nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 7),
|
|
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
|
])
|
|
}
|
|
|
|
private lazy var selectionImageView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg"))
|
|
view.contentMode = .scaleAspectFill
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var avatarImageView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.backgroundColor = UIColor(hexStr: "#F1F2F4")
|
|
view.contentMode = .scaleAspectFill
|
|
view.layer.cornerRadius = 25
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var bubbleImageView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "GroupMemberList/bubble"))
|
|
view.contentMode = .scaleAspectFit
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var sosImageView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "GroupMemberList/sos"))
|
|
view.contentMode = .scaleAspectFit
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var vipBackgroundView: UIImageView = {
|
|
let view = UIImageView(image: UIImage(named: "GroupMemberList/vip"))
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var nameLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
label.textAlignment = .center
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}()
|
|
}
|