jsdw_ios/QuickLocation/Section/Home/HomeView.swift

820 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// HomeView.swift
// QuickLocation
//
// Created by on 2026/5/27.
//
import UIKit
import RxSwift
import RxCocoa
import Lottie
#if !targetEnvironment(simulator)
import AMapNaviKit
import MarqueeLabel
#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))
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")
}
}
// 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)
}
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(messageStackView)
addSubview(toolsView)
addSubview(searchLottieView)
addSubview(locationView)
locationView.addSubview(locationIconView)
addSubview(emojiPopView)
addSubview(sosPopView)
#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)
messageStackView.layoutChain
.topToBottomOfView(messageView, offset: 23)
.edgesHorzontal(15)
toolsView.layoutChain
.left(15)
.bottom(kScreenHeight / 2 - 58)
.width(40)
bubbleView.layoutChain.top(15).height(58)
signInView.layoutChain.height(58)
sosView.layoutChain
.height(56)
sosIcon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
sosLab.layoutChain
.topToBottomOfView(sosIcon, offset: 4)
.edgesHorzontal()
searchLottieView.layoutChain
.centerY(toolsView)
.right()
.width(100)
.height(100)
locationView.layoutChain
.topToBottomOfView(searchLottieView, offset: 8)
.right(15)
.bottomToView(toolsView)
.width(40)
.height(40)
locationIconView.layoutChain
.centerX()
.centerY()
emojiPopView.layoutChain
.centerX().centerY()
.width(180).height(180)
sosPopView.layoutChain
.bottomToView(toolsView)
.leftToRightOfView(toolsView)
.height(57).width(57)
// 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
toolsView.isHidden = true
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
toolsView.isHidden = false
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()
noticeView.layoutIfNeeded()
noticeInfoView.setGradientLayer(frame: noticeInfoView.bounds,
startPoint: CGPoint(x: 0, y: 0.5),
endPoint: CGPoint(x: 1, y: 0.5),
colors: [UIColor(hexStr: "#FFFFFF", alpha: 0), UIColor(hexStr: "#FFFFFF"), UIColor(hexStr: "#FFFFFF", alpha: 0)],
locations: [0, 0.5, 1])
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
}()
lazy var messageStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [tipsView, messageBubbleView])
view.axis = .vertical
view.alignment = .leading
view.spacing = 5
view.backgroundColor = .clear
return view
}()
lazy var tipsView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layoutChain.height(22)
view.addSubview(gpsSignalView)
gpsSignalView.layoutChain
.left()
.centerY()
view.addSubview(noticeView)
noticeView.layoutChain
.leftToRightOfView(gpsSignalView, offset: 0)
.right(15)
.edgesVertical()
return view
}()
// MARK: -
lazy var gpsSignalView: UIView = {
let view = UIView()
view.cornerRadius = 5
view.backgroundColor = .black.withAlphaComponent(0.4)
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 = .white
view.addSubview(label)
label.layoutChain
.leftToRightOfView(signalView4, offset: 2)
.edgesVertical(4)
.right(7)
gpsSignalViewList = [signalView1, signalView2, signalView3, signalView4]
return view
}()
// MARK: -
lazy var noticeView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.addSubview(noticeInfoView)
noticeInfoView.layoutChain
.left()
.right()
.edgesVertical()
view.addSubview(noticeCloseBtn)
noticeCloseBtn.layoutChain
.right()
.width(16)
.height(16)
.centerY()
return view
}()
lazy var noticeInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
// let titleLab = UILabel()
// titleLab.text = ""
// titleLab.font = .systemFont(ofSize: 10, weight: .medium)
// titleLab.textColor = UIColor(hexStr: "#0F2948")
// view.addSubview(titleLab)
// titleLab.layoutChain
// .left(16)
// .centerY()
// .width(40)
view.addSubview(noticeLab)
noticeLab.layoutChain
.left(16)
.centerY()
.right(24)
return view
}()
lazy var noticeLab: CallbackMarqueeLabel = {
let label = CallbackMarqueeLabel()
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2948")
return label
}()
lazy var noticeCloseBtn: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "Common/x_black"), for: .normal)
btn.backgroundColor = .white
btn.cornerRadius = 8
btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
btn.rx.tap.subscribe(onNext: { [weak self] _ in
self?.noticeView.isHidden = true
}).disposed(by: disposeBag)
return btn
}()
// MARK: -
lazy var messageBubbleView: UIView = {
let view = UIView()
view.backgroundColor = .white.withAlphaComponent(0.8)
view.cornerRadius = 10
view.isHidden = true
view.addSubview(messageLab)
messageLab.layoutChain
.edges(all: 12)
return view
}()
lazy var messageLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2846")
return label
}()
// MARK: - SOS
lazy var sosPopView: LottieAnimationView = {
let view = LottieAnimationView(name: "siren")
view.loopMode = .loop
view.isHidden = true
return view
}()
// MARK: -
lazy var emojiPopView: LottieAnimationView = {
let view = LottieAnimationView()
view.loopMode = .playOnce
view.isHidden = true
return view
}()
// MARK: -
lazy var toolsView: UIStackView = {
let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView])
view.axis = .vertical
view.distribution = .equalSpacing
view.alignment = .center
view.backgroundColor = .black.withAlphaComponent(0.5)
view.cornerRadius = 20
return view
}()
//
lazy var bubbleView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.addSubview(bubbleIcon)
bubbleIcon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
view.addSubview(bubbleLab)
bubbleLab.layoutChain
.topToBottomOfView(bubbleIcon, offset: 4)
.edgesHorzontal()
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)
signInIcon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
view.addSubview(signInLab)
signInLab.layoutChain
.topToBottomOfView(signInIcon, offset: 4)
.edgesHorzontal()
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
}
}
// MARK: - CallbackMarqueeLabel
class CallbackMarqueeLabel: MarqueeLabel {
//
var onScrollLoopComplete: (() -> Void)?
var onScrollStart: (() -> Void)?
//
override func labelWillBeginScroll() {
super.labelWillBeginScroll()
onScrollStart?()
}
//
override func labelReturnedToHome(_ finished: Bool) {
super.labelReturnedToHome(finished)
//
guard finished else { return }
onScrollLoopComplete?()
}
}