// // 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(40) .height(40) 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() let label = UILabel() label.text = "有权限未开启,数据可能会异常" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) view.addSubview(label) label.layoutChain .edgesHorzontal(8) .edgesVertical(10) return view }() // MARK: - 定位按钮 lazy var locationView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.4) view.cornerRadius = 20 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 } }