jsdw_ios/QuickLocation/Section/Map/MemberAnnotationView.swift

362 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MemberAnnotationView.swift
// QuickLocation
//
import Foundation
#if !targetEnvironment(simulator)
import AMapNaviKit
import Lottie
final class MemberAnnotationView: MAAnnotationView {
///
var onTap: ((MemberAnnotation) -> Void)?
// MARK: - Design Constants
private static let bubbleSize: CGFloat = 66
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 moodSize: CGFloat = 36
private static let gapMoodToName: CGFloat = 2
private static let dotOuterSize: CGFloat = 15
private static let dotInnerSize: CGFloat = 12
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 var showsMood = false
private var playingMoodName: String?
private var finishedMoodName: String?
private let containerView = UIView()
private let moodView: LottieAnimationView = {
let view = LottieAnimationView()
view.contentMode = .scaleAspectFit
view.loopMode = .playOnce
view.backgroundBehavior = .pauseAndRestore
view.isHidden = true
view.isUserInteractionEnabled = false
return view
}()
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 = 3
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
// image bounds 1px SDK bounds
image = Self.transparentPixel
addSubview(containerView)
containerView.isUserInteractionEnabled = true
containerView.addSubview(moodView)
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()
}
func setMood(index: Int, selected: Bool) {
let item = selected ? MoodStatusCatalog.item(moodIndex: index) : nil
guard let item, let path = item.jsonPath else {
stopMood()
finishedMoodName = nil
layoutMarker()
return
}
if finishedMoodName == item.jsonName {
layoutMarker()
return
}
if playingMoodName == item.jsonName, moodView.animation != nil {
layoutMarker()
return
}
showsMood = true
moodView.isHidden = false
playingMoodName = item.jsonName
moodView.stop()
moodView.animation = LottieAnimation.filepath(path)
moodView.currentProgress = 0
moodView.play { [weak self] completed in
guard let self, completed else { return }
self.finishedMoodName = item.jsonName
self.stopMood()
self.layoutMarker()
}
layoutMarker()
}
private func stopMood() {
showsMood = false
playingMoodName = nil
moodView.stop()
moodView.animation = nil
moodView.isHidden = true
}
override func prepareForReuse() {
super.prepareForReuse()
showsNameTag = false
nameTagView.isHidden = true
nameLabel.text = nil
onTap = nil
finishedMoodName = nil
stopMood()
layoutMarker()
}
/// didAdd bounds/centerOffset layout
func applyAnchor() {
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 moodH: CGFloat = showsMood ? Self.moodSize : 0
let moodGap: CGFloat = showsMood ? Self.gapMoodToName : 0
let nameH: CGFloat = showsNameTag ? Self.nameTagHeight : 0
let nameGap: CGFloat = showsNameTag ? Self.gapNameToBubble : (showsMood ? 2 : 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, showsMood ? Self.moodSize : 0)
let totalHeight = moodH + moodGap + nameH + nameGap + bubble + pointerH + gap + dot
bounds = CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight)
containerView.frame = bounds
centerOffset = CGPoint(x: 0, y: -(totalHeight / 2 - Self.dotOuterSize / 2))
if showsMood {
moodView.frame = CGRect(
x: (totalWidth - Self.moodSize) / 2,
y: 0,
width: Self.moodSize,
height: Self.moodSize
)
}
if showsNameTag {
nameTagView.frame = CGRect(
x: (totalWidth - nameTagWidth) / 2,
y: moodH + moodGap,
width: nameTagWidth,
height: Self.nameTagHeight
)
nameLabel.frame = nameTagView.bounds.insetBy(dx: Self.nameTagHPadding - 2, dy: 0)
}
let bubbleY = moodH + moodGap + 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 = 6
let centerInset = (Self.dotInnerSize - centerSize) / 2
dotCenterView.frame = CGRect(
x: centerInset, y: centerInset,
width: centerSize, height: centerSize
)
}
// MARK: - Dot Color
private static let transparentPixel: UIImage = {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0)
UIColor.clear.setFill()
UIRectFill(CGRect(x: 0, y: 0, width: 1, height: 1))
let img = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
UIGraphicsEndImageContext()
return img
}()
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