// // GroupScheduleView.swift // QuickLocation // // Created by 八条 on 2026/6/29. // import UIKit import RxSwift import RxCocoa import AMapNaviKit class GroupScheduleView: UIView { var disposeBag = DisposeBag() let selectedSchedule = PublishSubject() // MARK: - PopView 拖拽 private var popTopConstraint: NSLayoutConstraint? private var isLimitsSet = false private let popDownHeight: CGFloat = 250 private var popUpLimit: CGFloat = 0 private var panStartTop: CGFloat = 0 private var isSubCanScroll = false private func setupRx() { backBtn.rx.tap.subscribe(onNext: { _ in AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) } private func setupUI() { addSubview(mapView) addSubview(navBgView) addSubview(navBarView) navBarView.addSubview(navTitleLabel) navBarView.addSubview(backBtn) addSubview(bottomView) bottomView.addSubview(lineView) bottomView.addSubview(tableView) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerY(backBtn) .centerX() backBtn.layoutChain .centerY(navTitleLabel) .left(15) .width(24) .height(24) mapView.layoutChain .top() .edgesHorzontal() .bottom() // 底部面板 bottomView.layoutChain .edgesHorzontal() .bottom() .top(kScreenHeight - popDownHeight) lineView.layoutChain .top(8) .centerX() .width(36) .height(4) tableView.layoutChain .topToBottomOfView(lineView, offset: 10) .edgesHorzontal() .bottom() popTopConstraint = bottomView.jh_constraint( .top, toAttribute: .top, otherView: bottomView.superview, relation: .equal ) let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) pan.delegate = self bottomView.addGestureRecognizer(pan) } // MARK: - Pan Gesture @objc private func handlePan(_ pan: UIPanGestureRecognizer) { guard isLimitsSet, let topConstraint = popTopConstraint else { return } switch pan.state { case .began: panStartTop = bottomView.frame.minY case .changed: let newTop = panStartTop + pan.translation(in: self).y let scrollOffset = tableView.contentOffset.y if isSubCanScroll { if scrollOffset > 0 { return } if pan.velocity(in: self).y > 0 || bottomView.frame.minY > popUpLimit + 1 { isSubCanScroll = false panStartTop = bottomView.frame.minY } } else { if bottomView.frame.minY <= popUpLimit && newTop <= popUpLimit { isSubCanScroll = true panStartTop = bottomView.frame.minY topConstraint.constant = popUpLimit return } } let clamped = max(popUpLimit, min(kScreenHeight - popDownHeight, newTop)) topConstraint.constant = clamped case .ended, .cancelled: let velocity = pan.velocity(in: self) let frameMinY = bottomView.frame.minY let isNearUp = abs(frameMinY - popUpLimit) < abs(frameMinY - (kScreenHeight - popDownHeight)) let target: CGFloat if frameMinY <= popUpLimit + 5 { target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight) } else if abs(velocity.y) > 200 { target = velocity.y < 0 ? popUpLimit : (kScreenHeight - popDownHeight) } else { target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight) } topConstraint.constant = target isSubCanScroll = target == popUpLimit UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.85, initialSpringVelocity: abs(velocity.y) / 1000, options: [.allowUserInteraction]) { self.layoutIfNeeded() } default: break } } override func layoutSubviews() { super.layoutSubviews() if !isLimitsSet { isLimitsSet = true popUpLimit = navBarView.frame.maxY } } // MARK: - Views lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill return iv }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.text = "行程路线" label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.textAlignment = .center return label }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100) return btn }() lazy var mapView: MAMapView! = { let mv = MAMapView() mv.zoomLevel = 14 mv.showsUserLocation = false mv.showsCompass = false mv.userTrackingMode = .none return mv }() lazy var bottomView: UIView = { let v = UIView() v.backgroundColor = .white v.layer.cornerRadius = 16 v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] return v }() lazy var lineView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#EBEBEB") v.cornerRadius = 2 return v }() lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.estimatedRowHeight = 137 tv.rowHeight = UITableView.automaticDimension tv.register(GroupScheduleCell.self) return tv }() /// 收起底部面板到初始位置 func dismissPanel() { guard let topConstraint = popTopConstraint else { return } isSubCanScroll = false topConstraint.constant = kScreenHeight - popDownHeight UIView.animate(withDuration: 0.3) { self.layoutIfNeeded() } } func cleanupMap() { #if !targetEnvironment(simulator) mapView?.delegate = nil mapView?.removeFromSuperview() mapView = nil #endif } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() setupRx() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UIGestureRecognizerDelegate extension GroupScheduleView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool { return true } } // MARK: - GroupScheduleCell class GroupScheduleCell: UITableViewCell { func configure(_ model: ScheduleModel, isSelected: Bool) { dateLab.text = "行程时间:\(getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "yyyy年MM月dd日"))" iconView.image = model.userIcon nameLab.text = "\(model.nick_name) 的行程路线" selectedBgView.isHidden = !isSelected } private func setupSubviews() { contentView.addSubview(bgView) bgView.addSubview(selectedBgView) bgView.addSubview(dateLab) bgView.addSubview(detailView) bgView.layoutChain .top() .edgesHorzontal(15) .height(137) .bottom(12) selectedBgView.layoutChain.edges() dateLab.layoutChain .top(9) .left(15) detailView.layoutChain .topToBottomOfView(dateLab, offset: 8) .edges(all: 15, excludingEdge: .top) } lazy var bgView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#EEFAFF") view.cornerRadius = 10 return view }() lazy var selectedBgView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#C0EAFF") view.borderWidth = 1 view.borderColor = UIColor(hexStr: "#16B3FF") view.cornerRadius = 10 view.isHidden = true return view }() lazy var dateLab: UILabel = { let label = UILabel() label.text = " " label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#0F2846") return label }() lazy var detailView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 10 view.addSubview(iconView) iconView.layoutChain .left(15) .centerY() .width(50) .heightToWidth(1) view.addSubview(nameLab) nameLab.layoutChain .leftToRightOfView(iconView, offset: 14) .right(15, relation: .greaterThanOrEqual) .centerY(iconView) return view }() lazy var iconView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill return view }() lazy var nameLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#0F2846") return label }() override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear setupSubviews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }