390 lines
12 KiB
Swift
390 lines
12 KiB
Swift
//
|
||
// HomeView2.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/8/4.
|
||
//
|
||
|
||
import UIKit
|
||
import RxSwift
|
||
import RxCocoa
|
||
import AMapNaviKit
|
||
import MarqueeLabel
|
||
|
||
class HomeView2: UIView {
|
||
|
||
var disposeBag = DisposeBag()
|
||
|
||
let groupMemberView = GroupMemberView2(frame: .zero)
|
||
|
||
/// 三段高度(可见区域)
|
||
private let stage1Height: CGFloat = 160 + 49 + kSafeBottomMargin
|
||
private let stage2Height: CGFloat = 327 + 49 + kSafeBottomMargin
|
||
|
||
// MARK: - groupMemberView 拖拽(三段)
|
||
private var groupMemberTopConstraint: NSLayoutConstraint?
|
||
/// 第三段:贴顶
|
||
private var stage3Top: CGFloat = 0
|
||
/// 第二段:初始
|
||
private var stage2Top: CGFloat = 0
|
||
/// 第一段:最小
|
||
private var stage1Top: CGFloat = 0
|
||
private var isGroupMemberLimitsSet = false
|
||
private var panStartTop: CGFloat = 0
|
||
/// 内嵌 scrollView 是否可滑动(仅第三段)
|
||
private var isSubCanScroll = false
|
||
|
||
private var groupMemberScrollTopOffset: CGFloat {
|
||
-groupMemberView.scrollView.adjustedContentInset.top
|
||
}
|
||
|
||
private var stageTops: [CGFloat] {
|
||
[stage3Top, stage2Top, stage1Top]
|
||
}
|
||
|
||
var gpsSignalViewList: [UIView] = []
|
||
/// 根据信号格数更新信号条颜色
|
||
func updateGPSSignal(bars: Int) {
|
||
let filled = max(0, min(bars, gpsSignalViewList.count))
|
||
for (i, view) in gpsSignalViewList.enumerated() {
|
||
view.backgroundColor = i < filled ? UIColor(hexStr: "#4ED178") : UIColor(hexStr: "#E0E0E0")
|
||
}
|
||
}
|
||
|
||
private func setupRx() {
|
||
// 感叹号
|
||
warningView.rx.tapGesture.subscribe { _ in
|
||
LocationPermissionPopView.show()
|
||
}.disposed(by: disposeBag)
|
||
}
|
||
|
||
private func setupUI() {
|
||
addSubview(mapView)
|
||
addSubview(gpsSignalView)
|
||
addSubview(warningView)
|
||
addSubview(inviteBtn)
|
||
addSubview(locationView)
|
||
addSubview(groupMemberView)
|
||
|
||
mapView.layoutChain
|
||
.edges(excludingEdge: .bottom)
|
||
.bottom(49 + kSafeBottomMargin)
|
||
|
||
gpsSignalView.layoutChain
|
||
.top(kStatusBarHeight + 11)
|
||
.left(15)
|
||
|
||
warningView.layoutChain
|
||
.topToBottomOfView(gpsSignalView, offset: 11)
|
||
.leftToView(gpsSignalView)
|
||
|
||
groupMemberView.layoutChain
|
||
.top(kScreenHeight - stage2Height)
|
||
.edgesHorzontal()
|
||
.bottom()
|
||
|
||
inviteBtn.layoutChain
|
||
.bottomToTopOfView(groupMemberView, offset: -9)
|
||
.edgesHorzontal(15)
|
||
.heightToWidth(50/345)
|
||
|
||
locationView.layoutChain
|
||
.bottomToTopOfView(inviteBtn, offset: -8)
|
||
.left(15)
|
||
.width(32)
|
||
.heightToWidth(1)
|
||
|
||
groupMemberTopConstraint = groupMemberView.jh_constraint(
|
||
.top, toAttribute: .top, otherView: self, relation: .equal
|
||
)
|
||
|
||
let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGroupMemberPan(_:)))
|
||
pan.delegate = self
|
||
groupMemberView.addGestureRecognizer(pan)
|
||
groupMemberView.scrollView.delegate = self
|
||
groupMemberView.scrollView.isScrollEnabled = false
|
||
}
|
||
|
||
// MARK: - Map
|
||
lazy var mapView: MAMapView! = {
|
||
let mv = MAMapView()
|
||
mv.zoomLevel = 16
|
||
mv.showsUserLocation = true
|
||
mv.userTrackingMode = .none
|
||
mv.showsCompass = false
|
||
mv.showsScale = false
|
||
mv.isRotateEnabled = false
|
||
mv.isRotateCameraEnabled = false
|
||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: kScreenWidth - 50, y: kStatusBarHeight) }
|
||
return mv
|
||
}()
|
||
|
||
// MARK: - 卫星信号
|
||
lazy var gpsSignalView: UIView = {
|
||
let view = UIView()
|
||
view.cornerRadius = 6
|
||
view.backgroundColor = .white
|
||
|
||
let signalView1 = UIView()
|
||
signalView1.backgroundColor = UIColor(hexStr: "#E6E6E6")
|
||
view.addSubview(signalView1)
|
||
signalView1.layoutChain
|
||
.left(7)
|
||
.bottom(5)
|
||
.width(3)
|
||
.height(4)
|
||
|
||
let signalView2 = UIView()
|
||
signalView2.backgroundColor = UIColor(hexStr: "#E6E6E6")
|
||
view.addSubview(signalView2)
|
||
signalView2.layoutChain
|
||
.leftToRightOfView(signalView1, offset: 2)
|
||
.bottomToView(signalView1)
|
||
.width(3)
|
||
.height(6)
|
||
|
||
let signalView3 = UIView()
|
||
signalView3.backgroundColor = UIColor(hexStr: "#E6E6E6")
|
||
view.addSubview(signalView3)
|
||
signalView3.layoutChain
|
||
.leftToRightOfView(signalView2, offset: 2)
|
||
.bottomToView(signalView1)
|
||
.width(3)
|
||
.height(8)
|
||
|
||
let signalView4 = UIView()
|
||
signalView4.backgroundColor = UIColor(hexStr: "#E6E6E6")
|
||
view.addSubview(signalView4)
|
||
signalView4.layoutChain
|
||
.leftToRightOfView(signalView3, offset: 2)
|
||
.bottomToView(signalView1)
|
||
.width(3)
|
||
.height(10)
|
||
|
||
let label = UILabel()
|
||
label.text = "卫星信号"
|
||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||
label.textColor = UIColor(hexStr: "#353B4F")
|
||
view.addSubview(label)
|
||
label.layoutChain
|
||
.leftToRightOfView(signalView4, offset: 2)
|
||
.edgesVertical(4)
|
||
.right(7)
|
||
|
||
gpsSignalViewList = [signalView1, signalView2, signalView3, signalView4]
|
||
|
||
return view
|
||
}()
|
||
|
||
// MARK: - warning
|
||
lazy var warningView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .white
|
||
view.cornerRadius = 8
|
||
view.isHidden = true
|
||
|
||
let icon = UIImageView(image: UIImage(named: "Home/warning"))
|
||
view.addSubview(icon)
|
||
icon.layoutChain
|
||
.left(8)
|
||
.width(14)
|
||
.heightToWidth(1)
|
||
.centerY()
|
||
|
||
let label = UILabel()
|
||
label.text = "有权限未开启,数据可能会异常"
|
||
label.textColor = UIColor(hexStr: "#353B4F")
|
||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||
view.addSubview(label)
|
||
label.layoutChain
|
||
.leftToRightOfView(icon, offset: 6)
|
||
.right(8)
|
||
.edgesVertical(10)
|
||
|
||
return view
|
||
}()
|
||
|
||
// MARK: - 定位按钮
|
||
lazy var locationView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .white
|
||
view.cornerRadius = 10
|
||
|
||
view.addSubview(locationIconView)
|
||
locationIconView.layoutChain.centerX().centerY()
|
||
|
||
return view
|
||
}()
|
||
|
||
lazy var locationIconView: UIImageView = {
|
||
let view = UIImageView()
|
||
view.backgroundColor = .clear
|
||
view.image = UIImage(named: "Home/location")
|
||
return view
|
||
}()
|
||
|
||
// MARK: - 邀请加入
|
||
lazy var inviteBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setBackgroundImage(UIImage(named: "Home/invite"), for: .normal)
|
||
return btn
|
||
}()
|
||
|
||
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: - Layout
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
if !isGroupMemberLimitsSet, bounds.height > 0 {
|
||
isGroupMemberLimitsSet = true
|
||
stage3Top = 0
|
||
stage2Top = bounds.height - stage2Height
|
||
stage1Top = bounds.height - stage1Height
|
||
groupMemberTopConstraint?.constant = stage2Top
|
||
}
|
||
}
|
||
|
||
// MARK: - Pan Gesture
|
||
@objc private func handleGroupMemberPan(_ pan: UIPanGestureRecognizer) {
|
||
guard isGroupMemberLimitsSet, let topConstraint = groupMemberTopConstraint else { return }
|
||
|
||
switch pan.state {
|
||
case .began:
|
||
layoutIfNeeded()
|
||
panStartTop = groupMemberView.frame.minY
|
||
|
||
case .changed:
|
||
let newTop = panStartTop + pan.translation(in: self).y
|
||
let scrollOffset = groupMemberView.scrollView.contentOffset.y
|
||
|
||
if isSubCanScroll {
|
||
if scrollOffset > groupMemberScrollTopOffset + 0.5 {
|
||
return
|
||
}
|
||
|
||
if pan.velocity(in: self).y > 0 || groupMemberView.frame.minY > stage3Top + 1 {
|
||
isSubCanScroll = false
|
||
panStartTop = groupMemberView.frame.minY
|
||
}
|
||
} else {
|
||
if groupMemberView.frame.minY <= stage3Top && newTop <= stage3Top {
|
||
isSubCanScroll = true
|
||
panStartTop = groupMemberView.frame.minY
|
||
topConstraint.constant = stage3Top
|
||
groupMemberView.setNavBarExpanded(true)
|
||
return
|
||
}
|
||
}
|
||
|
||
let clamped = max(stage3Top, min(stage1Top, newTop))
|
||
topConstraint.constant = clamped
|
||
|
||
case .ended, .cancelled:
|
||
let velocity = pan.velocity(in: self)
|
||
let currentTop = groupMemberView.frame.minY
|
||
let target = snapTarget(currentTop: currentTop, velocityY: velocity.y)
|
||
applyStage(top: target, animated: true)
|
||
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
/// 按位置与速度吸附到最近一档
|
||
private func snapTarget(currentTop: CGFloat, velocityY: CGFloat) -> CGFloat {
|
||
if currentTop <= stage3Top + 5 {
|
||
// 顶部附近优先按位置,避免 scroll 松手速度误回收
|
||
return nearestStage(to: currentTop)
|
||
}
|
||
if abs(velocityY) > 200 {
|
||
if velocityY < 0 {
|
||
// 上滑:往上一档(更小的 top 中取最大,即最近一档)
|
||
return stageTops.filter { $0 < currentTop - 1 }.max() ?? stage3Top
|
||
} else {
|
||
// 下滑:往下一档(更大的 top 中取最小,即最近一档)
|
||
return stageTops.filter { $0 > currentTop + 1 }.min() ?? stage1Top
|
||
}
|
||
}
|
||
return nearestStage(to: currentTop)
|
||
}
|
||
|
||
private func nearestStage(to top: CGFloat) -> CGFloat {
|
||
stageTops.min(by: { abs($0 - top) < abs($1 - top) }) ?? stage2Top
|
||
}
|
||
|
||
private func applyStage(top: CGFloat, animated: Bool) {
|
||
guard let topConstraint = groupMemberTopConstraint else { return }
|
||
let atTop = abs(top - stage3Top) < 0.5
|
||
topConstraint.constant = top
|
||
|
||
if !atTop {
|
||
isSubCanScroll = false
|
||
groupMemberView.scrollView.isScrollEnabled = false
|
||
groupMemberView.scrollView.setContentOffset(CGPoint(x: 0, y: groupMemberScrollTopOffset), animated: false)
|
||
}
|
||
isSubCanScroll = atTop
|
||
groupMemberView.setNavBarExpanded(atTop)
|
||
|
||
let animations = {
|
||
self.layoutIfNeeded()
|
||
}
|
||
let completion: (Bool) -> Void = { _ in
|
||
self.groupMemberView.scrollView.isScrollEnabled = atTop
|
||
}
|
||
|
||
if animated {
|
||
UIView.animate(withDuration: 0.25, delay: 0,
|
||
options: [.curveEaseOut, .allowUserInteraction],
|
||
animations: animations, completion: completion)
|
||
} else {
|
||
animations()
|
||
completion(true)
|
||
}
|
||
}
|
||
|
||
/// 收起 GroupMemberView2 到第二段(初始段)
|
||
func dismissGroupMemberView() {
|
||
guard isGroupMemberLimitsSet else { return }
|
||
applyStage(top: stage2Top, animated: true)
|
||
}
|
||
}
|
||
|
||
// MARK: - UIScrollViewDelegate
|
||
extension HomeView2: UIScrollViewDelegate {
|
||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||
guard scrollView == groupMemberView.scrollView else { return }
|
||
if scrollView.contentOffset.y > groupMemberScrollTopOffset {
|
||
isSubCanScroll = true
|
||
}
|
||
}
|
||
|
||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||
guard scrollView == groupMemberView.scrollView else { return }
|
||
if isSubCanScroll {
|
||
if scrollView.contentOffset.y <= groupMemberScrollTopOffset {
|
||
isSubCanScroll = false
|
||
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberScrollTopOffset)
|
||
}
|
||
} else {
|
||
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberScrollTopOffset)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - UIGestureRecognizerDelegate
|
||
extension HomeView2: UIGestureRecognizerDelegate {
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
||
return true
|
||
}
|
||
}
|