// // HomeView.swift // QuickLocation // // Created by 八条 on 2026/5/27. // import UIKit import RxSwift import RxCocoa import Lottie #if !targetEnvironment(simulator) import MAMapKit #endif class HomeView: UIView { var disposeBag = DisposeBag() let groupMemberView = GroupMemberView(frame: .zero) let quickMessageView = QuickMessageView(frame: CGRectMake(0, kScreenHeight, kScreenWidth, 93)) let interactionView = InteractionView(frame: CGRectMake(0, kScreenHeight, kScreenWidth, 384)) // MARK: - groupMemberView 拖拽 private var groupMemberTopConstraint: NSLayoutConstraint? private var groupMemberUpLimit: CGFloat = 0 private var groupMemberDownLimit: CGFloat = 0 private var isGroupMemberLimitsSet = false private var panStartTop: CGFloat = 0 /// 内嵌 tableView 是否可滑动(最大化时才可) private var isSubCanScroll = false // MARK: - Map #if !targetEnvironment(simulator) 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 return mv }() #else lazy var mapPlaceholderView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#EDEDED") let label = UILabel() label.text = "Map requires a real device" label.font = UIFont.systemFont(ofSize: 14) label.textColor = UIColor(hexStr: "#999999") label.textAlignment = .center v.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: v.centerXAnchor), label.centerYAnchor.constraint(equalTo: v.centerYAnchor) ]) return v }() #endif /// 面板关闭回调 var onDismissPanel: (() -> Void)? // MARK: - Setup private func setupRx() { quickMessageView.closeBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.dismissMemberPanel() self?.onDismissPanel?() }) .disposed(by: disposeBag) // tableView 到达顶部继续下拉时,改由 GroupMemberView 的 pan 手势接管 groupMemberView.tableView.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.groupMemberView.tableView.setContentOffset(.zero, animated: false) } } else if offset.y != 0 { self.groupMemberView.tableView.setContentOffset(.zero, animated: false) } }) .disposed(by: disposeBag) // 收回互动view quickMessageView.closeBtn.rx.tap.subscribe(onNext: { _ in self.dismissMemberPanel() }).disposed(by: disposeBag) } private func setupUI() { #if !targetEnvironment(simulator) addSubview(mapView) #else addSubview(mapPlaceholderView) #endif addSubview(navBarBg) addSubview(avatarImgView) addSubview(groupView) groupView.addSubview(groupIconView) groupView.addSubview(groupNameLab) groupView.addSubview(groupArrowIconView) addSubview(messageView) addSubview(toolsView) addSubview(searchLottieView) addSubview(locationView) locationView.addSubview(locationIconView) #if !targetEnvironment(simulator) mapView.layoutChain .top() .edges(excludingEdge: .top) sendSubviewToBack(mapView) #else mapPlaceholderView.layoutChain .topToBottomOfView(navBarBg) .left(0).right(0).bottom(0) #endif navBarBg.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) avatarImgView.layoutChain .top(59) .left(15) .width(36) .heightToWidth(1) groupView.layoutChain .centerY(avatarImgView) .height(36) .centerX() .width(185, relation: .greaterThanOrEqual) groupIconView.layoutChain .left(11) .centerY() .width(30) .height(30) groupArrowIconView.layoutChain .right(15) .centerY() .width(15) .height(8.5) groupNameLab.layoutChain .edgesVertical() .leftToRightOfView(groupIconView) .rightToLeftOfView(groupArrowIconView) messageView.layoutChain .right(15) .centerY(groupView) .width(36) .height(36) toolsView.layoutChain .left(23) .centerY() .width(40) bubbleView.layoutChain .top(12) .height(58) bubbleIcon.layoutChain .top() .centerX() .width(28) .height(28) bubbleLab.layoutChain .topToBottomOfView(bubbleIcon, offset: 4) .edgesHorzontal() signInView.layoutChain.height(58) signInIcon.layoutChain .top() .centerX() .width(28) .height(28) signInLab.layoutChain .topToBottomOfView(signInIcon, offset: 4) .edgesHorzontal() sosView.layoutChain .height(56) sosIcon.layoutChain .top() .centerX() .width(28) .height(28) sosLab.layoutChain .topToBottomOfView(sosIcon, offset: 4) .edgesHorzontal() searchLottieView.layoutChain .centerY() .right() .width(100) .height(100) locationView.layoutChain .topToBottomOfView(searchLottieView, offset: 8) .right(15) .bottomToView(toolsView) .width(40) .height(40) locationIconView.layoutChain .centerX() .centerY() // 圈子成员View addSubview(groupMemberView) groupMemberView.layoutChain .topToBottomOfView(toolsView, offset: 10) .edgesHorzontal() .bottom() groupMemberTopConstraint = groupMemberView.jh_constraint( .top, toAttribute: .bottom, otherView: toolsView, relation: .equal ) let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGroupMemberPan(_:))) pan.delegate = self groupMemberView.addGestureRecognizer(pan) // 底部弹出面板(quickMessageView 在上,interactionView 在下) addSubview(interactionView) addSubview(quickMessageView) } // MARK: - 底部面板显隐 func showMemberPanel(member: CircleMember) { interactionView.configure(member: member) quickMessageView.isHidden = false interactionView.isHidden = false quickMessageView.frame.origin.y = kScreenHeight - 384 - 93 interactionView.frame.origin.y = kScreenHeight - 384 } func dismissMemberPanel() { quickMessageView.frame.origin.y = kScreenHeight interactionView.frame.origin.y = kScreenHeight quickMessageView.isHidden = true interactionView.isHidden = true onDismissPanel?() } override init(frame: CGRect) { super.init(frame: .zero) backgroundColor = .white setupUI() setupRx() searchLottieView.play() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Layout override func layoutSubviews() { super.layoutSubviews() if !isGroupMemberLimitsSet { isGroupMemberLimitsSet = true groupMemberDownLimit = groupMemberView.frame.minY groupMemberUpLimit = groupView.frame.maxY + 20 } } // MARK: - Pan Gesture @objc private func handleGroupMemberPan(_ pan: UIPanGestureRecognizer) { guard isGroupMemberLimitsSet, let topConstraint = groupMemberTopConstraint else { return } switch pan.state { case .began: panStartTop = groupMemberView.frame.minY case .changed: let newTop = panStartTop + pan.translation(in: self).y // 最大化状态下,tableView 可以向下滑时,不干扰自身滑动; // 只有 tableView 到达顶部继续下拉时,才切换为 view 拖拽 if isSubCanScroll { let tableViewOffset = self.groupMemberView.tableView.contentOffset.y if tableViewOffset > 0, newTop >= groupMemberUpLimit { // 还未滑到顶部,让 tableView 处理 return } // 到达顶部继续下拉 → 关闭子滚动,交由 view 的 pan 处理 isSubCanScroll = false panStartTop = groupMemberView.frame.minY } let clamped = max(groupMemberUpLimit, min(groupMemberDownLimit, newTop)) topConstraint.constant = clamped - groupMemberDownLimit + 10 case .ended, .cancelled: let velocity = pan.velocity(in: self) let isNearUp = abs(groupMemberView.frame.minY - groupMemberUpLimit) < abs(groupMemberView.frame.minY - groupMemberDownLimit) let target: CGFloat if abs(velocity.y) > 200 { target = velocity.y < 0 ? groupMemberUpLimit : groupMemberDownLimit } else { target = isNearUp ? groupMemberUpLimit : groupMemberDownLimit } topConstraint.constant = target - groupMemberDownLimit + 10 UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) { self.layoutIfNeeded() } completion: { _ in let atTop = target == self.groupMemberUpLimit self.isSubCanScroll = atTop // 未最大化时重置 tableView 到顶部 if !atTop { self.groupMemberView.tableView.contentOffset.y = 0 } } default: break } } /// 收起 GroupMemberView(选中成员后调用) func dismissGroupMemberView() { guard isGroupMemberLimitsSet, let topConstraint = groupMemberTopConstraint else { return } isSubCanScroll = false topConstraint.constant = groupMemberDownLimit - groupMemberDownLimit + 10 groupMemberView.tableView.setContentOffset(.zero, animated: false) UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) { self.layoutIfNeeded() } } // MARK: - UI Components lazy var navBarBg: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/navBar_bg") view.contentMode = .scaleAspectFill return view }() lazy var avatarImgView: UIImageView = { let view = UIImageView() view.backgroundColor = .lightGray view.contentMode = .scaleAspectFill view.cornerRadius = 18 return view }() // 圈子选择 lazy var groupView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 18 return view }() lazy var groupIconView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/group") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var groupNameLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16, weight: .medium) label.textColor = UIColor(hexStr: "#0F2846") label.textAlignment = .center return label }() lazy var groupArrowIconView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/arrow_down") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() /// 消息 lazy var messageView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 18 view.clipsToBounds = false view.addSubview(messageIcon) view.addSubview(messageDotView) messageIcon.layoutChain .centerX().centerY() messageDotView.layoutChain .top() .right() .width(10) .height(10) return view }() lazy var messageIcon: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/message") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var messageDotView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#FD5E61") view.cornerRadius = 5 return view }() // MARK: - 侧边工具栏 lazy var toolsView: UIStackView = { let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView]) view.axis = .vertical view.distribution = .fillEqually view.alignment = .center view.spacing = 0 view.backgroundColor = .black.withAlphaComponent(0.5) view.cornerRadius = 20 return view }() // 气泡 lazy var bubbleView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(bubbleIcon) view.addSubview(bubbleLab) let lineView = UIView() lineView.backgroundColor = .white view.addSubview(lineView) lineView.layoutChain .width(12) .height(2) .centerX() .bottom(7) return view }() lazy var bubbleIcon: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/bubble") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var bubbleLab: UILabel = { let label = UILabel() label.text = "气泡" label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() // 签到 lazy var signInView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(signInIcon) view.addSubview(signInLab) let lineView = UIView() lineView.backgroundColor = .white view.addSubview(lineView) lineView.layoutChain .width(12) .height(2) .centerX() .bottom(7) return view }() lazy var signInIcon: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/signIn") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var signInLab: UILabel = { let label = UILabel() label.text = "签到" label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() // SOS lazy var sosView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(sosIcon) view.addSubview(sosLab) return view }() lazy var sosIcon: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/sos") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var sosLab: UILabel = { let label = UILabel() label.text = "SOS" label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = .white label.textAlignment = .center return label }() // MARK: - 查位置 lazy var searchLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "home_search") view.loopMode = .loop return view }() // MARK: - 定位按钮 lazy var locationView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.4) view.cornerRadius = 20 return view }() lazy var locationIconView: UIImageView = { let view = UIImageView() view.backgroundColor = .clear view.image = UIImage(named: "Home/location") return view }() } // MARK: - UIGestureRecognizerDelegate extension HomeView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool { // 允许 GroupMemberView 的 pan 与 tableView 的 pan 同时识别 // 滚动状态由 isSubCanScroll + contentOffset 控制,不影响手势判定 return true } }