270 lines
8.7 KiB
Swift
270 lines
8.7 KiB
Swift
//
|
||
// MemberAnnotationView.swift
|
||
// QuickLocation
|
||
//
|
||
|
||
import Foundation
|
||
#if !targetEnvironment(simulator)
|
||
import AMapNaviKit
|
||
|
||
final class MemberAnnotationView: MAAnnotationView {
|
||
|
||
/// 标注被点击的回调
|
||
var onTap: ((MemberAnnotation) -> Void)?
|
||
|
||
// MARK: - Design Constants
|
||
private static let bubbleSize: CGFloat = 56
|
||
private static let bubbleCorner: CGFloat = 14
|
||
private static let borderWidth: CGFloat = 4
|
||
private static let pointerWidth: CGFloat = 14
|
||
private static let pointerHeight: CGFloat = 6
|
||
private static let gapBubbleToDot: CGFloat = 4
|
||
private static let gapNameToBubble: CGFloat = 4
|
||
private static let nameTagHeight: CGFloat = 20
|
||
private static let nameTagHPadding: CGFloat = 8
|
||
private static let dotOuterSize: CGFloat = 12
|
||
private static let dotInnerSize: CGFloat = 10
|
||
|
||
private static let currentUserDotColor = UIColor(hexStr: "#5ED4FF")
|
||
private static let memberDotPalette: [UIColor] = [
|
||
UIColor(hexStr: "#FF6B6B"),
|
||
UIColor(hexStr: "#FF9F43"),
|
||
UIColor(hexStr: "#FECA57"),
|
||
UIColor(hexStr: "#1DD1A1"),
|
||
UIColor(hexStr: "#54A0FF"),
|
||
UIColor(hexStr: "#5F27CD"),
|
||
UIColor(hexStr: "#FF9FF3"),
|
||
UIColor(hexStr: "#00D2D3"),
|
||
UIColor(hexStr: "#EE5A24"),
|
||
UIColor(hexStr: "#10AC84"),
|
||
UIColor(hexStr: "#2E86DE"),
|
||
UIColor(hexStr: "#341F97"),
|
||
]
|
||
|
||
private var showsNameTag = false
|
||
|
||
private let containerView = UIView()
|
||
|
||
private let nameTagView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = .white
|
||
v.layer.cornerRadius = 8
|
||
v.layer.shadowColor = UIColor.black.cgColor
|
||
v.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||
v.layer.shadowRadius = 2
|
||
v.layer.shadowOpacity = 0.12
|
||
v.isHidden = true
|
||
return v
|
||
}()
|
||
|
||
private let nameLabel: UILabel = {
|
||
let l = UILabel()
|
||
l.font = UIFont(name: "PingFangSC-Medium", size: 11)
|
||
?? UIFont.systemFont(ofSize: 11, weight: .medium)
|
||
l.textColor = UIColor(hexStr: "#0F2846")
|
||
l.textAlignment = .center
|
||
l.lineBreakMode = .byTruncatingTail
|
||
return l
|
||
}()
|
||
|
||
private let bubbleView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = .white
|
||
v.layer.cornerRadius = bubbleCorner
|
||
v.layer.shadowColor = UIColor.black.cgColor
|
||
v.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||
v.layer.shadowRadius = 4
|
||
v.layer.shadowOpacity = 0.18
|
||
return v
|
||
}()
|
||
|
||
private let avatarImageView: UIImageView = {
|
||
let iv = UIImageView()
|
||
iv.contentMode = .scaleAspectFill
|
||
iv.clipsToBounds = true
|
||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||
iv.layer.cornerRadius = bubbleCorner - borderWidth * 0.6
|
||
return iv
|
||
}()
|
||
|
||
private let pointerView: UIImageView = {
|
||
let iv = UIImageView(image: UIImage(named: "Map/arrow"))
|
||
iv.contentMode = .scaleAspectFit
|
||
return iv
|
||
}()
|
||
|
||
private let dotOuterView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = .white
|
||
v.layer.cornerRadius = dotOuterSize / 2
|
||
v.layer.shadowColor = UIColor.black.cgColor
|
||
v.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||
v.layer.shadowRadius = 2
|
||
v.layer.shadowOpacity = 0.2
|
||
return v
|
||
}()
|
||
|
||
private let dotInnerView: UIView = {
|
||
let v = UIView()
|
||
v.layer.cornerRadius = dotInnerSize / 2
|
||
return v
|
||
}()
|
||
|
||
private let dotCenterView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = .white
|
||
v.layer.cornerRadius = 2
|
||
return v
|
||
}()
|
||
|
||
// MARK: - Init
|
||
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
|
||
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||
setupUI()
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
private func setupUI() {
|
||
canShowCallout = false
|
||
isEnabled = true
|
||
isUserInteractionEnabled = true
|
||
|
||
addSubview(containerView)
|
||
containerView.isUserInteractionEnabled = true
|
||
containerView.addSubview(nameTagView)
|
||
nameTagView.addSubview(nameLabel)
|
||
containerView.addSubview(bubbleView)
|
||
bubbleView.addSubview(avatarImageView)
|
||
containerView.addSubview(pointerView)
|
||
containerView.addSubview(dotOuterView)
|
||
dotOuterView.addSubview(dotInnerView)
|
||
dotInnerView.addSubview(dotCenterView)
|
||
}
|
||
|
||
// MARK: - Configure
|
||
func configure(with member: CircleMember) {
|
||
isEnabled = true
|
||
isUserInteractionEnabled = true
|
||
canShowCallout = false
|
||
|
||
if let img = UIImage(named: "UserIcon/\(member.avatar)") {
|
||
avatarImageView.image = img
|
||
} else {
|
||
avatarImageView.image = UIImage(named: "Common/default_avatar")
|
||
}
|
||
avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear
|
||
|
||
dotInnerView.backgroundColor = Self.dotColor(for: member)
|
||
|
||
showsNameTag = !member.isCurrentUser
|
||
nameLabel.text = member.name
|
||
nameTagView.isHidden = !showsNameTag
|
||
|
||
// 所有标注(含当前用户)可点击,同步列表选中
|
||
if gestureRecognizers?.isEmpty ?? true {
|
||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||
addGestureRecognizer(tap)
|
||
}
|
||
|
||
layoutMarker()
|
||
}
|
||
|
||
@objc private func handleTap() {
|
||
guard let ann = annotation as? MemberAnnotation else { return }
|
||
onTap?(ann)
|
||
}
|
||
|
||
/// 朝向扇形已从图示样式中移除,保留 API 兼容 HomeViewController
|
||
func updateHeading(_ heading: Double) {}
|
||
|
||
// MARK: - Layout
|
||
private func layoutMarker() {
|
||
let bubble = Self.bubbleSize
|
||
let pointerH = Self.pointerHeight
|
||
let gap = Self.gapBubbleToDot
|
||
let dot = Self.dotOuterSize
|
||
|
||
let nameH: CGFloat = showsNameTag ? Self.nameTagHeight : 0
|
||
let nameGap: CGFloat = showsNameTag ? Self.gapNameToBubble : 0
|
||
|
||
var nameTagWidth: CGFloat = 0
|
||
if showsNameTag {
|
||
let text = nameLabel.text ?? ""
|
||
let textWidth = (text as NSString).size(withAttributes: [.font: nameLabel.font as Any]).width
|
||
nameTagWidth = min(max(textWidth + Self.nameTagHPadding * 2, 44), 120)
|
||
}
|
||
|
||
let totalWidth = max(bubble, nameTagWidth)
|
||
let totalHeight = nameH + nameGap + bubble + pointerH + gap + dot
|
||
|
||
bounds = CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight)
|
||
containerView.frame = bounds
|
||
|
||
// 色点中心对准 annotation 坐标(负 y 将视图上移)
|
||
centerOffset = CGPoint(x: 0, y: -(totalHeight / 2 - Self.dotOuterSize / 2))
|
||
|
||
if showsNameTag {
|
||
nameTagView.frame = CGRect(
|
||
x: (totalWidth - nameTagWidth) / 2,
|
||
y: 0,
|
||
width: nameTagWidth,
|
||
height: Self.nameTagHeight
|
||
)
|
||
nameLabel.frame = nameTagView.bounds.insetBy(dx: Self.nameTagHPadding - 2, dy: 0)
|
||
}
|
||
|
||
let bubbleY = nameH + nameGap
|
||
bubbleView.frame = CGRect(
|
||
x: (totalWidth - bubble) / 2,
|
||
y: bubbleY,
|
||
width: bubble,
|
||
height: bubble
|
||
)
|
||
let inner = bubble - Self.borderWidth * 2
|
||
avatarImageView.frame = CGRect(
|
||
x: Self.borderWidth, y: Self.borderWidth,
|
||
width: inner, height: inner
|
||
)
|
||
|
||
// 箭头:贴气泡底边居中
|
||
let pointerTop = bubbleY + bubble - 1
|
||
pointerView.frame = CGRect(
|
||
x: (totalWidth - Self.pointerWidth) / 2,
|
||
y: pointerTop,
|
||
width: Self.pointerWidth,
|
||
height: Self.pointerHeight
|
||
)
|
||
|
||
let dotY = bubbleY + bubble + Self.pointerHeight + gap
|
||
dotOuterView.frame = CGRect(
|
||
x: (totalWidth - dot) / 2, y: dotY,
|
||
width: dot, height: dot
|
||
)
|
||
let inset = (Self.dotOuterSize - Self.dotInnerSize) / 2
|
||
dotInnerView.frame = CGRect(
|
||
x: inset, y: inset,
|
||
width: Self.dotInnerSize, height: Self.dotInnerSize
|
||
)
|
||
let centerSize: CGFloat = 4
|
||
let centerInset = (Self.dotInnerSize - centerSize) / 2
|
||
dotCenterView.frame = CGRect(
|
||
x: centerInset, y: centerInset,
|
||
width: centerSize, height: centerSize
|
||
)
|
||
}
|
||
|
||
// MARK: - Dot Color
|
||
private static func dotColor(for member: CircleMember) -> UIColor {
|
||
if member.isCurrentUser {
|
||
return currentUserDotColor
|
||
}
|
||
let hash = member.id.utf8.reduce(0) { ($0 &* 31) &+ Int($1) }
|
||
let index = abs(hash) % memberDotPalette.count
|
||
return memberDotPalette[index]
|
||
}
|
||
}
|
||
#endif
|