- 改版(登录100%、首页50%)
|
|
@ -294,6 +294,8 @@
|
|||
30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */; };
|
||||
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; };
|
||||
557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; };
|
||||
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; };
|
||||
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; };
|
||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
|
||||
|
|
@ -601,6 +603,8 @@
|
|||
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
|
||||
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
|
||||
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
|
||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
|
|
@ -1003,9 +1007,11 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
305A76272FCA8C7000227D26 /* GroupMemberView.swift */,
|
||||
305A76282FCA8C7000227D26 /* HomeView.swift */,
|
||||
305A76292FCA8C7000227D26 /* HomeViewController.swift */,
|
||||
305A798B2FCAB99300227D26 /* HomeViewModel.swift */,
|
||||
305A76282FCA8C7000227D26 /* HomeView.swift */,
|
||||
55B2179030217D6600784774 /* HomeView2.swift */,
|
||||
55B2179230218CE100784774 /* GroupMemberView2.swift */,
|
||||
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */,
|
||||
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */,
|
||||
30D87CDC2FDFF07500E958FD /* InteractionView.swift */,
|
||||
|
|
@ -1915,6 +1921,7 @@
|
|||
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
|
||||
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
||||
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
||||
55B2179130217D6600784774 /* HomeView2.swift in Sources */,
|
||||
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */,
|
||||
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
||||
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
||||
|
|
@ -1934,6 +1941,7 @@
|
|||
3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */,
|
||||
30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */,
|
||||
305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */,
|
||||
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */,
|
||||
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
|
||||
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
|
||||
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
|
||||
|
|
@ -2155,6 +2163,7 @@
|
|||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UIBackgroundModes = location;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
|
|
@ -2208,6 +2217,7 @@
|
|||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UIBackgroundModes = location;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "arrow-right@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "arrow-right@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/Common/arrow_right.imageset/arrow-right@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 362 B |
BIN
QuickLocation/Assets.xcassets/Common/arrow_right.imageset/arrow-right@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 620 B |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "group_member_bg2@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "group_member_bg2@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/Home/group_member_bg2.imageset/group_member_bg2@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
QuickLocation/Assets.xcassets/Home/group_member_bg2.imageset/group_member_bg2@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 188 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "vuesax/bold/mouse@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "vuesax/bold/mouse@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/Home/group_name_icon.imageset/vuesax/bold/mouse@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
QuickLocation/Assets.xcassets/Home/group_name_icon.imageset/vuesax/bold/mouse@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "invite@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "invite@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 992 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.7 KiB |
|
|
@ -59,16 +59,12 @@ public let kScreenWidth = UIScreen.main.bounds.size.width
|
|||
public let kScreenHeight = UIScreen.main.bounds.size.height
|
||||
/// 状态栏高度
|
||||
public var kStatusBarHeight: CGFloat {
|
||||
if #available(iOS 13.0, *) {
|
||||
let statusBarHeight = kKeyWindow?
|
||||
.windowScene?
|
||||
.statusBarManager?
|
||||
.statusBarFrame
|
||||
.height ?? 0
|
||||
return statusBarHeight
|
||||
} else {
|
||||
return UIApplication.shared.statusBarFrame.height
|
||||
}
|
||||
let statusBarHeight = kKeyWindow?
|
||||
.windowScene?
|
||||
.statusBarManager?
|
||||
.statusBarFrame
|
||||
.height ?? 0
|
||||
return statusBarHeight
|
||||
}
|
||||
/// 顶部导航+状态栏高度
|
||||
public let kNaviHeight = 44 + kStatusBarHeight
|
||||
|
|
|
|||
|
|
@ -69,5 +69,9 @@
|
|||
<string>douyu.otf</string>
|
||||
<string>zihunbiantaoti-r.ttf</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ final class MainTabBarController: UITabBarController {
|
|||
|
||||
view.addSubview(customTabBar)
|
||||
customTabBar.delegate = self
|
||||
// 点击"探索"(index=1)时游客跳登录、不选中
|
||||
// 点击"探索"(index=1)或"圈子"(index=2)时游客跳登录、不选中
|
||||
customTabBar.shouldSelectTab = { index in
|
||||
if index == 1 || index == 2, AppContextManager.shared.isGuest {
|
||||
if (index == 1 || index == 2), AppContextManager.shared.isGuest {
|
||||
AppRouter.push(Route.login)
|
||||
return false
|
||||
}
|
||||
|
|
@ -98,11 +98,6 @@ final class MainTabBarController: UITabBarController {
|
|||
// MARK: - QuickLocationTabBarDelegate
|
||||
extension MainTabBarController: QuickLocationTabBarDelegate {
|
||||
func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int) {
|
||||
// 点击"探索"(index=1)时判断游客,是则跳登录、不选中
|
||||
if index == 1 || index == 2, AppContextManager.shared.isGuest {
|
||||
AppRouter.push(Route.login)
|
||||
return
|
||||
}
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ final class QuickLocationTabBar: UIView {
|
|||
}
|
||||
|
||||
// MARK: - Design Constants (in iOS points, from Lanhu @2x 容器 2)
|
||||
private let barContentHeight: CGFloat = 56
|
||||
private let containerCornerRadius: CGFloat = 28
|
||||
private let horizontalInset: CGFloat = 18
|
||||
private let barContentHeight: CGFloat = 49
|
||||
private let containerCornerRadius: CGFloat = 0
|
||||
private let horizontalInset: CGFloat = 0
|
||||
private let iconSize: CGFloat = 32
|
||||
private let iconLabelSpacing: CGFloat = 4
|
||||
private let labelHeight: CGFloat = 11
|
||||
|
|
@ -46,6 +46,7 @@ final class QuickLocationTabBar: UIView {
|
|||
private let items: [TabItem]
|
||||
private var tabViews: [UIView] = []
|
||||
private let containerView = UIView()
|
||||
private let topSeparatorView = UIView()
|
||||
|
||||
private(set) var selectedIndex: Int = 0
|
||||
|
||||
|
|
@ -72,12 +73,11 @@ final class QuickLocationTabBar: UIView {
|
|||
|
||||
containerView.backgroundColor = .white
|
||||
containerView.layer.cornerRadius = containerCornerRadius
|
||||
containerView.layer.shadowColor = UIColor(red: 0.059, green: 0.157, blue: 0.275, alpha: 0.1).cgColor
|
||||
containerView.layer.shadowOffset = .zero
|
||||
containerView.layer.shadowRadius = 8
|
||||
containerView.layer.shadowOpacity = 1
|
||||
addSubview(containerView)
|
||||
|
||||
topSeparatorView.backgroundColor = UIColor(hexStr: "#E8E8E8")
|
||||
addSubview(topSeparatorView)
|
||||
|
||||
for (index, item) in items.enumerated() {
|
||||
let tabView = makeTabView(item: item, index: index)
|
||||
containerView.addSubview(tabView)
|
||||
|
|
@ -86,11 +86,18 @@ final class QuickLocationTabBar: UIView {
|
|||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
topSeparatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
topSeparatorView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topSeparatorView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
topSeparatorView.topAnchor.constraint(equalTo: topAnchor),
|
||||
topSeparatorView.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale),
|
||||
|
||||
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
|
||||
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
|
||||
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -kSafeBottomMargin),
|
||||
containerView.heightAnchor.constraint(equalToConstant: barContentHeight)
|
||||
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
containerView.heightAnchor.constraint(equalToConstant: barContentHeight + kSafeBottomMargin)
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +164,7 @@ final class QuickLocationTabBar: UIView {
|
|||
guard containerWidth > 0, !tabViews.isEmpty else { return }
|
||||
|
||||
let tabWidth = containerWidth / CGFloat(tabViews.count)
|
||||
let tabHeight = containerView.bounds.height
|
||||
let tabHeight = containerView.bounds.height - kSafeBottomMargin
|
||||
|
||||
let totalHeight = iconSize + iconLabelSpacing + labelHeight
|
||||
let startY = (tabHeight - totalHeight) / 2
|
||||
|
|
|
|||
|
|
@ -657,13 +657,13 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
||||
selectedBgView.isHidden = !isSelected
|
||||
// 会员权益
|
||||
if AppContextManager.shared.vip > 1, model.is_online {
|
||||
batteryInfoView.isHidden = model.battery.int == 0
|
||||
// 电量 16是电池图标宽度,右边有电池造型需要减去
|
||||
let batteryPercent = min(CGFloat(model.battery.int), 100)
|
||||
batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
|
||||
batteryLab.text = "\(model.battery)%"
|
||||
}
|
||||
// if AppContextManager.shared.vip > 1, model.is_online {
|
||||
// batteryInfoView.isHidden = model.battery.int == 0
|
||||
// // 电量 16是电池图标宽度,右边有电池造型需要减去
|
||||
// let batteryPercent = min(CGFloat(model.battery.int), 100)
|
||||
// batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
|
||||
// batteryLab.text = "\(model.battery)%"
|
||||
// }
|
||||
}
|
||||
|
||||
private func setupSubviews() {
|
||||
|
|
@ -684,8 +684,8 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
selectedBgView.layoutChain.edges()
|
||||
|
||||
avaterImgView.layoutChain
|
||||
.top(11)
|
||||
.edgesHorzontal(10)
|
||||
.top(9)
|
||||
.edgesHorzontal(5.5)
|
||||
.heightToWidth(1)
|
||||
|
||||
batteryInfoView.layoutChain
|
||||
|
|
|
|||
|
|
@ -161,8 +161,12 @@ class GroupView: UIView {
|
|||
return iv
|
||||
}()
|
||||
|
||||
lazy var navView = BaseNavigationView(title: "圈子")
|
||||
|
||||
lazy var navView: BaseNavigationView = {
|
||||
let nav = BaseNavigationView(title: "圈子")
|
||||
nav.backBtn.isHidden = true
|
||||
return nav
|
||||
}()
|
||||
|
||||
lazy var scanBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setImage(UIImage(named: "Group/scan"), for: .normal)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
// Created by 八条 on 2026/5/29.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
//
|
||||
// GroupMemberView2.swift
|
||||
// QuickLocation
|
||||
//
|
||||
// Created by 八条 on 2026/8/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
class GroupMemberView2: UIView {
|
||||
|
||||
private var navBarHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
/// 最大化时展开导航占位为 kNaviHeight,收起/dismiss 时为 0
|
||||
func setNavBarExpanded(_ expanded: Bool) {
|
||||
navBarHeightConstraint?.constant = expanded ? kNaviHeight : 0
|
||||
}
|
||||
|
||||
func setupMemberInfo(_ model: GroupMemberModel, isOwner: Bool) {
|
||||
memberNameLab.text = model.nick_name
|
||||
ownView.isHidden = !isOwner
|
||||
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
|
||||
statusLab.text = model.is_online ? "在线" : "离线"
|
||||
locationLab.text = model.lastLocation
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(headerBgView)
|
||||
addSubview(navBarView)
|
||||
addSubview(scrollView)
|
||||
|
||||
headerBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(160/375)
|
||||
|
||||
navBarView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(0)
|
||||
navBarHeightConstraint = navBarView.jh_constraint(
|
||||
.height, toAttribute: .notAnAttribute, otherView: nil, relation: .equal
|
||||
)
|
||||
|
||||
scrollView.layoutChain
|
||||
.topToBottomOfView(navBarView)
|
||||
.edges(excludingEdge: .top)
|
||||
scrollView.isScrollEnabled = false
|
||||
|
||||
let lineView = UIView()
|
||||
lineView.backgroundColor = .black.withAlphaComponent(0.2)
|
||||
lineView.cornerRadius = 2
|
||||
addSubview(lineView)
|
||||
lineView.layoutChain
|
||||
.top(9)
|
||||
.centerX()
|
||||
.width(40)
|
||||
.height(4)
|
||||
}
|
||||
|
||||
lazy var headerBgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "Home/group_member_bg2")
|
||||
view.backgroundColor = .clear
|
||||
view.contentMode = .scaleAspectFill
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var navBarView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.backgroundColor = .clear
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.bounces = false
|
||||
|
||||
view.addSubview(scrollContentView)
|
||||
scrollContentView.layoutChain.edges().widthToView(view)
|
||||
|
||||
scrollContentView.addSubview(memberView)
|
||||
memberView.layoutChain
|
||||
.edges()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var scrollContentView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 成员
|
||||
lazy var memberView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
let icon = UIImageView()
|
||||
icon.image = UIImage(named: "Home/group_name_icon")
|
||||
view.addSubview(icon)
|
||||
icon.layoutChain
|
||||
.top(23)
|
||||
.left(15)
|
||||
.width(18)
|
||||
.heightToWidth(1)
|
||||
|
||||
view.addSubview(groupNameLab)
|
||||
groupNameLab.layoutChain
|
||||
.leftToRightOfView(icon, offset: 6)
|
||||
.centerY(icon)
|
||||
|
||||
view.addSubview(changeGroupBtn)
|
||||
changeGroupBtn.layoutChain
|
||||
.centerY(icon)
|
||||
.right(15)
|
||||
.height(20)
|
||||
|
||||
let memberView = UIView()
|
||||
memberView.backgroundColor = .white
|
||||
memberView.cornerRadius = 20
|
||||
memberView.addSubview(memberCV)
|
||||
view.addSubview(memberView)
|
||||
|
||||
memberView.layoutChain
|
||||
.topToBottomOfView(icon, offset: 12)
|
||||
.edgesHorzontal(15)
|
||||
.height(100)
|
||||
|
||||
memberCV.layoutChain.edges(all: 5)
|
||||
|
||||
view.addSubview(memberInfoView)
|
||||
memberInfoView.layoutChain
|
||||
.topToBottomOfView(memberView, offset: 12)
|
||||
.edgesHorzontal(15)
|
||||
.height(150)
|
||||
.bottom()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var groupNameLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var changeGroupBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("切换圈子 ", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#353B4F"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
btn.setImage(UIImage(named: "Common/arrow_right"), for: .normal)
|
||||
btn.semanticContentAttribute = .forceRightToLeft
|
||||
return btn
|
||||
}()
|
||||
|
||||
/// 成员列表
|
||||
lazy var memberCV: UICollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.itemSize = CGSize(width: 61, height: 90)
|
||||
layout.minimumLineSpacing = 15
|
||||
layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||
layout.scrollDirection = .horizontal
|
||||
|
||||
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
cv.backgroundColor = .clear
|
||||
cv.showsHorizontalScrollIndicator = false
|
||||
cv.register(GroupMemberListCell.self)
|
||||
return cv
|
||||
}()
|
||||
|
||||
/// 成员信息
|
||||
lazy var memberInfoView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 20
|
||||
|
||||
view.addSubview(memberNameLab)
|
||||
memberNameLab.layoutChain
|
||||
.top(20)
|
||||
.left(18)
|
||||
|
||||
view.addSubview(tagView)
|
||||
tagView.layoutChain
|
||||
.leftToRightOfView(memberNameLab, offset: 6)
|
||||
.centerY(memberNameLab)
|
||||
.height(20)
|
||||
ownView.layoutChain.height(16)
|
||||
|
||||
let statusView = UIView()
|
||||
statusView.backgroundColor = .clear
|
||||
statusView.addSubview(statusDotView)
|
||||
statusView.addSubview(statusLab)
|
||||
view.addSubview(statusView)
|
||||
statusView.layoutChain
|
||||
.right(18)
|
||||
.centerY(memberNameLab)
|
||||
|
||||
statusDotView.layoutChain
|
||||
.left()
|
||||
.width(8)
|
||||
.heightToWidth(1)
|
||||
.centerY()
|
||||
|
||||
statusLab.layoutChain
|
||||
.leftToRightOfView(statusDotView, offset: 4)
|
||||
.edgesVertical()
|
||||
.right()
|
||||
|
||||
let locationIcon = UIImageView()
|
||||
locationIcon.image = UIImage(named: "Home/member_location")
|
||||
view.addSubview(locationIcon)
|
||||
locationIcon.layoutChain
|
||||
.topToBottomOfView(memberNameLab, offset: 17)
|
||||
.left(20)
|
||||
.width(10)
|
||||
.height(10)
|
||||
|
||||
view.addSubview(locationLab)
|
||||
locationLab.layoutChain
|
||||
.centerY(locationIcon)
|
||||
.leftToRightOfView(locationIcon, offset: 7)
|
||||
|
||||
view.addSubview(distanceView)
|
||||
distanceView.layoutChain
|
||||
.right(18)
|
||||
.centerY(locationLab)
|
||||
|
||||
locationLab.layoutChain.rightToLeftOfView(distanceView, offset: -30, relation: .greaterThanOrEqual)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var memberNameLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 12, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var tagView: UIStackView = {
|
||||
let view = UIStackView(arrangedSubviews: [ownView])
|
||||
view.axis = .horizontal
|
||||
view.alignment = .center
|
||||
view.spacing = 5
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
// 圈主
|
||||
lazy var ownView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#E3F6FF")
|
||||
view.cornerRadius = 6
|
||||
view.isHidden = true
|
||||
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#128CC6")
|
||||
label.font = .systemFont(ofSize: 10, weight: .bold)
|
||||
label.text = "圈主"
|
||||
|
||||
view.addSubview(label)
|
||||
label.layoutChain
|
||||
.edgesVertical(2)
|
||||
.edgesHorzontal(8)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
/// 在线状态
|
||||
lazy var statusDotView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#D8D8D8")
|
||||
view.cornerRadius = 4
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var statusLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
return label
|
||||
}()
|
||||
|
||||
// 位置
|
||||
lazy var locationLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#767676")
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
// 距离
|
||||
lazy var distanceView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#353B4F")
|
||||
view.cornerRadius = 6
|
||||
view.isHidden = true
|
||||
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor(hexStr: "#58EDFF")
|
||||
label.font = .systemFont(ofSize: 10, weight: .bold)
|
||||
label.text = "距离"
|
||||
|
||||
view.addSubview(label)
|
||||
label.layoutChain
|
||||
.left(5)
|
||||
.edgesVertical(3)
|
||||
|
||||
view.addSubview(distanceLab)
|
||||
distanceLab.layoutChain
|
||||
.leftToRightOfView(label, offset: 2)
|
||||
.right(5)
|
||||
.centerY()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var distanceLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = .systemFont(ofSize: 10, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||
cornerRadius = 30
|
||||
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
//
|
||||
// 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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(mapView)
|
||||
addSubview(gpsSignalView)
|
||||
addSubview(warningView)
|
||||
addSubview(inviteBtn)
|
||||
addSubview(locationView)
|
||||
addSubview(groupMemberView)
|
||||
|
||||
mapView.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(40)
|
||||
.height(40)
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
// 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()
|
||||
|
||||
let label = UILabel()
|
||||
label.text = "有权限未开启,数据可能会异常"
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
view.addSubview(label)
|
||||
label.layoutChain
|
||||
.edgesHorzontal(8)
|
||||
.edgesVertical(10)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 定位按钮
|
||||
lazy var locationView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .black.withAlphaComponent(0.4)
|
||||
view.cornerRadius = 20
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -26,13 +26,15 @@ class HomeViewController: BaseViewController {
|
|||
override var preferredStatusBarStyle: UIStatusBarStyle { .default }
|
||||
|
||||
// MARK: - Properties
|
||||
fileprivate var rootView: HomeView!
|
||||
fileprivate var rootView: HomeView2!
|
||||
|
||||
private var tableView: UITableView {
|
||||
rootView.groupMemberView.tableView
|
||||
private var memberCV: UICollectionView {
|
||||
rootView.groupMemberView.memberCV
|
||||
}
|
||||
|
||||
private var viewModel = HomeViewModel()
|
||||
/// 当前选中的成员(用于 GroupMemberListCell 选中态)
|
||||
private var selectedMemberId: String = ""
|
||||
|
||||
private let locationManager = CLLocationManager()
|
||||
private var currentHeading: Double = 0
|
||||
|
|
@ -63,7 +65,7 @@ class HomeViewController: BaseViewController {
|
|||
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
#endif
|
||||
|
||||
rootView = HomeView(frame: UIScreen.main.bounds)
|
||||
rootView = HomeView2(frame: UIScreen.main.bounds)
|
||||
view = rootView
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ class HomeViewController: BaseViewController {
|
|||
setupHeading()
|
||||
reactiveAction()
|
||||
|
||||
rootView.quickMessageView.tagListView.delegate = self
|
||||
// rootView.quickMessageView.tagListView.delegate = self
|
||||
// MQTT 位置上报
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
startLocationTimer()
|
||||
|
|
@ -84,7 +86,7 @@ class HomeViewController: BaseViewController {
|
|||
super.viewDidAppear(animated)
|
||||
|
||||
let manager = AuthorizeManager.manager(type: .locationAlways)
|
||||
rootView.warningIconView.isHidden = manager?.authorizeStatus() == .authorized
|
||||
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
|
|
@ -140,49 +142,49 @@ class HomeViewController: BaseViewController {
|
|||
// MARK: - Actions
|
||||
private func reactiveAction() {
|
||||
// 气泡
|
||||
rootView.bubbleView.rx.tapGesture.subscribe { _ in
|
||||
AppRouter.push(Route.createBubble)
|
||||
}.disposed(by: disposeBag)
|
||||
// rootView.bubbleView.rx.tapGesture.subscribe { _ in
|
||||
// AppRouter.push(Route.createBubble)
|
||||
// }.disposed(by: disposeBag)
|
||||
|
||||
// 签到
|
||||
rootView.signInView.rx.tapGesture.subscribe { _ in
|
||||
let vc = SignInVC(lastLocation: self.lastLocation)
|
||||
vc.isNeedLogin = true
|
||||
AppRouter.push(vc)
|
||||
}.disposed(by: disposeBag)
|
||||
// rootView.signInView.rx.tapGesture.subscribe { _ in
|
||||
// let vc = SignInVC(lastLocation: self.lastLocation)
|
||||
// vc.isNeedLogin = true
|
||||
// AppRouter.push(vc)
|
||||
// }.disposed(by: disposeBag)
|
||||
|
||||
// SOS
|
||||
rootView.sosView.rx.tapGesture.subscribe { _ in
|
||||
let vc = SOSViewController()
|
||||
vc.isNeedLogin = true
|
||||
AppRouter.push(vc)
|
||||
}.disposed(by: disposeBag)
|
||||
// rootView.sosView.rx.tapGesture.subscribe { _ in
|
||||
// let vc = SOSViewController()
|
||||
// vc.isNeedLogin = true
|
||||
// AppRouter.push(vc)
|
||||
// }.disposed(by: disposeBag)
|
||||
|
||||
// 行程
|
||||
rootView.scheduleView.rx.tapGesture.subscribe { _ in
|
||||
guard let model = self.viewModel.groupModel else { return }
|
||||
AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
|
||||
}.disposed(by: disposeBag)
|
||||
// rootView.scheduleView.rx.tapGesture.subscribe { _ in
|
||||
// guard let model = self.viewModel.groupModel else { return }
|
||||
// AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
|
||||
// }.disposed(by: disposeBag)
|
||||
|
||||
// 顶部圈子
|
||||
rootView.groupView.rx.tapGesture.subscribe { _ in
|
||||
guard let groupModel = self.viewModel.groupModel else { return }
|
||||
let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView)
|
||||
let startPointY = groupViewFrame.origin.y + groupViewFrame.height
|
||||
GroupListPopView.show(start: CGPoint(x: 0, y: startPointY + 20),
|
||||
groupModel: groupModel) { groupKey in
|
||||
guard let key = groupKey else { return }
|
||||
self.requestOperateGroup(groupKey: key)
|
||||
}
|
||||
}.disposed(by: disposeBag)
|
||||
// rootView.groupView.rx.tapGesture.subscribe { _ in
|
||||
// guard let groupModel = self.viewModel.groupModel else { return }
|
||||
// let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView)
|
||||
// let startPointY = groupViewFrame.origin.y + groupViewFrame.height
|
||||
// GroupListPopView.show(start: CGPoint(x: 0, y: startPointY + 20),
|
||||
// groupModel: groupModel) { groupKey in
|
||||
// guard let key = groupKey else { return }
|
||||
// self.requestOperateGroup(groupKey: key)
|
||||
// }
|
||||
// }.disposed(by: disposeBag)
|
||||
|
||||
// 圈子成员列表 刷新列表
|
||||
rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
|
||||
self.requestGroupInfo()
|
||||
}).disposed(by: disposeBag)
|
||||
// rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
|
||||
// self.requestGroupInfo()
|
||||
// }).disposed(by: disposeBag)
|
||||
|
||||
// 圈子成员列表 邀请加入
|
||||
rootView.groupMemberView.inviteJoinBtn.rx.tap.subscribe(onNext: { _ in
|
||||
// 邀请加入圈子
|
||||
rootView.inviteBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo])
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -206,7 +208,7 @@ class HomeViewController: BaseViewController {
|
|||
guard let self = self, let model = response.model else { return }
|
||||
Defaults[\.loginToken] = model.token
|
||||
AppContextManager.shared.systemConfig = model.config
|
||||
rootView.searchLottieView.isHidden = model.config?.isIntercept == true
|
||||
// rootView.searchLottieView.isHidden = model.config?.isIntercept == true
|
||||
self.getUserIMToken()
|
||||
self.requestUserInfo { [weak self] in
|
||||
self?.requestGroupInfo()
|
||||
|
|
@ -249,66 +251,85 @@ class HomeViewController: BaseViewController {
|
|||
}.disposed(by: disposeBag)
|
||||
|
||||
// 面板关闭回调
|
||||
rootView.onDismissPanel = { [weak self] in
|
||||
self?.isMemberPanelShown = false
|
||||
(self?.tabBarController as? MainTabBarController)?.setTabBarHidden(false)
|
||||
}
|
||||
// rootView.onDismissPanel = { [weak self] in
|
||||
// self?.isMemberPanelShown = false
|
||||
// (self?.tabBarController as? MainTabBarController)?.setTabBarHidden(false)
|
||||
// }
|
||||
|
||||
// 导航按钮回调
|
||||
rootView.interactionView.onNavigate = { [weak self] in
|
||||
guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
|
||||
self.rootView.dismissMemberPanel()
|
||||
let userCoord = self.lastLocation?.coordinate
|
||||
let groupName = self.viewModel.groupName
|
||||
let model = self.viewModel.groupModel
|
||||
let iconIndex = model?.groups.first(where: { $0.group_key == model?.default_group_key })?.icon_index ?? 1
|
||||
let vc = NavigationVC(member: member, currentUserCoord: userCoord, groupName: groupName, groupIcon: "\(iconIndex)")
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
// rootView.interactionView.onNavigate = { [weak self] in
|
||||
// guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
|
||||
// self.rootView.dismissMemberPanel()
|
||||
// let userCoord = self.lastLocation?.coordinate
|
||||
// let groupName = self.viewModel.groupName
|
||||
// let model = self.viewModel.groupModel
|
||||
// let iconIndex = model?.groups.first(where: { $0.group_key == model?.default_group_key })?.icon_index ?? 1
|
||||
// let vc = NavigationVC(member: member, currentUserCoord: userCoord, groupName: groupName, groupIcon: "\(iconIndex)")
|
||||
// self.navigationController?.pushViewController(vc, animated: true)
|
||||
// }
|
||||
|
||||
// 发送表情
|
||||
rootView.interactionView.onSendEmote = { [weak self] emoteIdx in
|
||||
guard let self = self else { return }
|
||||
self.requestSendEmote(emoteIdx: emoteIdx)
|
||||
}
|
||||
// rootView.interactionView.onSendEmote = { [weak self] emoteIdx in
|
||||
// guard let self = self else { return }
|
||||
// self.requestSendEmote(emoteIdx: emoteIdx)
|
||||
// }
|
||||
|
||||
// 分享
|
||||
rootView.interactionView.onShare = {
|
||||
SharePopView.show()
|
||||
}
|
||||
// rootView.interactionView.onShare = {
|
||||
// SharePopView.show()
|
||||
// }
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.output.sectionedItems
|
||||
.bind(to: tableView.rx.items(dataSource: dataSource))
|
||||
.do(onNext: { [weak self] sections in
|
||||
guard let self = self else { return }
|
||||
let items = sections.flatMap(\.items)
|
||||
guard !items.isEmpty else { return }
|
||||
let stillValid = items.contains { $0.user_id == self.selectedMemberId }
|
||||
if self.selectedMemberId.isEmpty || !stillValid {
|
||||
if let me = items.first(where: { self.viewModel.isCurrentUser(id: $0.user_id) }) {
|
||||
self.selectedMemberId = me.user_id
|
||||
self.rootView.groupMemberView.setupMemberInfo(
|
||||
me,
|
||||
isOwner: self.viewModel.isGroupOwn(id: me.user_id)
|
||||
)
|
||||
} else {
|
||||
self.selectedMemberId = items[0].user_id
|
||||
self.rootView.groupMemberView.setupMemberInfo(
|
||||
items[0],
|
||||
isOwner: self.viewModel.isGroupOwn(id: items[0].user_id)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.bind(to: memberCV.rx.items(dataSource: dataSource))
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
let selectedMember = tableView.rx.modelSelected(GroupMemberModel.self)
|
||||
let selectedMember = memberCV.rx.modelSelected(GroupMemberModel.self)
|
||||
.share()
|
||||
|
||||
selectedMember
|
||||
.subscribe(viewModel.cellAction.inputs)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 选中成员 → 地图定位
|
||||
// 选中成员 → 高亮 + 地图定位
|
||||
selectedMember
|
||||
.subscribe(onNext: { [weak self] model in
|
||||
guard let self = self else { return }
|
||||
self.locateMember(userId: model.user_id)
|
||||
self?.selectMember(userId: model.user_id, centerMap: true, dismissPanel: true)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
lazy private var dataSource: RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
|
||||
return RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel>(
|
||||
configureCell: { (_, tableView, indexPath, model) in
|
||||
let cell: GroupMemberCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(model: model,
|
||||
isCurrentUser: self.viewModel.isCurrentUser(id: model.user_id),
|
||||
isOwn: self.viewModel.isGroupOwn(id: model.user_id))
|
||||
return cell
|
||||
})
|
||||
// MARK: - UICollectionViewDataSource
|
||||
lazy private var dataSource: RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
|
||||
RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> { [weak self] _, collectionView, indexPath, model in
|
||||
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(model: model,
|
||||
isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false,
|
||||
isSelected: model.user_id == self?.selectedMemberId)
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: - API
|
||||
|
|
@ -329,7 +350,7 @@ class HomeViewController: BaseViewController {
|
|||
UserService.userInfo().subscribe { response in
|
||||
guard let model = response.model else { return }
|
||||
AppContextManager.shared.saveAccount(model)
|
||||
self.rootView.avatarImgView.image = model.userIcon
|
||||
// self.rootView.avatarImgView.image = model.userIcon
|
||||
completion?()
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
|
@ -341,8 +362,8 @@ class HomeViewController: BaseViewController {
|
|||
NotificationCenter.default.post(name: .RefreshIMGroupListNotification, object: nil)
|
||||
|
||||
guard isDefaultGroup else { return }
|
||||
self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, 1)
|
||||
self.rootView.groupNameLab.text = self.viewModel.groupName
|
||||
// self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, 1)
|
||||
self.rootView.groupMemberView.groupNameLab.text = self.viewModel.groupName
|
||||
self.syncMemberAnnotations(model.select_group_employee)
|
||||
self.refreshMQTTSubscriptions(model.select_group_employee)
|
||||
}.disposed(by: disposeBag)
|
||||
|
|
@ -355,19 +376,19 @@ class HomeViewController: BaseViewController {
|
|||
}
|
||||
|
||||
private func requestNotice() {
|
||||
UserService.notice().subscribe { response in
|
||||
self.rootView.noticeLab.text = ""
|
||||
guard let data = response.data, let noticeList = data["notice"] as? [String] else { return }
|
||||
self.rootView.noticeView.isHidden = noticeList.count == 0
|
||||
self.rootView.noticeLab.text = noticeList.joined(separator: " ") + " "
|
||||
if let loop = data["loop"] as? Bool {
|
||||
self.rootView.noticeLab.type = loop ? .continuous : .leftRight
|
||||
self.rootView.noticeLab.onScrollLoopComplete = {
|
||||
guard loop == false else { return }
|
||||
self.rootView.noticeView.isHidden = true
|
||||
}
|
||||
}
|
||||
}.disposed(by: disposeBag)
|
||||
// UserService.notice().subscribe { response in
|
||||
// self.rootView.noticeLab.text = ""
|
||||
// guard let data = response.data, let noticeList = data["notice"] as? [String] else { return }
|
||||
// self.rootView.noticeView.isHidden = noticeList.count == 0
|
||||
// self.rootView.noticeLab.text = noticeList.joined(separator: " ") + " "
|
||||
// if let loop = data["loop"] as? Bool {
|
||||
// self.rootView.noticeLab.type = loop ? .continuous : .leftRight
|
||||
// self.rootView.noticeLab.onScrollLoopComplete = {
|
||||
// guard loop == false else { return }
|
||||
// self.rootView.noticeView.isHidden = true
|
||||
// }
|
||||
// }
|
||||
// }.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - Map Setup
|
||||
|
|
@ -404,7 +425,7 @@ class HomeViewController: BaseViewController {
|
|||
if hitRect.contains(point) {
|
||||
self.isMemberPanelShown = true
|
||||
(self.tabBarController as? MainTabBarController)?.setTabBarHidden(true)
|
||||
rootView.showMemberPanel(member: ann.member)
|
||||
// rootView.showMemberPanel(member: ann.member)
|
||||
viewModel.targetUid = ann.member.id
|
||||
break
|
||||
}
|
||||
|
|
@ -511,14 +532,58 @@ class HomeViewController: BaseViewController {
|
|||
#endif
|
||||
}
|
||||
|
||||
/// 选中成员:同步列表高亮 / 信息卡,并可选居中地图、收起面板
|
||||
private func selectMember(userId: String, centerMap: Bool = true, dismissPanel: Bool = false) {
|
||||
let listUserId = (userId == "current") ? AppContextManager.shared.userId : userId
|
||||
guard !listUserId.isEmpty else { return }
|
||||
|
||||
selectedMemberId = listUserId
|
||||
if let model = viewModel.memberList.first(where: { $0.user_id == listUserId }) {
|
||||
rootView.groupMemberView.setupMemberInfo(
|
||||
model,
|
||||
isOwner: viewModel.isGroupOwn(id: model.user_id)
|
||||
)
|
||||
}
|
||||
memberCV.reloadData()
|
||||
|
||||
if let idx = viewModel.memberList.firstIndex(where: { $0.user_id == listUserId }) {
|
||||
let indexPath = IndexPath(item: idx, section: 0)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self,
|
||||
self.memberCV.numberOfSections > 0,
|
||||
self.memberCV.numberOfItems(inSection: 0) > idx else { return }
|
||||
self.memberCV.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
if dismissPanel {
|
||||
rootView.dismissGroupMemberView()
|
||||
}
|
||||
if centerMap {
|
||||
centerMapOnMember(userId: userId)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
/// 地图居中到成员(当前用户 id 为 "current" 或真实 userId)
|
||||
private func centerMapOnMember(userId: String) {
|
||||
let coordinate: CLLocationCoordinate2D?
|
||||
if userId == "current" || viewModel.isCurrentUser(id: userId) {
|
||||
coordinate = currentUserAnnotation?.coordinate
|
||||
?? lastLocation?.coordinate
|
||||
} else {
|
||||
coordinate = members.first(where: { $0.id == userId })?.coordinate
|
||||
}
|
||||
guard let coord = coordinate, CLLocationCoordinate2DIsValid(coord) else { return }
|
||||
rootView.mapView.setCenter(coord, animated: true)
|
||||
rootView.mapView.setZoomLevel(16, animated: true)
|
||||
}
|
||||
|
||||
/// 选中成员后地图定位到该成员,收起 GroupMemberView
|
||||
private func locateMember(userId: String) {
|
||||
rootView.dismissGroupMemberView()
|
||||
guard let member = members.first(where: { $0.id == userId }),
|
||||
CLLocationCoordinate2DIsValid(member.coordinate) else { return }
|
||||
rootView.mapView.setCenter(member.coordinate, animated: true)
|
||||
rootView.mapView.setZoomLevel(16, animated: true)
|
||||
selectMember(userId: userId, centerMap: true, dismissPanel: true)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -623,28 +688,28 @@ extension HomeViewController {
|
|||
let emoteUserId = parts[1]
|
||||
|
||||
if firstChar == "1" || firstChar == "2" { // 表情
|
||||
let emojiFileName = firstChar == "1" ? "normal_" : "fun_"
|
||||
// let emojiFileName = firstChar == "1" ? "normal_" : "fun_"
|
||||
let nickName = self.viewModel.getUserNickName(id: emoteUserId)
|
||||
let fullText = nickName + "对你发送了表情"
|
||||
let attr = NSMutableAttributedString(string: fullText)
|
||||
attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: NSRange(location: 0, length: fullText.count))
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(location: 0, length: nickName.count))
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.rootView.messageLab.attributedText = attr
|
||||
self.rootView.messageBubbleView.isHidden = false
|
||||
if let path = Bundle.main.path(forResource: "\(emojiFileName)\(lastChar)", ofType: "json") {
|
||||
self.rootView.emojiPopView.animation = LottieAnimation.filepath(path)
|
||||
self.rootView.bringSubviewToFront(self.rootView.emojiPopView)
|
||||
self.rootView.emojiPopView.isHidden = false
|
||||
self.rootView.emojiPopView.play { completed in
|
||||
self.rootView.emojiPopView.isHidden = true
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
self.rootView.messageBubbleView.isHidden = true
|
||||
}
|
||||
}
|
||||
// DispatchQueue.main.async { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// self.rootView.messageLab.attributedText = attr
|
||||
// self.rootView.messageBubbleView.isHidden = false
|
||||
// if let path = Bundle.main.path(forResource: "\(emojiFileName)\(lastChar)", ofType: "json") {
|
||||
// self.rootView.emojiPopView.animation = LottieAnimation.filepath(path)
|
||||
// self.rootView.bringSubviewToFront(self.rootView.emojiPopView)
|
||||
// self.rootView.emojiPopView.isHidden = false
|
||||
// self.rootView.emojiPopView.play { completed in
|
||||
// self.rootView.emojiPopView.isHidden = true
|
||||
// }
|
||||
// }
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
// self.rootView.messageBubbleView.isHidden = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
else if firstChar == "3" { // 文字
|
||||
let textIdx = lastChar.integer
|
||||
|
|
@ -656,14 +721,14 @@ extension HomeViewController {
|
|||
attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: NSRange(location: 0, length: fullText.count))
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(location: 0, length: nickName.count))
|
||||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#333333"), range: NSRange(location: nickName.count, length: texts[textIdx].count))
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.rootView.messageLab.attributedText = attr
|
||||
self.rootView.messageBubbleView.isHidden = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
self.rootView.messageBubbleView.isHidden = true
|
||||
}
|
||||
}
|
||||
// DispatchQueue.main.async { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// self.rootView.messageLab.attributedText = attr
|
||||
// self.rootView.messageBubbleView.isHidden = false
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
// self.rootView.messageBubbleView.isHidden = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
case "bubble": // 气泡
|
||||
guard let userId = msg.data?.user_id else { return }
|
||||
|
|
@ -683,18 +748,18 @@ extension HomeViewController {
|
|||
guard !self.isSOSPlaying else { return }
|
||||
self.isSOSPlaying = true
|
||||
self.playSOSAlarm()
|
||||
self.rootView.sosPopView.isHidden = false
|
||||
self.rootView.sosPopView.play { completed in
|
||||
self.rootView.sosPopView.stop()
|
||||
self.rootView.sosPopView.isHidden = true
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
|
||||
self?.sosPlayer?.stop()
|
||||
self?.sosPlayer = nil
|
||||
self?.rootView.sosPopView.stop()
|
||||
self?.rootView.sosPopView.isHidden = true
|
||||
self?.isSOSPlaying = false
|
||||
}
|
||||
// self.rootView.sosPopView.isHidden = false
|
||||
// self.rootView.sosPopView.play { completed in
|
||||
// self.rootView.sosPopView.stop()
|
||||
// self.rootView.sosPopView.isHidden = true
|
||||
// }
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
|
||||
// self?.sosPlayer?.stop()
|
||||
// self?.sosPlayer = nil
|
||||
// self?.rootView.sosPopView.stop()
|
||||
// self?.rootView.sosPopView.isHidden = true
|
||||
// self?.isSOSPlaying = false
|
||||
// }
|
||||
}
|
||||
case "join": // 圈子有成员加入
|
||||
guard let gk = msg.data?.group_key,
|
||||
|
|
@ -726,7 +791,7 @@ extension HomeViewController {
|
|||
guard self.viewModel.memberIsExist(userId: userId) else { return }
|
||||
self.viewModel.removeMember(userId: userId)
|
||||
self.removeAnnotation(userId: userId)
|
||||
self.rootView.groupMemberView.setupCountData(self.viewModel.memberList.count, 1)
|
||||
// self.rootView.groupMemberView.setupCountData(self.viewModel.memberList.count, 1)
|
||||
MQTTService.shared.unsubscribe(topic: "smartdrive/\(userId)")
|
||||
}
|
||||
case "dismiss": // 圈子解散
|
||||
|
|
@ -754,7 +819,7 @@ extension HomeViewController {
|
|||
private func updateOnlineCount() {
|
||||
let total = viewModel.memberCount
|
||||
let online = viewModel.memberList.filter { $0.is_online }.count
|
||||
rootView.groupMemberView.setupCountData(total, online)
|
||||
// rootView.groupMemberView.setupCountData(total, online)
|
||||
}
|
||||
|
||||
/// 移除指定成员的标注
|
||||
|
|
@ -818,8 +883,9 @@ extension HomeViewController: MAMapViewDelegate {
|
|||
}
|
||||
|
||||
annotationView?.configure(with: memberAnnotation.member)
|
||||
|
||||
|
||||
annotationView?.onTap = { [weak self] ann in
|
||||
self?.selectMember(userId: ann.member.id, centerMap: true, dismissPanel: false)
|
||||
}
|
||||
|
||||
if memberAnnotation.member.isCurrentUser {
|
||||
currentUserAnnotation = memberAnnotation
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class LoginView: UIView {
|
|||
}
|
||||
|
||||
func setupAgreementTextWithCarrier(carrier: String) {
|
||||
let text = carrier.isEmpty ? "登录即同意《隐私政策》和《用户协议》" : "登录即同意\(carrier)、《隐私政策》和《用户协议》"
|
||||
let text = carrier.isEmpty ? "登录即同意《用户协议》和《隐私政策》" : "登录即同意\(carrier)、《隐私政策》和《用户协议》"
|
||||
agreementLabel.text = text
|
||||
// 创建段落样式
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
|
|
@ -65,27 +65,31 @@ class LoginView: UIView {
|
|||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(bgMaskImage)
|
||||
// addSubview(bgMaskImage)
|
||||
addSubview(backBtn)
|
||||
addSubview(guestLoginButton)
|
||||
addSubview(welcomeView)
|
||||
welcomeView.addSubview(welcomeLineView)
|
||||
welcomeView.addSubview(welcomeLabel)
|
||||
welcomeView.addSubview(welcomeTitleLab)
|
||||
|
||||
addSubview(inputContainerView)
|
||||
inputContainerView.addSubview(welcomeLabel)
|
||||
inputContainerView.addSubview(phoneNumberLabel)
|
||||
inputContainerView.addSubview(carrierLabel)
|
||||
inputContainerView.addSubview(otherPhoneView)
|
||||
otherPhoneView.addSubview(phoneInputView)
|
||||
otherPhoneView.addSubview(smsCodeInputView)
|
||||
inputContainerView.addSubview(loginButton)
|
||||
inputContainerView.addSubview(phoneInputView)
|
||||
inputContainerView.addSubview(smsCodeInputView)
|
||||
addSubview(loginButton)
|
||||
addSubview(appleLoginBtn)
|
||||
addSubview(wechatLoginBtn)
|
||||
addSubview(phoneLoginBtn)
|
||||
addSubview(otherLoginTitleLab)
|
||||
addSubview(leftSeparator)
|
||||
addSubview(rightSeparator)
|
||||
|
||||
addSubview(agreementView)
|
||||
agreementView.addSubview(checkBox)
|
||||
agreementView.addSubview(agreementTV)
|
||||
agreementView.addSubview(agreementLabel)
|
||||
|
||||
bgMaskImage.layoutChain.edges()
|
||||
// bgMaskImage.layoutChain.edges()
|
||||
|
||||
backBtn.layoutChain
|
||||
.top(54)
|
||||
|
|
@ -100,31 +104,30 @@ class LoginView: UIView {
|
|||
.height(29)
|
||||
|
||||
inputContainerView.layoutChain
|
||||
.centerX()
|
||||
.centerY(self, offset: -17)
|
||||
.centerY()
|
||||
.edgesHorzontal()
|
||||
.height(306)
|
||||
|
||||
|
||||
welcomeView.layoutChain
|
||||
.bottomToTopOfView(inputContainerView, offset: -100)
|
||||
.left(38)
|
||||
|
||||
welcomeLineView.layoutChain
|
||||
.edges(excludingEdge: .right)
|
||||
.width(4)
|
||||
|
||||
welcomeLabel.layoutChain
|
||||
.top(0)
|
||||
.centerX()
|
||||
.edgesHorzontal()
|
||||
.top()
|
||||
.leftToRightOfView(welcomeLineView, offset: 12)
|
||||
|
||||
phoneNumberLabel.layoutChain
|
||||
.topToBottomOfView(welcomeLabel, offset: 78)
|
||||
.centerX()
|
||||
|
||||
carrierLabel.layoutChain
|
||||
.topToBottomOfView(phoneNumberLabel, offset: 4)
|
||||
.centerX()
|
||||
|
||||
otherPhoneView.layoutChain
|
||||
.topToBottomOfView(welcomeLabel, offset: 43)
|
||||
.centerX()
|
||||
welcomeTitleLab.layoutChain
|
||||
.topToBottomOfView(welcomeLabel)
|
||||
.leftToView(welcomeLabel)
|
||||
.right()
|
||||
.bottom()
|
||||
|
||||
phoneInputView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.width(259)
|
||||
.edgesHorzontal(38)
|
||||
.height(50)
|
||||
|
||||
phoneLineView.layoutChain
|
||||
|
|
@ -145,13 +148,14 @@ class LoginView: UIView {
|
|||
|
||||
smsCodeInputView.layoutChain
|
||||
.topToBottomOfView(phoneInputView, offset: 20)
|
||||
.edges(excludingEdge: .top)
|
||||
.widthToView(phoneInputView)
|
||||
.leftToView(phoneInputView)
|
||||
.rightToView(phoneInputView)
|
||||
.heightToView(phoneInputView)
|
||||
.bottom()
|
||||
|
||||
smsCodeBtn.layoutChain
|
||||
.edgesVertical()
|
||||
.right()
|
||||
.edgesVertical(5)
|
||||
.right(5)
|
||||
.width(100)
|
||||
|
||||
smsCodeTF.layoutChain
|
||||
|
|
@ -160,15 +164,13 @@ class LoginView: UIView {
|
|||
.rightToLeftOfView(smsCodeBtn)
|
||||
|
||||
loginButton.layoutChain
|
||||
.topToBottomOfView(otherPhoneView, offset: 63)
|
||||
.centerX()
|
||||
.width(247)
|
||||
.height(50)
|
||||
.topToBottomOfView(inputContainerView, offset: 46)
|
||||
.edgesHorzontal(30)
|
||||
.height(56)
|
||||
|
||||
agreementView.layoutChain
|
||||
.leftToView(loginButton)
|
||||
.rightToView(loginButton)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
.topToBottomOfView(loginButton, offset: 14)
|
||||
.centerX()
|
||||
.height(30)
|
||||
|
||||
agreementLabel.layoutChain.edges()
|
||||
|
|
@ -185,9 +187,9 @@ class LoginView: UIView {
|
|||
.top()
|
||||
|
||||
phoneLoginBtn.layoutChain
|
||||
.bottomToTopOfView(agreementView, offset: -30)
|
||||
.bottom(kSafeBottomMargin + 30)
|
||||
.centerX()
|
||||
.width(38)
|
||||
.width(40)
|
||||
.heightToWidth(1)
|
||||
|
||||
appleLoginBtn.layoutChain
|
||||
|
|
@ -201,6 +203,22 @@ class LoginView: UIView {
|
|||
.centerY(phoneLoginBtn)
|
||||
.widthToView(phoneLoginBtn)
|
||||
.heightToView(phoneLoginBtn)
|
||||
|
||||
otherLoginTitleLab.layoutChain
|
||||
.bottomToTopOfView(phoneLoginBtn, offset: -18)
|
||||
.centerX()
|
||||
|
||||
leftSeparator.layoutChain
|
||||
.centerY(otherLoginTitleLab)
|
||||
.rightToLeftOfView(otherLoginTitleLab, offset: -14)
|
||||
.width(60)
|
||||
.height(2)
|
||||
|
||||
rightSeparator.layoutChain
|
||||
.centerY(otherLoginTitleLab)
|
||||
.leftToRightOfView(otherLoginTitleLab, offset: 14)
|
||||
.widthToView(leftSeparator)
|
||||
.heightToView(leftSeparator)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
|
@ -215,6 +233,24 @@ class LoginView: UIView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
leftSeparator.layoutIfNeeded()
|
||||
leftSeparator.setGradientLayer(frame: leftSeparator.bounds,
|
||||
startPoint: CGPoint(x: 0, y: 0.5),
|
||||
endPoint: CGPoint(x: 1, y: 0.5),
|
||||
colors: [UIColor(hexStr: "#D8D8D8", alpha: 0), UIColor(hexStr: "#E5E5E5")],
|
||||
locations: [0, 1])
|
||||
|
||||
rightSeparator.layoutIfNeeded()
|
||||
rightSeparator.setGradientLayer(frame: rightSeparator.bounds,
|
||||
startPoint: CGPoint(x: 0, y: 0.5),
|
||||
endPoint: CGPoint(x: 1, y: 0.5),
|
||||
colors: [UIColor(hexStr: "#E5E5E5"), UIColor(hexStr: "#D8D8D8", alpha: 0)],
|
||||
locations: [0, 1])
|
||||
}
|
||||
|
||||
// MARK: - UI Components
|
||||
lazy var bgMaskImage: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
|
|
@ -240,12 +276,32 @@ class LoginView: UIView {
|
|||
return btn
|
||||
}()
|
||||
|
||||
lazy var welcomeView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var welcomeLineView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#99F2FC")
|
||||
view.cornerRadius = 2
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var welcomeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "欢迎登录"
|
||||
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#030303")
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
label.textColor = .black
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var welcomeTitleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "极速定位"
|
||||
label.font = .systemFont(ofSize: 32, weight: .bold)
|
||||
label.textColor = .black
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
@ -271,22 +327,12 @@ class LoginView: UIView {
|
|||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
// 其他手机号登录
|
||||
lazy var otherPhoneView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
// view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
// 手机号
|
||||
lazy var phoneInputView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.borderWidth = 1
|
||||
view.borderColor = .white
|
||||
view.cornerRadius = 8
|
||||
view.backgroundColor = UIColor(hexStr: "#F8F8F8")
|
||||
view.cornerRadius = 12
|
||||
view.addSubview(areaCodeLab)
|
||||
view.addSubview(phoneLineView)
|
||||
view.addSubview(phoneInputTF)
|
||||
|
|
@ -297,14 +343,14 @@ class LoginView: UIView {
|
|||
let label = UILabel()
|
||||
label.text = "+86"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = .white
|
||||
label.textColor = UIColor(hexStr: "#3D3D3D")
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var phoneLineView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.backgroundColor = UIColor(hexStr: "#D8D8D8")
|
||||
return view
|
||||
}()
|
||||
|
||||
|
|
@ -312,7 +358,7 @@ class LoginView: UIView {
|
|||
let textField = UITextField()
|
||||
textField.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
textField.placeholderColor(placeholder: "请输入手机号码",
|
||||
color: .white)
|
||||
color: UIColor(hexStr: "#767676"))
|
||||
// textField.tintColor = ThemeManager.shared.color.mainColor
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.keyboardType = .numberPad
|
||||
|
|
@ -322,10 +368,8 @@ class LoginView: UIView {
|
|||
// 验证码
|
||||
lazy var smsCodeInputView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.borderWidth = 1
|
||||
view.borderColor = .white
|
||||
view.cornerRadius = 8
|
||||
view.backgroundColor = UIColor(hexStr: "#F8F8F8")
|
||||
view.cornerRadius = 12
|
||||
view.addSubview(smsCodeTF)
|
||||
view.addSubview(smsCodeBtn)
|
||||
return view
|
||||
|
|
@ -335,29 +379,53 @@ class LoginView: UIView {
|
|||
let textField = UITextField()
|
||||
textField.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
textField.placeholderColor(placeholder: "请输入验证码",
|
||||
color: .white)
|
||||
color: UIColor(hexStr: "#767676"))
|
||||
textField.keyboardType = .numberPad
|
||||
return textField
|
||||
}()
|
||||
|
||||
lazy var smsCodeBtn: UIButton = {
|
||||
let button = UIButton()
|
||||
button.backgroundColor = .white
|
||||
button.cornerRadius = 8
|
||||
button.setTitle("获取验证码", for: .normal)
|
||||
button.setTitleColor(UIColor(hexStr: "#030303"), for: .normal)
|
||||
button.setTitleColor(UIColor(hexStr: "#999999", alpha: 1.0), for: .disabled)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
button.setTitleColor(UIColor(hexStr: "#1A1A1A"), for: .normal)
|
||||
button.setTitleColor(UIColor(hexStr: "#767676"), for: .disabled)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var loginButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.setTitle("立即登录", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Login/login_bg"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 20
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var otherLoginTitleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "其他登录方式"
|
||||
label.font = .systemFont(ofSize: 14, weight: .regular)
|
||||
label.textColor = UIColor(hexStr: "#767676")
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var leftSeparator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var rightSeparator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var wechatLoginBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setBackgroundImage(UIImage(named: "Login/wechat"), for: .normal)
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ class OneTapLoginView: UIView {
|
|||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(bgMaskImage)
|
||||
// addSubview(bgMaskImage)
|
||||
addSubview(backBtn)
|
||||
addSubview(inputContainerView)
|
||||
inputContainerView.addSubview(welcomeLabel)
|
||||
// inputContainerView.addSubview(welcomeLabel)
|
||||
inputContainerView.addSubview(phoneNumberLabel)
|
||||
inputContainerView.addSubview(carrierLabel)
|
||||
inputContainerView.addSubview(loginButton)
|
||||
|
|
@ -70,7 +70,7 @@ class OneTapLoginView: UIView {
|
|||
agreementView.addSubview(agreementLabel)
|
||||
agreementView.addSubview(agreementTV)
|
||||
|
||||
bgMaskImage.layoutChain.edges()
|
||||
// bgMaskImage.layoutChain.edges()
|
||||
|
||||
backBtn.layoutChain
|
||||
.top(54)
|
||||
|
|
@ -84,28 +84,27 @@ class OneTapLoginView: UIView {
|
|||
.edgesHorzontal()
|
||||
.height(301)
|
||||
|
||||
welcomeLabel.layoutChain
|
||||
.top(0)
|
||||
.centerX()
|
||||
.edgesHorzontal()
|
||||
// welcomeLabel.layoutChain
|
||||
// .top(0)
|
||||
// .centerX()
|
||||
// .edgesHorzontal()
|
||||
|
||||
phoneNumberLabel.layoutChain
|
||||
.topToBottomOfView(welcomeLabel, offset: 78)
|
||||
.top()
|
||||
.centerX()
|
||||
|
||||
carrierLabel.layoutChain
|
||||
.topToBottomOfView(phoneNumberLabel, offset: 4)
|
||||
.topToBottomOfView(phoneNumberLabel, offset: 14)
|
||||
.centerX()
|
||||
|
||||
loginButton.layoutChain
|
||||
.topToBottomOfView(carrierLabel, offset: 99)
|
||||
.centerX()
|
||||
.width(247)
|
||||
.height(50)
|
||||
.edgesHorzontal(30)
|
||||
.height(56)
|
||||
|
||||
agreementView.layoutChain
|
||||
.topToBottomOfView(inputContainerView, offset: 14)
|
||||
.edgesHorzontal(30)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
|
||||
checkBox.layoutChain
|
||||
.left()
|
||||
|
|
@ -166,7 +165,7 @@ class OneTapLoginView: UIView {
|
|||
|
||||
lazy var welcomeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "欢迎登录"
|
||||
// label.text = "欢迎登录"
|
||||
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#030303")
|
||||
label.textAlignment = .center
|
||||
|
|
@ -175,16 +174,16 @@ class OneTapLoginView: UIView {
|
|||
|
||||
lazy var phoneNumberLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 20, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.font = .systemFont(ofSize: 32, weight: .heavy)
|
||||
label.textColor = UIColor(hexStr: "#3D3D3D")
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var carrierLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12)
|
||||
label.textColor = ThemeManager.shared.color.contentColor
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#767676")
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
|
@ -192,9 +191,10 @@ class OneTapLoginView: UIView {
|
|||
lazy var loginButton: UIButton = {
|
||||
let btn = UIButton(type: .system)
|
||||
btn.setTitle("本机号码一键登录", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Login/login_bg"), for: .normal)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||
btn.cornerRadius = 20
|
||||
return btn
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,53 +13,51 @@ final class MemberAnnotationView: MAAnnotationView {
|
|||
var onTap: ((MemberAnnotation) -> Void)?
|
||||
|
||||
// MARK: - Design Constants
|
||||
static let avatarOuterSize: CGFloat = 50
|
||||
static let avatarInnerSize: CGFloat = 48
|
||||
static let nameTagWidth: CGFloat = 60
|
||||
static let nameTagHeight: CGFloat = 20
|
||||
static let totalHeight: CGFloat = avatarOuterSize + 4 + nameTagHeight
|
||||
static let radarSize: CGFloat = 180
|
||||
private static let bubbleSize: CGFloat = 56
|
||||
private static let bubbleCorner: CGFloat = 14
|
||||
private static let borderWidth: CGFloat = 4
|
||||
private static let pointerWidth: CGFloat = 12
|
||||
private static let pointerHeight: CGFloat = 7
|
||||
private static let gapBubbleToDot: CGFloat = 4
|
||||
private static let gapNameToBubble: CGFloat = 4
|
||||
private static let nameTagHeight: CGFloat = 20
|
||||
private static let nameTagHPadding: CGFloat = 8
|
||||
private static let dotOuterSize: CGFloat = 12
|
||||
private static let dotInnerSize: CGFloat = 10
|
||||
|
||||
private let containerView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .clear
|
||||
return v
|
||||
}()
|
||||
private static let currentUserDotColor = UIColor(hexStr: "#5ED4FF")
|
||||
private static let memberDotPalette: [UIColor] = [
|
||||
UIColor(hexStr: "#FF6B6B"),
|
||||
UIColor(hexStr: "#FF9F43"),
|
||||
UIColor(hexStr: "#FECA57"),
|
||||
UIColor(hexStr: "#1DD1A1"),
|
||||
UIColor(hexStr: "#54A0FF"),
|
||||
UIColor(hexStr: "#5F27CD"),
|
||||
UIColor(hexStr: "#FF9FF3"),
|
||||
UIColor(hexStr: "#00D2D3"),
|
||||
UIColor(hexStr: "#EE5A24"),
|
||||
UIColor(hexStr: "#10AC84"),
|
||||
UIColor(hexStr: "#2E86DE"),
|
||||
UIColor(hexStr: "#341F97"),
|
||||
]
|
||||
|
||||
private let avatarOuterCircle: UIView = {
|
||||
private var showsNameTag = false
|
||||
|
||||
private let containerView = UIView()
|
||||
|
||||
private let nameTagView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
v.layer.cornerRadius = avatarOuterSize / 2
|
||||
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.15).cgColor
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
v.layer.shadowRadius = 4
|
||||
v.layer.shadowOpacity = 1
|
||||
return v
|
||||
}()
|
||||
|
||||
private let avatarImageView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.layer.cornerRadius = avatarInnerSize / 2
|
||||
iv.layer.borderWidth = 1
|
||||
iv.layer.borderColor = UIColor.white.cgColor
|
||||
iv.clipsToBounds = true
|
||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
return iv
|
||||
}()
|
||||
|
||||
private lazy var nameTagView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
v.layer.cornerRadius = 4
|
||||
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
|
||||
v.layer.cornerRadius = 8
|
||||
v.layer.shadowColor = UIColor.black.cgColor
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||
v.layer.shadowRadius = 2
|
||||
v.layer.shadowOpacity = 1
|
||||
v.layer.shadowOpacity = 0.12
|
||||
v.isHidden = true
|
||||
return v
|
||||
}()
|
||||
|
||||
private lazy var nameLabel: UILabel = {
|
||||
private let nameLabel: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = UIFont(name: "PingFangSC-Medium", size: 11)
|
||||
?? UIFont.systemFont(ofSize: 11, weight: .medium)
|
||||
|
|
@ -69,10 +67,55 @@ final class MemberAnnotationView: MAAnnotationView {
|
|||
return l
|
||||
}()
|
||||
|
||||
// MARK: - Layers
|
||||
private var headingLayer: CAShapeLayer?
|
||||
private var pulseLayer: CAShapeLayer?
|
||||
private var isCurrentUser: Bool = false
|
||||
private let bubbleView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
v.layer.cornerRadius = bubbleCorner
|
||||
v.layer.shadowColor = UIColor.black.cgColor
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
v.layer.shadowRadius = 4
|
||||
v.layer.shadowOpacity = 0.18
|
||||
return v
|
||||
}()
|
||||
|
||||
private let avatarImageView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.clipsToBounds = true
|
||||
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||
iv.layer.cornerRadius = bubbleCorner - borderWidth * 0.6
|
||||
return iv
|
||||
}()
|
||||
|
||||
private let pointerLayer: CAShapeLayer = {
|
||||
let layer = CAShapeLayer()
|
||||
layer.fillColor = UIColor.white.cgColor
|
||||
return layer
|
||||
}()
|
||||
|
||||
private let dotOuterView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
v.layer.cornerRadius = dotOuterSize / 2
|
||||
v.layer.shadowColor = UIColor.black.cgColor
|
||||
v.layer.shadowOffset = CGSize(width: 0, height: 1)
|
||||
v.layer.shadowRadius = 2
|
||||
v.layer.shadowOpacity = 0.2
|
||||
return v
|
||||
}()
|
||||
|
||||
private let dotInnerView: UIView = {
|
||||
let v = UIView()
|
||||
v.layer.cornerRadius = dotInnerSize / 2
|
||||
return v
|
||||
}()
|
||||
|
||||
private let dotCenterView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
v.layer.cornerRadius = 2
|
||||
return v
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
|
||||
|
|
@ -89,34 +132,41 @@ final class MemberAnnotationView: MAAnnotationView {
|
|||
isEnabled = true
|
||||
|
||||
addSubview(containerView)
|
||||
|
||||
containerView.addSubview(avatarOuterCircle)
|
||||
avatarOuterCircle.addSubview(avatarImageView)
|
||||
|
||||
containerView.addSubview(nameTagView)
|
||||
nameTagView.addSubview(nameLabel)
|
||||
containerView.layer.addSublayer(pointerLayer)
|
||||
containerView.addSubview(bubbleView)
|
||||
bubbleView.addSubview(avatarImageView)
|
||||
containerView.addSubview(dotOuterView)
|
||||
dotOuterView.addSubview(dotInnerView)
|
||||
dotInnerView.addSubview(dotCenterView)
|
||||
}
|
||||
|
||||
// MARK: - Configure
|
||||
func configure(with member: CircleMember) {
|
||||
self.isEnabled = true
|
||||
avatarImageView.image = UIImage(named: "UserIcon/\(member.avatar)")
|
||||
avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear
|
||||
nameLabel.text = member.name
|
||||
isCurrentUser = member.isCurrentUser
|
||||
isEnabled = true
|
||||
canShowCallout = false
|
||||
|
||||
// 非当前用户标注可点击弹出面板
|
||||
if !member.isCurrentUser && gestureRecognizers?.isEmpty ?? true {
|
||||
if let img = UIImage(named: "UserIcon/\(member.avatar)") {
|
||||
avatarImageView.image = img
|
||||
} else {
|
||||
avatarImageView.image = UIImage(named: "Common/default_avatar")
|
||||
}
|
||||
avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear
|
||||
|
||||
dotInnerView.backgroundColor = Self.dotColor(for: member)
|
||||
|
||||
showsNameTag = !member.isCurrentUser
|
||||
nameLabel.text = member.name
|
||||
nameTagView.isHidden = !showsNameTag
|
||||
|
||||
// 所有标注(含当前用户)可点击,同步列表选中
|
||||
if gestureRecognizers?.isEmpty ?? true {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
if member.isCurrentUser {
|
||||
configureAsCurrentUser(heading: member.heading)
|
||||
} else {
|
||||
configureAsMember()
|
||||
}
|
||||
layoutMarker()
|
||||
}
|
||||
|
||||
@objc private func handleTap() {
|
||||
|
|
@ -124,144 +174,94 @@ final class MemberAnnotationView: MAAnnotationView {
|
|||
onTap?(ann)
|
||||
}
|
||||
|
||||
private func configureAsCurrentUser(heading: Double) {
|
||||
nameTagView.isHidden = true
|
||||
stopPulseAnimation()
|
||||
/// 朝向扇形已从图示样式中移除,保留 API 兼容 HomeViewController
|
||||
func updateHeading(_ heading: Double) {}
|
||||
|
||||
let outer = Self.avatarOuterSize
|
||||
let padding: CGFloat = 30
|
||||
let size = outer + padding * 2
|
||||
bounds = CGRect(x: 0, y: 0, width: size, height: size)
|
||||
centerOffset = .zero
|
||||
containerView.frame = bounds
|
||||
// MARK: - Layout
|
||||
private func layoutMarker() {
|
||||
let bubble = Self.bubbleSize
|
||||
let pointerH = Self.pointerHeight
|
||||
let gap = Self.gapBubbleToDot
|
||||
let dot = Self.dotOuterSize
|
||||
|
||||
let avatarX = (size - outer) / 2
|
||||
let avatarY = (size - outer) / 2
|
||||
avatarOuterCircle.frame = CGRect(x: avatarX, y: avatarY,
|
||||
width: outer, height: outer)
|
||||
avatarImageView.frame = CGRect(
|
||||
x: (outer - Self.avatarInnerSize) / 2,
|
||||
y: (outer - Self.avatarInnerSize) / 2,
|
||||
width: Self.avatarInnerSize, height: Self.avatarInnerSize
|
||||
)
|
||||
let nameH: CGFloat = showsNameTag ? Self.nameTagHeight : 0
|
||||
let nameGap: CGFloat = showsNameTag ? Self.gapNameToBubble : 0
|
||||
|
||||
addHeadingIndicator(heading: heading)
|
||||
}
|
||||
var nameTagWidth: CGFloat = 0
|
||||
if showsNameTag {
|
||||
let text = nameLabel.text ?? ""
|
||||
let textWidth = (text as NSString).size(withAttributes: [.font: nameLabel.font as Any]).width
|
||||
nameTagWidth = min(max(textWidth + Self.nameTagHPadding * 2, 44), 120)
|
||||
}
|
||||
|
||||
private func configureAsMember() {
|
||||
nameTagView.isHidden = false
|
||||
headingLayer?.removeFromSuperlayer()
|
||||
headingLayer = nil
|
||||
stopPulseAnimation()
|
||||
|
||||
// 动态宽度
|
||||
let text = nameLabel.text ?? ""
|
||||
let textWidth = (text as NSString).size(withAttributes: [.font: nameLabel.font as Any]).width
|
||||
let tagWidth = min(max(textWidth + 16, 44), 120)
|
||||
let totalWidth = max(tagWidth, Self.avatarOuterSize)
|
||||
let tagTop: CGFloat = 0
|
||||
let avatarTop = Self.nameTagHeight + 4
|
||||
let totalHeight = avatarTop + Self.avatarOuterSize
|
||||
let totalWidth = max(bubble, nameTagWidth)
|
||||
let totalHeight = nameH + nameGap + bubble + pointerH + gap + dot
|
||||
|
||||
bounds = CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight)
|
||||
containerView.frame = bounds
|
||||
// avatar 中心对准 annotation 坐标
|
||||
let avatarCenterY = avatarTop + Self.avatarOuterSize / 2
|
||||
centerOffset = CGPoint(x: 0, y: avatarCenterY - totalHeight / 2)
|
||||
|
||||
// nameTag 在头像上面
|
||||
nameTagView.frame = CGRect(
|
||||
x: (totalWidth - tagWidth) / 2, y: tagTop,
|
||||
width: tagWidth, height: Self.nameTagHeight
|
||||
)
|
||||
nameLabel.frame = nameTagView.bounds.insetBy(dx: 6, dy: 0)
|
||||
// 色点中心对准 annotation 坐标(负 y 将视图上移)
|
||||
centerOffset = CGPoint(x: 0, y: -(totalHeight / 2 - Self.dotOuterSize / 2))
|
||||
|
||||
// 头像在 nameTag 下面
|
||||
avatarOuterCircle.frame = CGRect(
|
||||
x: (totalWidth - Self.avatarOuterSize) / 2, y: avatarTop,
|
||||
width: Self.avatarOuterSize, height: Self.avatarOuterSize
|
||||
if showsNameTag {
|
||||
nameTagView.frame = CGRect(
|
||||
x: (totalWidth - nameTagWidth) / 2,
|
||||
y: 0,
|
||||
width: nameTagWidth,
|
||||
height: Self.nameTagHeight
|
||||
)
|
||||
nameLabel.frame = nameTagView.bounds.insetBy(dx: Self.nameTagHPadding - 2, dy: 0)
|
||||
}
|
||||
|
||||
let bubbleY = nameH + nameGap
|
||||
bubbleView.frame = CGRect(
|
||||
x: (totalWidth - bubble) / 2,
|
||||
y: bubbleY,
|
||||
width: bubble,
|
||||
height: bubble
|
||||
)
|
||||
let inner = bubble - Self.borderWidth * 2
|
||||
avatarImageView.frame = CGRect(
|
||||
x: (Self.avatarOuterSize - Self.avatarInnerSize) / 2,
|
||||
y: (Self.avatarOuterSize - Self.avatarInnerSize) / 2,
|
||||
width: Self.avatarInnerSize, height: Self.avatarInnerSize
|
||||
x: Self.borderWidth, y: Self.borderWidth,
|
||||
width: inner, height: inner
|
||||
)
|
||||
|
||||
// 白色三角:尖朝下,顶边贴气泡底边
|
||||
let pointerTop = bubbleY + bubble - 1
|
||||
let pointerPath = UIBezierPath()
|
||||
let leftX = (totalWidth - Self.pointerWidth) / 2
|
||||
pointerPath.move(to: CGPoint(x: leftX, y: pointerTop))
|
||||
pointerPath.addLine(to: CGPoint(x: leftX + Self.pointerWidth, y: pointerTop))
|
||||
pointerPath.addLine(to: CGPoint(x: totalWidth / 2, y: pointerTop + pointerH))
|
||||
pointerPath.close()
|
||||
pointerLayer.path = pointerPath.cgPath
|
||||
|
||||
let dotY = bubbleY + bubble + pointerH + gap
|
||||
dotOuterView.frame = CGRect(
|
||||
x: (totalWidth - dot) / 2, y: dotY,
|
||||
width: dot, height: dot
|
||||
)
|
||||
let inset = (Self.dotOuterSize - Self.dotInnerSize) / 2
|
||||
dotInnerView.frame = CGRect(
|
||||
x: inset, y: inset,
|
||||
width: Self.dotInnerSize, height: Self.dotInnerSize
|
||||
)
|
||||
let centerSize: CGFloat = 4
|
||||
let centerInset = (Self.dotInnerSize - centerSize) / 2
|
||||
dotCenterView.frame = CGRect(
|
||||
x: centerInset, y: centerInset,
|
||||
width: centerSize, height: centerSize
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Heading
|
||||
func updateHeading(_ heading: Double) {
|
||||
headingLayer?.removeFromSuperlayer()
|
||||
headingLayer = nil
|
||||
addHeadingIndicator(heading: heading)
|
||||
}
|
||||
|
||||
private func addHeadingIndicator(heading: Double) {
|
||||
headingLayer?.removeFromSuperlayer()
|
||||
|
||||
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
|
||||
let radius: CGFloat = 80
|
||||
|
||||
let halfFan = 30.0 * .pi / 180
|
||||
let centerAngle = CGFloat(heading) * .pi / 180 - .pi / 2
|
||||
let startAngle = centerAngle - halfFan
|
||||
let endAngle = centerAngle + halfFan
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: center)
|
||||
path.addArc(withCenter: center, radius: radius,
|
||||
startAngle: startAngle, endAngle: endAngle,
|
||||
clockwise: true)
|
||||
path.close()
|
||||
|
||||
let shapeLayer = CAShapeLayer()
|
||||
shapeLayer.frame = containerView.bounds
|
||||
shapeLayer.path = path.cgPath
|
||||
shapeLayer.fillColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.25).cgColor
|
||||
shapeLayer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.4).cgColor
|
||||
shapeLayer.lineWidth = 1
|
||||
containerView.layer.insertSublayer(shapeLayer, at: 0)
|
||||
headingLayer = shapeLayer
|
||||
}
|
||||
|
||||
// MARK: - Pulse Animation
|
||||
private func startPulseAnimation() {
|
||||
guard pulseLayer == nil else { return }
|
||||
|
||||
let layer = CAShapeLayer()
|
||||
layer.fillColor = UIColor.clear.cgColor
|
||||
layer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.5).cgColor
|
||||
layer.lineWidth = 2
|
||||
layer.frame = bounds
|
||||
containerView.layer.insertSublayer(layer, at: 0)
|
||||
pulseLayer = layer
|
||||
|
||||
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
|
||||
let radius = Self.avatarOuterSize / 2 + 8
|
||||
let ringRect = CGRect(x: center.x - radius, y: center.y - radius,
|
||||
width: radius * 2, height: radius * 2)
|
||||
layer.path = UIBezierPath(ovalIn: ringRect).cgPath
|
||||
|
||||
let scale = CABasicAnimation(keyPath: "transform.scale")
|
||||
scale.fromValue = 0.9
|
||||
scale.toValue = 1.5
|
||||
|
||||
let opacity = CABasicAnimation(keyPath: "opacity")
|
||||
opacity.fromValue = 0.8
|
||||
opacity.toValue = 0.0
|
||||
|
||||
let group = CAAnimationGroup()
|
||||
group.animations = [scale, opacity]
|
||||
group.duration = 1.6
|
||||
group.repeatCount = .infinity
|
||||
group.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
|
||||
layer.add(group, forKey: "pulse")
|
||||
}
|
||||
|
||||
private func stopPulseAnimation() {
|
||||
pulseLayer?.removeAllAnimations()
|
||||
pulseLayer?.removeFromSuperlayer()
|
||||
pulseLayer = nil
|
||||
// MARK: - Dot Color
|
||||
private static func dotColor(for member: CircleMember) -> UIColor {
|
||||
if member.isCurrentUser {
|
||||
return currentUserDotColor
|
||||
}
|
||||
let hash = member.id.utf8.reduce(0) { ($0 &* 31) &+ Int($1) }
|
||||
let index = abs(hash) % memberDotPalette.count
|
||||
return memberDotPalette[index]
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -81,7 +81,13 @@ class AccountVC: BaseViewController {
|
|||
private func deleteAccount() {
|
||||
DLToast.showLoading()
|
||||
UserService.deleteAccount().subscribe(onNext: { response in
|
||||
DLToast.show(text: "申请成功")
|
||||
DLToast.show(text: "申请成功") {
|
||||
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
|
||||
GeYanSdk.deletePreResultCache()
|
||||
AppContextManager.shared.deleteAccount()
|
||||
GroupIMService.shared.logout()
|
||||
AppDelegate.shared.showMainViewController()
|
||||
}
|
||||
}, onError: { _ in }).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
|
|
|
|||