jsdw_ios/QuickLocation/Section/Home/HomeView2.swift

541 lines
18 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.

//
// 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()
var onUnlockRequestTap: (() -> Void)?
var onUnlockReceiveTap: (() -> Void)?
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)
unlockRequestBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.onUnlockRequestTap?()
})
.disposed(by: disposeBag)
pigeonMsgBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.onUnlockReceiveTap?()
})
.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(mapView)
addSubview(sosGradientView)
addSubview(gpsSignalView)
addSubview(warningView)
addSubview(inviteBtn)
addSubview(locationView)
addSubview(actionStackView)
addSubview(groupMemberView)
mapView.layoutChain
.edges(excludingEdge: .bottom)
.bottom(49 + kSafeBottomMargin)
sosGradientView.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(32)
.heightToWidth(1)
actionStackView.layoutChain
.right(14)
.bottomToTopOfView(inviteBtn, offset: -20)
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
}()
private let sosGradientView = HomeSOSGradientView()
func setSOSGradientActive(_ isActive: Bool) {
sosGradientView.setActive(isActive)
}
// 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(image: UIImage(named: "Home/warning"))
view.addSubview(icon)
icon.layoutChain
.left(8)
.width(14)
.heightToWidth(1)
.centerY()
let label = UILabel()
label.text = "定位权限未开启,数据可能会异常"
label.textColor = UIColor(hexStr: "#353B4F")
label.font = .systemFont(ofSize: 12, weight: .medium)
view.addSubview(label)
label.layoutChain
.leftToRightOfView(icon, offset: 6)
.right(8)
.edgesVertical(10)
return view
}()
// MARK: - 解锁操作
lazy var actionStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [unlockRequestBtn, pigeonMsgBtn])
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 8
unlockRequestBtn.layoutChain
.width(51)
.height(50)
pigeonMsgBtn.layoutChain
.width(50)
.height(50)
return stackView
}()
lazy var unlockRequestBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/unlock_request")?.withRenderingMode(.alwaysOriginal), for: .normal)
btn.imageView?.contentMode = .scaleAspectFit
btn.isHidden = true
return btn
}()
lazy var pigeonMsgBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/pigeon_msg")?.withRenderingMode(.alwaysOriginal), for: .normal)
btn.imageView?.contentMode = .scaleAspectFit
btn.isHidden = true
return btn
}()
// MARK: - 定位按钮
lazy var locationView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
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)
}
}
private final class HomeSOSGradientView: UIView {
private let gradientLayer = CAGradientLayer()
private static let flashAnimationKey = "home.sos.gradient.flash"
override init(frame: CGRect) {
super.init(frame: frame)
isHidden = true
isUserInteractionEnabled = false
backgroundColor = .clear
gradientLayer.type = .radial
gradientLayer.colors = [
UIColor.clear.cgColor,
UIColor(hexStr: "#FF7378").withAlphaComponent(0.16).cgColor,
UIColor(hexStr: "#FF7378").withAlphaComponent(0.58).cgColor
]
gradientLayer.locations = [0, 0.58, 1]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.48)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
layer.addSublayer(gradientLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
func setActive(_ isActive: Bool) {
guard isActive != !isHidden else { return }
if isActive {
isHidden = false
startFlashing()
} else {
gradientLayer.removeAnimation(forKey: Self.flashAnimationKey)
gradientLayer.opacity = 1
isHidden = true
}
}
private func startFlashing() {
guard !UIAccessibility.isReduceMotionEnabled else { return }
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [0.38, 1.0, 0.38]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 1.15
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.isRemovedOnCompletion = false
gradientLayer.add(animation, forKey: Self.flashAnimationKey)
}
}
// 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 {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer,
pan.view === groupMemberView else { return true }
let memberCV = groupMemberView.memberCV
guard !memberCV.isHidden, isTouch(pan, inside: memberCV) else { return true }
if memberCV.isDragging || memberCV.isDecelerating {
return false
}
let velocity = pan.velocity(in: groupMemberView)
let translation = pan.translation(in: groupMemberView)
let dx = max(abs(velocity.x), abs(translation.x) * 60)
let dy = max(abs(velocity.y), abs(translation.y) * 60)
return dy > dx * 1.8
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
if isMemberListPan(other) {
return false
}
return true
}
private func isMemberListPan(_ gesture: UIGestureRecognizer) -> Bool {
let memberCV = groupMemberView.memberCV
if gesture.view === memberCV { return true }
return gesture.view?.isDescendant(of: memberCV) == true
}
private func isTouch(_ pan: UIPanGestureRecognizer, inside view: UIView) -> Bool {
view.bounds.contains(pan.location(in: view))
}
}