// // 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 = 12 private static let pointerHeight: CGFloat = 7 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 pointerLayer: CAShapeLayer = { let layer = CAShapeLayer() layer.fillColor = UIColor.white.cgColor return layer }() 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 addSubview(containerView) containerView.addSubview(nameTagView) nameTagView.addSubview(nameLabel) containerView.layer.addSublayer(pointerLayer) containerView.addSubview(bubbleView) bubbleView.addSubview(avatarImageView) containerView.addSubview(dotOuterView) dotOuterView.addSubview(dotInnerView) dotInnerView.addSubview(dotCenterView) } // MARK: - Configure func configure(with member: CircleMember) { isEnabled = 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 let pointerPath = UIBezierPath() let leftX = (totalWidth - Self.pointerWidth) / 2 pointerPath.move(to: CGPoint(x: leftX, y: pointerTop)) pointerPath.addLine(to: CGPoint(x: leftX + Self.pointerWidth, y: pointerTop)) pointerPath.addLine(to: CGPoint(x: totalWidth / 2, y: pointerTop + pointerH)) pointerPath.close() pointerLayer.path = pointerPath.cgPath let dotY = bubbleY + bubble + pointerH + 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