// // 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) // 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 // 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 // MARK: - Setup private func setupRx() { } 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(_:))) groupMemberView.addGestureRecognizer(pan) } 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 = pan.location(in: self).y - pan.translation(in: self).y case .changed: let newTop = panStartTop + pan.translation(in: self).y 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() } default: break } } // 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 }() }