274 lines
8.5 KiB
Swift
274 lines
8.5 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)
|
||
|
||
// 嵌套滑动协调(参考 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
|
||
}
|
||
}
|