220 lines
6.6 KiB
Swift
220 lines
6.6 KiB
Swift
//
|
||
// 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)
|
||
|
||
// tableView 到达顶部继续下拉时,改由 PopView 的 pan 手势接管
|
||
createSchedulePopView.scrollView.rx.contentOffset
|
||
.subscribe(onNext: { [weak self] offset in
|
||
guard let self = self else { return }
|
||
if self.isSubCanScroll {
|
||
if offset.y <= 0 {
|
||
self.isSubCanScroll = false
|
||
self.createSchedulePopView.scrollView.setContentOffset(.zero, animated: false)
|
||
}
|
||
} else if offset.y != 0 {
|
||
self.createSchedulePopView.scrollView.setContentOffset(.zero, animated: false)
|
||
}
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private func setupUI() {
|
||
#if !targetEnvironment(simulator)
|
||
addSubview(mapView)
|
||
#endif
|
||
addSubview(navBgView)
|
||
addSubview(navBarView)
|
||
navBarView.addSubview(backBtn)
|
||
navBarView.addSubview(navTitleLabel)
|
||
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()
|
||
|
||
#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
|
||
|
||
if isSubCanScroll {
|
||
let tableViewOffset = self.createSchedulePopView.scrollView.contentOffset.y
|
||
if tableViewOffset > 0, newTop >= popUpLimit {
|
||
return
|
||
}
|
||
isSubCanScroll = false
|
||
panStartTop = createSchedulePopView.frame.minY
|
||
}
|
||
|
||
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 abs(velocity.y) > 200 {
|
||
target = velocity.y < 0 ? popUpLimit : popDownLimit
|
||
} else {
|
||
target = isNearUp ? popUpLimit : popDownLimit
|
||
}
|
||
topConstraint.constant = target
|
||
|
||
UIView.animate(withDuration: 0.2, delay: 0,
|
||
options: [.curveEaseInOut, .allowUserInteraction]) {
|
||
self.layoutIfNeeded()
|
||
} completion: { _ in
|
||
let atTop = target == self.popUpLimit
|
||
self.isSubCanScroll = atTop
|
||
if !atTop {
|
||
self.createSchedulePopView.scrollView.contentOffset.y = 0
|
||
}
|
||
}
|
||
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
if !isLimitsSet {
|
||
isLimitsSet = true
|
||
popDownLimit = kScreenHeight / 3 * 2
|
||
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 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
|
||
}()
|
||
|
||
#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
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
setupUI()
|
||
setupRx()
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - UIGestureRecognizerDelegate
|
||
extension CreateScheduleView: UIGestureRecognizerDelegate {
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
||
return true
|
||
}
|
||
}
|