// // MemberAnnotationView.swift // QuickLocation // import Foundation #if !targetEnvironment(simulator) import MAMapKit final class MemberAnnotationView: MAAnnotationView { // MARK: - Design Constants static let avatarOuterSize: CGFloat = 50 static let avatarInnerSize: CGFloat = 48 static let nameTagWidth: CGFloat = 60 static let nameTagHeight: CGFloat = 20 static let totalHeight: CGFloat = avatarOuterSize + 4 + nameTagHeight static let radarSize: CGFloat = 180 private let containerView: UIView = { let v = UIView() v.backgroundColor = .clear return v }() private let avatarOuterCircle: UIView = { let v = UIView() v.backgroundColor = .white v.layer.cornerRadius = avatarOuterSize / 2 v.layer.shadowColor = UIColor.black.withAlphaComponent(0.15).cgColor v.layer.shadowOffset = CGSize(width: 0, height: 2) v.layer.shadowRadius = 4 v.layer.shadowOpacity = 1 return v }() private let avatarImageView: UIImageView = { let iv = UIImageView() iv.contentMode = .scaleAspectFill iv.layer.cornerRadius = avatarInnerSize / 2 iv.layer.borderWidth = 1 iv.layer.borderColor = UIColor.white.cgColor iv.clipsToBounds = true iv.backgroundColor = UIColor(hexStr: "#E0E0E0") return iv }() private lazy var nameTagView: UIView = { let v = UIView() v.backgroundColor = .white v.layer.cornerRadius = 4 v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor v.layer.shadowOffset = CGSize(width: 0, height: 1) v.layer.shadowRadius = 2 v.layer.shadowOpacity = 1 return v }() private lazy var 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 }() // MARK: - Layers private var headingLayer: CAShapeLayer? private var pulseLayer: CAShapeLayer? private var isCurrentUser: Bool = false // 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(avatarOuterCircle) avatarOuterCircle.addSubview(avatarImageView) containerView.addSubview(nameTagView) nameTagView.addSubview(nameLabel) } // MARK: - Configure func configure(with member: CircleMember) { avatarImageView.image = UIImage(named: "UserIcon/\(member.avatar)") avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear nameLabel.text = member.name isCurrentUser = member.isCurrentUser if member.isCurrentUser { configureAsCurrentUser(heading: member.heading) } else { configureAsMember() } } private func configureAsCurrentUser(heading: Double) { nameTagView.isHidden = true stopPulseAnimation() let size = Self.radarSize bounds = CGRect(x: 0, y: 0, width: size, height: size) centerOffset = .zero containerView.frame = bounds let avatarX = (size - Self.avatarOuterSize) / 2 let avatarY = (size - Self.avatarOuterSize) / 2 avatarOuterCircle.frame = CGRect(x: avatarX, y: avatarY, width: Self.avatarOuterSize, height: Self.avatarOuterSize) avatarImageView.frame = CGRect( x: (Self.avatarOuterSize - Self.avatarInnerSize) / 2, y: (Self.avatarOuterSize - Self.avatarInnerSize) / 2, width: Self.avatarInnerSize, height: Self.avatarInnerSize ) addHeadingIndicator(heading: heading) } private func configureAsMember() { nameTagView.isHidden = false headingLayer?.removeFromSuperlayer() headingLayer = nil stopPulseAnimation() // 动态宽度 let text = nameLabel.text ?? "" let textWidth = (text as NSString).size(withAttributes: [.font: nameLabel.font as Any]).width let tagWidth = min(max(textWidth + 16, 44), 120) let totalWidth = max(tagWidth, Self.avatarOuterSize) let tagTop: CGFloat = 0 let avatarTop = Self.nameTagHeight + 4 let totalHeight = avatarTop + Self.avatarOuterSize bounds = CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight) containerView.frame = bounds // avatar 中心对准 annotation 坐标 let avatarCenterY = avatarTop + Self.avatarOuterSize / 2 centerOffset = CGPoint(x: 0, y: avatarCenterY - totalHeight / 2) // nameTag 在头像上面 nameTagView.frame = CGRect( x: (totalWidth - tagWidth) / 2, y: tagTop, width: tagWidth, height: Self.nameTagHeight ) nameLabel.frame = nameTagView.bounds.insetBy(dx: 6, dy: 0) // 头像在 nameTag 下面 avatarOuterCircle.frame = CGRect( x: (totalWidth - Self.avatarOuterSize) / 2, y: avatarTop, width: Self.avatarOuterSize, height: Self.avatarOuterSize ) avatarImageView.frame = CGRect( x: (Self.avatarOuterSize - Self.avatarInnerSize) / 2, y: (Self.avatarOuterSize - Self.avatarInnerSize) / 2, width: Self.avatarInnerSize, height: Self.avatarInnerSize ) } // MARK: - Heading func updateHeading(_ heading: Double) { headingLayer?.removeFromSuperlayer() headingLayer = nil addHeadingIndicator(heading: heading) } private func addHeadingIndicator(heading: Double) { headingLayer?.removeFromSuperlayer() let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) let radius: CGFloat = 80 let halfFan = 30.0 * .pi / 180 let centerAngle = CGFloat(heading) * .pi / 180 - .pi / 2 let startAngle = centerAngle - halfFan let endAngle = centerAngle + halfFan let path = UIBezierPath() path.move(to: center) path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) path.close() let shapeLayer = CAShapeLayer() shapeLayer.frame = containerView.bounds shapeLayer.path = path.cgPath shapeLayer.fillColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.25).cgColor shapeLayer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.4).cgColor shapeLayer.lineWidth = 1 containerView.layer.insertSublayer(shapeLayer, at: 0) headingLayer = shapeLayer } // MARK: - Pulse Animation private func startPulseAnimation() { guard pulseLayer == nil else { return } let layer = CAShapeLayer() layer.fillColor = UIColor.clear.cgColor layer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.5).cgColor layer.lineWidth = 2 layer.frame = bounds containerView.layer.insertSublayer(layer, at: 0) pulseLayer = layer let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) let radius = Self.avatarOuterSize / 2 + 8 let ringRect = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2) layer.path = UIBezierPath(ovalIn: ringRect).cgPath let scale = CABasicAnimation(keyPath: "transform.scale") scale.fromValue = 0.9 scale.toValue = 1.5 let opacity = CABasicAnimation(keyPath: "opacity") opacity.fromValue = 0.8 opacity.toValue = 0.0 let group = CAAnimationGroup() group.animations = [scale, opacity] group.duration = 1.6 group.repeatCount = .infinity group.timingFunction = CAMediaTimingFunction(name: .easeOut) layer.add(group, forKey: "pulse") } private func stopPulseAnimation() { pulseLayer?.removeAllAnimations() pulseLayer?.removeFromSuperlayer() pulseLayer = nil } } #endif