// // CreateScheduleView.swift // QuickLocation // // Created by 八条 on 2026/6/23. // import UIKit import RxSwift import RxCocoa #if !targetEnvironment(simulator) import AMapNaviKit #endif class CreateScheduleView: UIView { var disposeBag = DisposeBag() let createSchedulePopView = CreateSchedulePopView() // MARK: - PopView 拖拽 private var popTopConstraint: NSLayoutConstraint? private var popUpLimit: CGFloat = 0 private var popDownLimit: CGFloat = 0 private var isLimitsSet = false private var panStartTop: CGFloat = 0 private var isSubCanScroll = false private func setupRx() { backBtn.rx.tap.subscribe(onNext: { _ in AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) // 嵌套滑动协调(参考 GroupView 的 PanScrollView 模式) createSchedulePopView.scrollView.delegate = self } private func setupUI() { #if !targetEnvironment(simulator) addSubview(mapView) #endif addSubview(navBgView) addSubview(navBarView) navBarView.addSubview(backBtn) navBarView.addSubview(navTitleLabel) navBarView.addSubview(deleteBtn) addSubview(createSchedulePopView) navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(kNaviHeight) backBtn.layoutChain .centerY(navTitleLabel) .left(15) .width(24).height(24) navTitleLabel.layoutChain .top(kStatusBarHeight + 12) .centerX() deleteBtn.layoutChain .centerY(navTitleLabel) .right(15) .width(16) .height(16) #if !targetEnvironment(simulator) mapView.layoutChain .top() .edgesHorzontal() .bottom() #endif // PopView: 初始底部 1/3,最大滑到 navBar 底部 createSchedulePopView.layoutChain .edgesHorzontal() .bottom() .top(kScreenHeight / 3 * 2) popTopConstraint = createSchedulePopView.jh_constraint( .top, toAttribute: .top, otherView: createSchedulePopView.superview, relation: .equal ) let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePopPan(_:))) pan.delegate = self createSchedulePopView.addGestureRecognizer(pan) } // MARK: - Pan Gesture @objc private func handlePopPan(_ pan: UIPanGestureRecognizer) { guard isLimitsSet, let topConstraint = popTopConstraint else { return } switch pan.state { case .began: layoutIfNeeded() panStartTop = createSchedulePopView.frame.minY case .changed: let newTop = panStartTop + pan.translation(in: self).y let scrollOffset = createSchedulePopView.scrollView.contentOffset.y if isSubCanScroll { // 内容正在滑动,不移动 PopView if scrollOffset > 0 { return } // 内容滑到顶部,切回 Pan 拖拽(用户下拉时 velocity > 0) if pan.velocity(in: self).y > 0 || createSchedulePopView.frame.minY > popUpLimit + 1 { isSubCanScroll = false panStartTop = createSchedulePopView.frame.minY } } else { // PopView 在顶部且继续上滑 → 激活内容滑动 if createSchedulePopView.frame.minY <= popUpLimit && newTop <= popUpLimit { isSubCanScroll = true panStartTop = createSchedulePopView.frame.minY topConstraint.constant = popUpLimit return } } let clamped = max(popUpLimit, min(popDownLimit, newTop)) topConstraint.constant = clamped case .ended, .cancelled: let velocity = pan.velocity(in: self) let isNearUp = abs(createSchedulePopView.frame.minY - popUpLimit) < abs(createSchedulePopView.frame.minY - popDownLimit) let target: CGFloat if createSchedulePopView.frame.minY <= popUpLimit + 5 { // 顶部附近:由位置决定去向,速度不触发回收(避免 scrollView 松手误回收) target = isNearUp ? popUpLimit : popDownLimit } else if abs(velocity.y) > 200 { target = velocity.y < 0 ? popUpLimit : popDownLimit } else { target = isNearUp ? popUpLimit : popDownLimit } topConstraint.constant = target // 动画前设置 scrollView 状态,避免弹跳 let atTop = target == self.popUpLimit if !atTop { isSubCanScroll = false createSchedulePopView.scrollView.isScrollEnabled = false createSchedulePopView.scrollView.setContentOffset(.zero, animated: false) } isSubCanScroll = atTop UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.85, initialSpringVelocity: abs(velocity.y) / 1000, options: [.allowUserInteraction]) { self.layoutIfNeeded() } completion: { _ in self.createSchedulePopView.scrollView.isScrollEnabled = atTop } default: break } } override func layoutSubviews() { super.layoutSubviews() if !isLimitsSet { isLimitsSet = true popDownLimit = kScreenHeight / 3 * 2 popUpLimit = navBarView.frame.maxY createSchedulePopView.scrollView.isScrollEnabled = false } } // 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 v = UIView() v.backgroundColor = .clear return v }() lazy var backBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Common/back"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 30) return btn }() lazy var navTitleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 18, weight: .medium) label.textColor = ThemeManager.shared.color.titleAuxColor label.text = "创建行程" return label }() lazy var deleteBtn: UIButton = { let btn = UIButton() btn.setImage(UIImage(named: "Common/delete"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 15) btn.isHidden = true return btn }() #if !targetEnvironment(simulator) lazy var mapView: MAMapView! = { let mv = MAMapView() mv.zoomLevel = 14 mv.showsUserLocation = false mv.showsCompass = false mv.userTrackingMode = .none return mv }() #endif #if !targetEnvironment(simulator) func cleanupMap() { mapView?.delegate = nil mapView?.removeFromSuperview() mapView = nil } #endif override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear setupUI() setupRx() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UIScrollViewDelegate(参考 GroupView 嵌套滑动协调) extension CreateScheduleView: UIScrollViewDelegate { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { // 内容已有偏移时才允许子滚动 if scrollView.contentOffset.y > 0 { isSubCanScroll = true } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if isSubCanScroll { if scrollView.contentOffset.y <= 0 { isSubCanScroll = false scrollView.contentOffset.y = 0 } } else { scrollView.contentOffset.y = 0 } } } // MARK: - UIGestureRecognizerDelegate extension CreateScheduleView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool { return true } }