485 lines
13 KiB
Swift
485 lines
13 KiB
Swift
//
|
|
// 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
|
|
}()
|
|
}
|