jsdw_ios/QuickLocation/Section/Home/TodayTrackDetail/TodayTrackDetailView.swift

544 lines
18 KiB
Swift

//
// TodayTrackDetailView.swift
// QuickLocation
//
import UIKit
import AMapNaviKit
final class TodayTrackDetailView: UIView {
lazy var headerBgImg: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Home/HistoryTrack/header_bg")
return view
}()
let mapView: MAMapView = {
let mv = MAMapView()
mv.zoomLevel = 14
mv.showsUserLocation = false
mv.showsCompass = false
mv.showsScale = false
return mv
}()
let backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 40)
return btn
}()
let titleLab: UILabel = {
let lab = UILabel()
lab.text = "历史轨迹"
lab.font = .systemFont(ofSize: 17, weight: .semibold)
lab.textColor = UIColor(hexStr: "#293445")
lab.textAlignment = .center
return lab
}()
lazy var memberCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 61, height: 90)
layout.minimumLineSpacing = 12
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.showsHorizontalScrollIndicator = false
cv.contentInset = UIEdgeInsets(top: 3, left: 6, bottom: 0, right: 6)
cv.register(GroupMemberListCell.self)
cv.cornerRadius = 16
return cv
}()
let playBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/HistoryTrack/play"), for: .normal)
btn.setImage(UIImage(named: "Home/HistoryTrack/pause"), for: .selected)
return btn
}()
let progressSlider: HistoryTrackSlider = {
let slider = HistoryTrackSlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 0
slider.minimumTrackTintColor = UIColor(hexStr: "#16B3FF")
slider.maximumTrackTintColor = UIColor(hexStr: "#D9EEFF")
let thumb = UIImage(named: "Home/HistoryTrack/slider_thumb")
slider.setThumbImage(thumb, for: .normal)
slider.setThumbImage(thumb, for: .highlighted)
return slider
}()
private let playbackBar: UIView = {
let v = UIView()
v.backgroundColor = UIColor.white.withAlphaComponent(0.96)
v.cornerRadius = 12
return v
}()
let sheetView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#FAFAFA")
v.layer.cornerRadius = 30
v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
v.clipsToBounds = true
return v
}()
let handleView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#E5E7EB")
v.cornerRadius = 2.5
return v
}()
let datePrevBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/HistoryTrack/date_prev"), for: .normal)
return btn
}()
let dateNextBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/HistoryTrack/date_next"), for: .normal)
return btn
}()
private let dateBar: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#ECEFF3")
v.cornerRadius = 16
return v
}()
lazy var dateCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.sectionInset = .zero
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.isPagingEnabled = true
cv.register(TodayTrackDateCell.self)
return cv
}()
lazy var timelineTV: UITableView = {
let tv = UITableView(frame: .zero, style: .plain)
tv.separatorStyle = .none
tv.backgroundColor = .clear
tv.showsVerticalScrollIndicator = false
tv.register(TodayTrackTripCell.self)
tv.rowHeight = UITableView.automaticDimension
tv.estimatedRowHeight = 128
return tv
}()
let emptyLab: UILabel = {
let lab = UILabel()
lab.text = "暂无轨迹"
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#9CA3AF")
lab.textAlignment = .center
lab.isHidden = true
return lab
}()
private(set) var sheetHeightConstraint: NSLayoutConstraint?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#F5F7FA")
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
addSubview(mapView)
mapView.layoutChain.edges()
addSubview(headerBgImg)
headerBgImg.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
addSubview(backBtn)
backBtn.layoutChain
.top(kStatusBarHeight + 6)
.left(15)
.width(42)
.height(42)
addSubview(titleLab)
titleLab.layoutChain
.centerY(backBtn)
.centerX()
addSubview(memberCV)
memberCV.layoutChain
.topToBottomOfView(backBtn, offset: 12)
.edgesHorzontal(15)
.height(100)
addSubview(sheetView)
let sheetH = sheetView.heightAnchor.constraint(equalToConstant: 200 + kSafeBottomMargin)
sheetH.isActive = true
sheetHeightConstraint = sheetH
sheetView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
sheetView.leadingAnchor.constraint(equalTo: leadingAnchor),
sheetView.trailingAnchor.constraint(equalTo: trailingAnchor),
sheetView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
addSubview(playbackBar)
playbackBar.layoutChain
.bottomToTopOfView(sheetView, offset: -12)
.edgesHorzontal(15)
.height(35)
playbackBar.addSubview(playBtn)
playbackBar.addSubview(progressSlider)
playBtn.layoutChain
.left(12)
.centerY()
.width(28)
.height(28)
progressSlider.layoutChain
.leftToRightOfView(playBtn, offset: 10)
.right(16)
.centerY()
.height(28)
sheetView.addSubview(handleView)
handleView.layoutChain
.top(8)
.centerX()
.width(36)
.height(5)
sheetView.addSubview(dateBar)
dateBar.layoutChain
.topToBottomOfView(handleView, offset: 10)
.edgesHorzontal(10)
.height(32)
dateBar.addSubview(datePrevBtn)
dateBar.addSubview(dateNextBtn)
dateBar.addSubview(dateCV)
datePrevBtn.layoutChain
.left(4)
.centerY()
.width(28)
.height(28)
dateNextBtn.layoutChain
.right(4)
.centerY()
.width(28)
.height(28)
dateCV.layoutChain
.leftToRightOfView(datePrevBtn, offset: 0)
.rightToLeftOfView(dateNextBtn, offset: 0)
.top()
.bottom()
sheetView.addSubview(timelineTV)
timelineTV.layoutChain
.topToBottomOfView(dateBar, offset: 12)
.edgesHorzontal(10)
.bottom()
sheetView.addSubview(emptyLab)
emptyLab.layoutChain
.centerX()
.topToBottomOfView(dateBar, offset: 40)
}
func cleanupMap() {
mapView.removeAnnotations(mapView.annotations)
mapView.removeOverlays(mapView.overlays)
mapView.delegate = nil
}
func dateItemSize() -> CGSize {
let width = dateCV.bounds.width
let height = dateCV.bounds.height
let itemWidth = width > 0 ? width / 5 : 50
let itemHeight = height > 0 ? height : 32
return CGSize(width: itemWidth, height: itemHeight)
}
func scrollDates(direction: CGFloat) {
let page = dateCV.bounds.width
guard page > 0 else { return }
let offset = dateCV.contentOffset.x + direction * page
let maxX = max(0, dateCV.contentSize.width - page)
let x = min(max(0, offset), maxX)
dateCV.setContentOffset(CGPoint(x: x, y: 0), animated: true)
}
func scrollDatesToEnd(animated: Bool) {
dateCV.layoutIfNeeded()
let page = dateCV.bounds.width
let maxX = max(0, dateCV.contentSize.width - page)
dateCV.setContentOffset(CGPoint(x: maxX, y: 0), animated: animated)
}
}
final class HistoryTrackSlider: UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var rect = super.trackRect(forBounds: bounds)
rect.size.height = 4
rect.origin.y = (bounds.height - 4) / 2
return rect
}
}
// MARK: - Date cell
final class TodayTrackDateCell: UICollectionViewCell {
private let titleLab = UILabel()
private let selectedBg = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .clear
selectedBg.cornerRadius = 12
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
titleLab.textAlignment = .center
titleLab.adjustsFontSizeToFitWidth = true
titleLab.minimumScaleFactor = 0.8
titleLab.lineBreakMode = .byClipping
contentView.addSubview(selectedBg)
selectedBg.addSubview(titleLab)
selectedBg.layoutChain.edgesVertical(4).edgesHorzontal()
titleLab.layoutChain.edges()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(title: String, selected: Bool) {
titleLab.text = title
titleLab.textColor = selected ? UIColor(hexStr: "#293445") : UIColor(hexStr: "#767676")
selectedBg.backgroundColor = selected ? .white : .clear
}
}
// MARK: - Trip cell
final class TodayTrackTripCell: UITableViewCell {
var onViewTapped: (() -> Void)?
private let marker = UIImageView()
private let dot = UIView()
private let lineTop = UIView()
private let lineBottom = UIView()
private let card = UIView()
private let minutesValue = UILabel()
private let minutesUnit = UILabel()
private let distanceValue = UILabel()
private let distanceUnit = UILabel()
private let divider = UIView()
private let viewBtn = UIButton(type: .custom)
private let startPrefix = UILabel()
private let startLab = UILabel()
private let routeLine = UIView()
private let endPrefix = UILabel()
private let endLab = UILabel()
override init(style: UITableViewCell.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(trip: ScheduleRecordModel, isFirst: Bool, isLast: Bool) {
if isFirst {
marker.image = UIImage(named: "Home/HistoryTrack/start")
marker.isHidden = false
dot.isHidden = true
} else if isLast {
marker.image = UIImage(named: "Home/HistoryTrack/end")
marker.isHidden = false
dot.isHidden = true
} else {
marker.image = nil
marker.isHidden = true
dot.isHidden = false
}
lineTop.isHidden = isFirst
lineBottom.isHidden = isLast
minutesValue.text = "\(max(0, trip.duration_minutes))"
distanceValue.text = Self.distanceText(trip.distance_km)
let startTime = trip.start_time.isoStringToCustom(trip.start_time, format: "HH:mm") ?? ""
let endTime = trip.end_time.isoStringToCustom(trip.end_time, format: "HH:mm") ?? ""
startLab.text = Self.placeText(trip.start_address, time: startTime)
endLab.text = Self.placeText(trip.end_address, time: endTime)
}
private func setupUI() {
lineTop.backgroundColor = UIColor(hexStr: "#D1D5DB")
lineBottom.backgroundColor = UIColor(hexStr: "#D1D5DB")
marker.contentMode = .scaleAspectFit
dot.backgroundColor = UIColor(hexStr: "#293445")
dot.cornerRadius = 4
dot.isHidden = true
contentView.addSubview(lineTop)
contentView.addSubview(lineBottom)
contentView.addSubview(marker)
contentView.addSubview(dot)
marker.layoutChain
.centerY()
.left(4)
.width(20)
.height(20)
dot.layoutChain
.centerX(marker)
.centerY(marker)
.width(8)
.height(8)
lineTop.layoutChain
.top()
.centerX(marker)
.width(2)
.bottomToTopOfView(marker, offset: 0)
lineBottom.layoutChain
.topToBottomOfView(marker, offset: 0)
.centerX(marker)
.width(2)
.bottom()
card.backgroundColor = .white
card.cornerRadius = 16
contentView.addSubview(card)
card.layoutChain
.top(4)
.leftToRightOfView(marker, offset: 10)
.right(6)
.bottom(8)
minutesValue.font = .systemFont(ofSize: 20, weight: .semibold)
minutesValue.textColor = UIColor(hexStr: "#16B3FF")
minutesUnit.text = "分钟"
minutesUnit.font = .systemFont(ofSize: 12, weight: .medium)
minutesUnit.textColor = UIColor(hexStr: "#16B3FF")
distanceValue.font = .systemFont(ofSize: 20, weight: .semibold)
distanceValue.textColor = UIColor(hexStr: "#16B3FF")
distanceUnit.text = "公里"
distanceUnit.font = .systemFont(ofSize: 12, weight: .medium)
distanceUnit.textColor = UIColor(hexStr: "#16B3FF")
divider.backgroundColor = UIColor(hexStr: "#D9EEFF")
viewBtn.setTitle("查看", for: .normal)
viewBtn.setTitleColor(UIColor(hexStr: "#9CA3AF"), for: .normal)
viewBtn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium)
viewBtn.addTarget(self, action: #selector(viewTapped), for: .touchUpInside)
startPrefix.text = "起点:"
startPrefix.font = .systemFont(ofSize: 13, weight: .medium)
startPrefix.textColor = UIColor(hexStr: "#9CA3AF")
startLab.font = .systemFont(ofSize: 13, weight: .medium)
startLab.textColor = UIColor(hexStr: "#293445")
startLab.numberOfLines = 2
routeLine.backgroundColor = UIColor(hexStr: "#D4F3FF")
routeLine.cornerRadius = 8
endPrefix.text = "终点:"
endPrefix.font = .systemFont(ofSize: 13, weight: .medium)
endPrefix.textColor = UIColor(hexStr: "#9CA3AF")
endLab.font = .systemFont(ofSize: 13, weight: .medium)
endLab.textColor = UIColor(hexStr: "#293445")
endLab.numberOfLines = 2
[minutesValue, minutesUnit, divider, distanceValue, distanceUnit, viewBtn,
startPrefix, startLab, routeLine, endPrefix, endLab].forEach { card.addSubview($0) }
minutesValue.layoutChain.top(14).left(14)
minutesUnit.layoutChain.leftToRightOfView(minutesValue, offset: 4).centerY(minutesValue)
divider.layoutChain
.leftToRightOfView(minutesUnit, offset: 10)
.centerY(minutesValue)
.width(1)
.height(16)
distanceValue.layoutChain.leftToRightOfView(divider, offset: 10).centerY(minutesValue)
distanceUnit.layoutChain.leftToRightOfView(distanceValue, offset: 4).centerY(minutesValue)
viewBtn.layoutChain.right(8).centerY(minutesValue).width(44).height(28)
startPrefix.layoutChain
.topToBottomOfView(minutesValue, offset: 14)
.left(14)
startLab.layoutChain
.topToView(startPrefix)
.leftToRightOfView(startPrefix, offset: 0)
.right(12)
routeLine.layoutChain
.topToBottomOfView(startLab, offset: 4)
.centerX(startLab)
.width(3)
.height(10)
endPrefix.layoutChain
.topToBottomOfView(routeLine, offset: 4)
.leftToView(startPrefix)
endLab.layoutChain
.topToView(endPrefix)
.leftToRightOfView(endPrefix, offset: 0)
.right(12)
.bottom(14)
}
@objc private func viewTapped() {
onViewTapped?()
}
private static func distanceText(_ km: Double) -> String {
if km < 0.05 { return "0.0" }
if abs(km - km.rounded()) < 0.05 {
return "\(Int(km.rounded()))"
}
return String(format: "%.1f", km)
}
private static func placeText(_ address: TripAddress?, time: String) -> String {
let district = address?.district ?? ""
let street = address?.street ?? ""
var place = ""
if !district.isEmpty, !street.isEmpty {
place = "\(district) \(street)"
} else if !street.isEmpty {
place = street
} else if !district.isEmpty {
place = district
} else {
place = address?.formatted_address ?? ""
}
if time.isEmpty { return place }
if place.isEmpty { return time }
return "\(place) \(time)"
}
}