|
|
@ -194,6 +194,7 @@
|
|||
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
|
||||
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
|
||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */; };
|
||||
30ACD54C2FF6336000174861 /* PopupWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54B2FF6336000174861 /* PopupWindow.swift */; };
|
||||
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
|
||||
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
|
||||
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
|
||||
|
|
@ -488,6 +489,7 @@
|
|||
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
|
||||
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
|
||||
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = "<group>"; };
|
||||
30ACD54B2FF6336000174861 /* PopupWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupWindow.swift; sourceTree = "<group>"; };
|
||||
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
|
||||
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
|
||||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -1062,6 +1064,7 @@
|
|||
305A762C2FCA8C7000227D26 /* Launch */,
|
||||
3062E8B32FCE6BA400CEF511 /* Scan */,
|
||||
30DC18592FD11E7A0041DCD1 /* Web */,
|
||||
30ACD54A2FF6332200174861 /* PopupWindow */,
|
||||
30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
|
||||
);
|
||||
path = Section;
|
||||
|
|
@ -1338,6 +1341,14 @@
|
|||
path = SearchLocation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30ACD54A2FF6332200174861 /* PopupWindow */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30ACD54B2FF6336000174861 /* PopupWindow.swift */,
|
||||
);
|
||||
path = PopupWindow;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1898,6 +1909,7 @@
|
|||
305A76C72FCA8C7000227D26 /* UserConfigModel.swift in Sources */,
|
||||
30B74B472FF3608600F6744D /* ScheduleRecordModel.swift in Sources */,
|
||||
305A76C82FCA8C7000227D26 /* UserConfigResponse.swift in Sources */,
|
||||
30ACD54C2FF6336000174861 /* PopupWindow.swift in Sources */,
|
||||
305A76C92FCA8C7000227D26 /* ApiManager.swift in Sources */,
|
||||
305A76CA2FCA8C7000227D26 /* AppSettings.swift in Sources */,
|
||||
305A76CB2FCA8C7000227D26 /* Authorize.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -3,22 +3,4 @@
|
|||
uuid = "AE547311-A826-480F-B1ED-BD6B815729FA"
|
||||
type = "0"
|
||||
version = "2.0">
|
||||
<Breakpoints>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "AA447600-D860-4876-A728-F18CC2D566FC"
|
||||
shouldBeEnabled = "Yes"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "QuickLocation/AppDelegate.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "70"
|
||||
endingLineNumber = "70"
|
||||
landmarkName = "application(_:open:options:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Slice 3@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Slice 3@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 340 B |
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 348 B |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_327@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_327@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
22
QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2159@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2159@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_326@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_326@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "pause@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "pause@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 413 B |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2174@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2174@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 988 B |
BIN
QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "slide@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "slide@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "start@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "start@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1545@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_1545@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2127@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2127@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 187 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2188@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2188@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 236 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2323@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2323@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 50 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2331@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2331@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 183 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "register_text@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "register_text@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 44 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "search_bg@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "search_bg@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 260 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "组 3 2@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "组 3 2@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 88 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 111@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 111@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 83 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2322@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group_2322@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 107@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 107@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 108@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "image 108@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip1_unlock@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip1_unlock@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 130 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip1_logo@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "vip1_logo@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2086@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2086@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2087@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2087@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
22
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2088@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2088@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2209@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Group 2209@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 202 KiB |
|
|
@ -73,6 +73,8 @@ enum Route: String {
|
|||
case groupSchedule = "groupSchedule"
|
||||
/// 圈子成员列表
|
||||
case groupMemberList = "groupMemberList"
|
||||
/// 行程轨迹
|
||||
case itineraryTrace = "itineraryTrace"
|
||||
}
|
||||
|
||||
extension Route: RouterTarget {
|
||||
|
|
@ -338,6 +340,12 @@ extension AppRouter: AppRouterProtocol {
|
|||
AppRouter.register(Route.groupMemberList) { url, parameters in
|
||||
GroupMemberListVC(groupKey: parameters["groupKey"].safeString)
|
||||
}
|
||||
|
||||
// MARK: - 行程轨迹
|
||||
AppRouter.register(Route.itineraryTrace) { url, parameters in
|
||||
ItineraryTraceVC(scheduleJson: parameters["model"].safeDictionary as! [String : Any],
|
||||
memberInfo: parameters["memberInfo"].safeDictionary as! [String : Any])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
// MARK: - Bindings
|
||||
private func bindViewModel() {
|
||||
rootView.tableView.rx.setDelegate(self)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.output.messages
|
||||
.skip(1)
|
||||
.map { [ChatSectionModel(model: "", items: $0)] }
|
||||
|
|
@ -158,10 +161,7 @@ final class GroupChatVC: BaseViewController {
|
|||
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||||
cell.configure(path: path)
|
||||
cell.playAnimation()
|
||||
}
|
||||
cell.configure(name: name)
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
|
@ -300,6 +300,9 @@ final class GroupChatVC: BaseViewController {
|
|||
// MARK: - 显示表情面板
|
||||
private func showEmojiPanel() {
|
||||
let show = self.rootView.emojiPanelView.isHidden
|
||||
if !show {
|
||||
self.rootView.stopVisibleEmojiAnimations()
|
||||
}
|
||||
self.rootView.emojiPanelView.isHidden = !show
|
||||
let offset: CGFloat = show ? 220 : 0
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
|
|
@ -313,6 +316,10 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
if show {
|
||||
self.rootView.textField.resignFirstResponder()
|
||||
DispatchQueue.main.async {
|
||||
self.rootView.playVisibleEmojiAnimations()
|
||||
self.rootView.preloadAdjacentEmojiPages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,6 +502,19 @@ final class GroupChatVC: BaseViewController {
|
|||
}()
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension GroupChatVC: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
(cell as? EmojiSendMsgCell)?.playAnimation()
|
||||
(cell as? EmojiReceivedMsgCell)?.playAnimation()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
(cell as? EmojiSendMsgCell)?.stopAnimation()
|
||||
(cell as? EmojiReceivedMsgCell)?.stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MessageListenerProxy
|
||||
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
||||
private let handler: (OIMMessageInfo) -> Void
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class GroupChatView: UIView {
|
|||
var bottomBarBottomConstraint: NSLayoutConstraint?
|
||||
/// 语音录制回调
|
||||
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
||||
private var emojiPlaybackGeneration = 0
|
||||
|
||||
private func setupRx() {
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ class GroupChatView: UIView {
|
|||
|| textField.isFirstResponder
|
||||
|
||||
guard needsReset else { return }
|
||||
stopVisibleEmojiAnimations()
|
||||
emojiPanelView.isHidden = true
|
||||
voiceRecordView.isHidden = true
|
||||
if !excludeTextField { textField.resignFirstResponder() }
|
||||
|
|
@ -64,6 +66,44 @@ class GroupChatView: UIView {
|
|||
self.voiceRecordView.layoutChain.bottom(-252)
|
||||
}
|
||||
}
|
||||
|
||||
func playVisibleEmojiAnimations() {
|
||||
emojiPlaybackGeneration += 1
|
||||
let generation = emojiPlaybackGeneration
|
||||
let indexPaths = emojiCollectionView.indexPathsForVisibleItems.sorted { $0.item < $1.item }
|
||||
for (offset, indexPath) in indexPaths.enumerated() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(offset) * 0.02) { [weak self] in
|
||||
guard let self = self,
|
||||
self.emojiPlaybackGeneration == generation,
|
||||
!self.emojiCollectionView.isDragging,
|
||||
!self.emojiCollectionView.isDecelerating,
|
||||
let cell = self.emojiCollectionView.cellForItem(at: indexPath) as? EmojiPanelCell
|
||||
else { return }
|
||||
cell.playAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopVisibleEmojiAnimations() {
|
||||
emojiPlaybackGeneration += 1
|
||||
emojiCollectionView.visibleCells.forEach { cell in
|
||||
(cell as? EmojiPanelCell)?.stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func preloadAdjacentEmojiPages() {
|
||||
let items = UIView.emojiFileNames
|
||||
guard emojiCollectionView.bounds.width > 0, !items.isEmpty else { return }
|
||||
let currentPage = Int(round(emojiCollectionView.contentOffset.x / emojiCollectionView.bounds.width))
|
||||
for page in [currentPage - 1, currentPage + 1] where page >= 0 {
|
||||
let start = page * Self.emojiPerPage
|
||||
guard start < items.count else { continue }
|
||||
let end = min(start + Self.emojiPerPage, items.count)
|
||||
items[start..<end].forEach { name in
|
||||
EmojiPanelCell.loadAnimation(for: name) { _ in }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
private func setupUI() {
|
||||
|
|
@ -270,6 +310,7 @@ class GroupChatView: UIView {
|
|||
lazy var reviewBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setImage(UIImage(named: "IM/review"), for: .normal)
|
||||
btn.isHidden = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
|
|
@ -809,6 +850,9 @@ final class EmojiSendMsgCell: UITableViewCell {
|
|||
return v
|
||||
}()
|
||||
|
||||
private var animationName: String?
|
||||
private var shouldPlayAnimation = false
|
||||
|
||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
|
|
@ -840,11 +884,43 @@ final class EmojiSendMsgCell: UITableViewCell {
|
|||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||
avatarView.image = msg.avatar
|
||||
let index = Int(msg.content.replacingOccurrences(of: "js_emoji:", with: "")) ?? 0
|
||||
if index < Self.emojiFileNames.count,
|
||||
let path = Bundle.main.path(forResource: Self.emojiFileNames[index], ofType: "json") {
|
||||
lottieView.animation = LottieAnimation.filepath(path)
|
||||
lottieView.play()
|
||||
guard Self.emojiFileNames.indices.contains(index) else {
|
||||
animationName = nil
|
||||
lottieView.animation = nil
|
||||
return
|
||||
}
|
||||
let name = Self.emojiFileNames[index]
|
||||
animationName = name
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
lottieView.currentProgress = 0
|
||||
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
|
||||
guard let self = self, self.animationName == name else { return }
|
||||
self.lottieView.animation = animation
|
||||
self.lottieView.currentProgress = 0
|
||||
if self.shouldPlayAnimation {
|
||||
self.lottieView.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playAnimation() {
|
||||
shouldPlayAnimation = true
|
||||
lottieView.play()
|
||||
}
|
||||
|
||||
func stopAnimation() {
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
animationName = nil
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
}
|
||||
|
||||
private func formatTime(_ t: TimeInterval) -> String {
|
||||
|
|
@ -896,6 +972,9 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
|||
return v
|
||||
}()
|
||||
|
||||
private var animationName: String?
|
||||
private var shouldPlayAnimation = false
|
||||
|
||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
|
|
@ -933,11 +1012,43 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
|||
avatarView.image = msg.avatar
|
||||
nameLabel.text = msg.senderName
|
||||
let index = Int(msg.content.replacingOccurrences(of: "js_emoji:", with: "")) ?? 0
|
||||
if index < Self.emojiFileNames.count,
|
||||
let path = Bundle.main.path(forResource: Self.emojiFileNames[index], ofType: "json") {
|
||||
lottieView.animation = LottieAnimation.filepath(path)
|
||||
lottieView.play()
|
||||
guard Self.emojiFileNames.indices.contains(index) else {
|
||||
animationName = nil
|
||||
lottieView.animation = nil
|
||||
return
|
||||
}
|
||||
let name = Self.emojiFileNames[index]
|
||||
animationName = name
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
lottieView.currentProgress = 0
|
||||
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
|
||||
guard let self = self, self.animationName == name else { return }
|
||||
self.lottieView.animation = animation
|
||||
self.lottieView.currentProgress = 0
|
||||
if self.shouldPlayAnimation {
|
||||
self.lottieView.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playAnimation() {
|
||||
shouldPlayAnimation = true
|
||||
lottieView.play()
|
||||
}
|
||||
|
||||
func stopAnimation() {
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
animationName = nil
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
}
|
||||
|
||||
private func formatTime(_ t: TimeInterval) -> String {
|
||||
|
|
@ -955,10 +1066,34 @@ final class EmojiReceivedMsgCell: UITableViewCell {
|
|||
// MARK: - UICollectionViewDelegate (page control)
|
||||
extension GroupChatView: UICollectionViewDelegate {
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
guard scrollView == emojiCollectionView else { return }
|
||||
stopVisibleEmojiAnimations()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
guard scrollView == emojiCollectionView, !decelerate else { return }
|
||||
playVisibleEmojiAnimations()
|
||||
preloadAdjacentEmojiPages()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
guard scrollView == emojiCollectionView else { return }
|
||||
let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)
|
||||
emojiPageControl.currentPage = page
|
||||
playVisibleEmojiAnimations()
|
||||
preloadAdjacentEmojiPages()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard collectionView == emojiCollectionView else { return }
|
||||
guard !collectionView.isDragging, !collectionView.isDecelerating else { return }
|
||||
(cell as? EmojiPanelCell)?.playAnimation()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard collectionView == emojiCollectionView else { return }
|
||||
(cell as? EmojiPanelCell)?.stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct DrivingStatsResponse: BaseModelProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
struct DrivingStatsModel: Mappable {
|
||||
struct DrivingStatsModel: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
/// 行驶总距离
|
||||
var distance_km: Double = 0
|
||||
|
|
@ -67,3 +67,11 @@ struct DrivingStatsModel: Mappable {
|
|||
total <- map["total"]
|
||||
}
|
||||
}
|
||||
|
||||
extension DrivingStatsModel: IdentifiableType {
|
||||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ class GroupMemberListVC: BaseViewController {
|
|||
self?.rootView.updateTableViewHeight(count: items.first?.items.count ?? 0)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
rootView.tableView.rx.modelSelected(ScheduleRecordModel.self)
|
||||
.subscribe(viewModel.scheduleCellRecordAction.inputs)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 日期选择 → 滚动报告到顶部 + 请求驾驶数据
|
||||
rootView.selectedDate
|
||||
.skip(1)
|
||||
|
|
@ -84,12 +88,13 @@ class GroupMemberListVC: BaseViewController {
|
|||
guard !viewModel.memberId.isEmpty else { return }
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.startOfDay(for: date)
|
||||
let end: Date
|
||||
if calendar.isDateInToday(date) {
|
||||
end = Date() // 当天以当前时间为结束
|
||||
} else {
|
||||
end = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: date) ?? date
|
||||
}
|
||||
let end = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: date) ?? date
|
||||
// let end: Date
|
||||
// if calendar.isDateInToday(date) {
|
||||
// end = Date() // 当天以当前时间为结束
|
||||
// } else {
|
||||
// end = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: date) ?? date
|
||||
// }
|
||||
requestDrivingEvents(start_time: "\(Int64(start.timeIntervalSince1970 * 1000))",
|
||||
end_time: "\(Int64(end.timeIntervalSince1970 * 1000))")
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@ class GroupMemberListVM {
|
|||
DrivingEventItem(title: "频繁变道", iconName: "GroupMemberList/6"),
|
||||
DrivingEventItem(title: "长时间驾驶", iconName: "GroupMemberList/8")
|
||||
]
|
||||
|
||||
lazy var scheduleCellRecordAction: Action<ScheduleRecordModel, Void> = { this in
|
||||
return Action { model in
|
||||
let memberInfo = self.memberList.first(where: { $0.user_id == self.memberId })
|
||||
AppRouter.push(Route.itineraryTrace, userInfo: ["model": model.toJSON(),
|
||||
"memberInfo": memberInfo?.toJSON() ?? [:]])
|
||||
return .empty()
|
||||
}
|
||||
}(self)
|
||||
|
||||
// 是否当前用户
|
||||
func isCurrentUser(id: String) -> Bool {
|
||||
|
|
|
|||
|
|
@ -6,24 +6,759 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import ObjectMapper
|
||||
import SwiftyUserDefaults
|
||||
import AMapNaviKit
|
||||
import AMapSearchKit
|
||||
|
||||
class ItineraryTraceVC: BaseViewController {
|
||||
|
||||
fileprivate var rootView: ItineraryTraceView!
|
||||
private let model: ScheduleRecordModel
|
||||
private let memberModel: GroupMemberModel
|
||||
private var routeOverlays: [MAPolyline] = []
|
||||
private let routeSearch = AMapSearchAPI()
|
||||
private var drivingSegmentIndexes: [ObjectIdentifier: Int] = [:]
|
||||
private var playbackSegments: [[CLLocationCoordinate2D]?] = []
|
||||
private var playbackPath: [(coordinate: CLLocationCoordinate2D, distance: Double)] = []
|
||||
private var playbackTotalDistance: Double = 0
|
||||
private var playbackAnnotation: TracePlaybackAnnotation?
|
||||
private var displayLink: CADisplayLink?
|
||||
private var playbackProgress: Double = 0
|
||||
private var playbackLastTick: Date?
|
||||
private var playbackLastCenterUpdate: Date?
|
||||
private var playbackDuration: TimeInterval = 30
|
||||
private let playbackFollowInterval: TimeInterval = 0.2
|
||||
private let minPlaybackDuration: TimeInterval = 10
|
||||
private let maxPlaybackDuration: TimeInterval = 180
|
||||
private let playbackReplaySpeed: CLLocationDistance = 80
|
||||
private let playbackPathMinStep: CLLocationDistance = 5
|
||||
private let playbackInterpolationSteps = 3
|
||||
private var playbackInterpolationFrame = 0
|
||||
private var playbackPrevCoordinate: CLLocationCoordinate2D?
|
||||
private var playbackTargetCoordinate: CLLocationCoordinate2D?
|
||||
|
||||
private let minPointDistance: CLLocationDistance = 10
|
||||
private let directSegmentDistance: CLLocationDistance = 80
|
||||
private let maxDrivingSegmentDistance: CLLocationDistance = 1500
|
||||
private let maxDrivingSegmentTimeGap: Int64 = 5 * 60 * 1000
|
||||
private let maxDrivingSegmentRequests = 40
|
||||
private let eventCalloutTag = 91301
|
||||
|
||||
override func loadView() {
|
||||
rootView = ItineraryTraceView(frame: UIScreen.main.bounds)
|
||||
view = rootView
|
||||
}
|
||||
|
||||
init(scheduleJson: [String: Any], memberInfo: [String: Any]) {
|
||||
self.model = ScheduleRecordModel(JSON: scheduleJson) ?? ScheduleRecordModel(JSON: ["id": ""])!
|
||||
self.memberModel = GroupMemberModel(JSON: memberInfo) ?? GroupMemberModel(JSON: ["id": ""])!
|
||||
print(self.model)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
rootView.eventCollectionView.dataSource = self
|
||||
rootView.eventCollectionView.delegate = self
|
||||
setupMap()
|
||||
populateData()
|
||||
addAnnotations()
|
||||
drawRoute()
|
||||
setupPlaybackControls()
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
if isMovingFromParent || isBeingDismissed {
|
||||
stopPlayback(resetProgress: false)
|
||||
rootView.cleanupMap()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@MainActor deinit {
|
||||
displayLink?.invalidate()
|
||||
}
|
||||
|
||||
private func populateData() {
|
||||
rootView.titleLab.text = "\(memberModel.nick_name) 的驾驶详细信息"
|
||||
rootView.startTimeLab.text = model.start_time.isoStringToCustom(model.start_time, format: "HH:mm")
|
||||
rootView.endTimeLab.text = model.end_time.isoStringToCustom(model.end_time, format: "HH:mm")
|
||||
rootView.startAddressLab.text = model.start_address?.street
|
||||
rootView.endAddressLab.text = model.end_address?.street
|
||||
}
|
||||
|
||||
// MARK: - Map
|
||||
private func setupMap() {
|
||||
rootView.mapView.delegate = self
|
||||
routeSearch?.delegate = self
|
||||
if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] {
|
||||
let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||
if CLLocationCoordinate2DIsValid(coord) {
|
||||
rootView.mapView.setCenter(coord, animated: false)
|
||||
rootView.mapView.setZoomLevel(14, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addAnnotations() {
|
||||
// 起点
|
||||
if let start = model.trajectory_path.first, abs(start.lat) > 0.0001 {
|
||||
let ann = MAPointAnnotation()
|
||||
ann.coordinate = CLLocationCoordinate2D(latitude: start.lat, longitude: start.lng)
|
||||
ann.title = "start"
|
||||
rootView.mapView.addAnnotation(ann)
|
||||
}
|
||||
|
||||
// 终点
|
||||
if let end = model.trajectory_path.last, abs(end.lat) > 0.0001 {
|
||||
let ann = MAPointAnnotation()
|
||||
ann.coordinate = CLLocationCoordinate2D(latitude: end.lat, longitude: end.lng)
|
||||
ann.title = "end"
|
||||
rootView.mapView.addAnnotation(ann)
|
||||
}
|
||||
|
||||
// 事件点(从 driving_events 取坐标)
|
||||
for ev in model.driving_events {
|
||||
guard abs(ev.latitude) > 0.0001 else { continue }
|
||||
let ann = TraceEventAnnotation()
|
||||
ann.coordinate = CLLocationCoordinate2D(latitude: ev.latitude, longitude: ev.longitude)
|
||||
ann.title = "event"
|
||||
ann.eventType = ev.event_type
|
||||
ann.eventTime = eventTimeText(ev.timestamp)
|
||||
ann.eventAddress = ev.address?.street ?? ""
|
||||
rootView.mapView.addAnnotation(ann)
|
||||
}
|
||||
|
||||
// 缩放到所有标注
|
||||
if let anns = rootView.mapView.annotations, anns.count > 0 {
|
||||
rootView.mapView.showAnnotations(anns, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func drawRoute() {
|
||||
clearRouteOverlays()
|
||||
|
||||
let points = cleanedTracePoints(model.trajectory_path)
|
||||
guard points.count >= 2 else { print("[trace] insufficient points: \(points.count)"); return }
|
||||
|
||||
playbackSegments = Array(repeating: nil, count: points.count - 1)
|
||||
|
||||
var drivingRequestCount = 0
|
||||
for i in 0..<(points.count - 1) {
|
||||
let from = points[i]
|
||||
let to = points[i + 1]
|
||||
let fallback = [from.coordinate, to.coordinate]
|
||||
let distance = from.coordinate.distance(to: to.coordinate)
|
||||
let timeGap = abs(to.timestamp - from.timestamp)
|
||||
|
||||
if distance < directSegmentDistance {
|
||||
addPolyline(fallback)
|
||||
playbackSegments[i] = fallback
|
||||
} else if distance <= maxDrivingSegmentDistance,
|
||||
timeGap <= maxDrivingSegmentTimeGap,
|
||||
drivingRequestCount < maxDrivingSegmentRequests {
|
||||
requestDrivingSegment(from: from.coordinate, to: to.coordinate, segmentIndex: i)
|
||||
drivingRequestCount += 1
|
||||
} else {
|
||||
// 断档或距离异常的轨迹段不绘制,避免把事件点/异常点直接连到终点。
|
||||
continue
|
||||
}
|
||||
}
|
||||
buildPlaybackPath()
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
print("[trace] cleaned=\(points.count) drivingRequests=\(drivingRequestCount)")
|
||||
}
|
||||
|
||||
private func buildPlaybackPath() {
|
||||
var path: [(coordinate: CLLocationCoordinate2D, distance: Double)] = []
|
||||
var cumulative: Double = 0
|
||||
var lastCoord: CLLocationCoordinate2D?
|
||||
for segment in playbackSegments.compactMap({ $0 }) where segment.count > 1 {
|
||||
for coord in segment {
|
||||
if let last = lastCoord {
|
||||
let d = last.distance(to: coord)
|
||||
if d < playbackPathMinStep { continue }
|
||||
cumulative += d
|
||||
}
|
||||
path.append((coordinate: coord, distance: cumulative))
|
||||
lastCoord = coord
|
||||
}
|
||||
}
|
||||
playbackPath = path
|
||||
playbackTotalDistance = cumulative
|
||||
}
|
||||
|
||||
private func clearRouteOverlays() {
|
||||
for overlay in routeOverlays {
|
||||
rootView.mapView.remove(overlay)
|
||||
}
|
||||
routeOverlays.removeAll()
|
||||
drivingSegmentIndexes.removeAll()
|
||||
}
|
||||
|
||||
private func cleanedTracePoints(_ points: [TrackPoint]) -> [(timestamp: Int64, coordinate: CLLocationCoordinate2D)] {
|
||||
let sorted = points.sorted { $0.timestamp < $1.timestamp }
|
||||
var result: [(timestamp: Int64, coordinate: CLLocationCoordinate2D)] = []
|
||||
|
||||
for point in sorted {
|
||||
let coordinate = CLLocationCoordinate2D(latitude: point.lat, longitude: point.lng)
|
||||
guard abs(point.lat) > 0.0001,
|
||||
abs(point.lng) > 0.0001,
|
||||
CLLocationCoordinate2DIsValid(coordinate) else { continue }
|
||||
|
||||
if let last = result.last,
|
||||
last.coordinate.distance(to: coordinate) < minPointDistance {
|
||||
continue
|
||||
}
|
||||
|
||||
result.append((timestamp: point.timestamp, coordinate: coordinate))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func requestDrivingSegment(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D, segmentIndex: Int) {
|
||||
let request = AMapDrivingRouteSearchRequest()
|
||||
request.origin = AMapGeoPoint.location(withLatitude: CGFloat(from.latitude), longitude: CGFloat(from.longitude))
|
||||
request.destination = AMapGeoPoint.location(withLatitude: CGFloat(to.latitude), longitude: CGFloat(to.longitude))
|
||||
request.strategy = 0
|
||||
drivingSegmentIndexes[ObjectIdentifier(request)] = segmentIndex
|
||||
routeSearch?.aMapDrivingRouteSearch(request)
|
||||
}
|
||||
|
||||
private func setupPlaybackControls() {
|
||||
rootView.playBtn.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
|
||||
rootView.progressSlider.addTarget(self, action: #selector(progressSliderTouchDown), for: .touchDown)
|
||||
rootView.progressSlider.addTarget(self, action: #selector(progressSliderValueChanged), for: .valueChanged)
|
||||
rootView.progressSlider.addTarget(self, action: #selector(progressSliderTouchEnded), for: .touchUpInside)
|
||||
rootView.progressSlider.addTarget(self, action: #selector(progressSliderTouchEnded), for: .touchUpOutside)
|
||||
rootView.progressSlider.addTarget(self, action: #selector(progressSliderTouchEnded), for: .touchCancel)
|
||||
}
|
||||
|
||||
@objc private func playButtonTapped() {
|
||||
if !rootView.playBtn.isSelected, playbackProgress >= 1 {
|
||||
playbackProgress = 0
|
||||
rootView.progressSlider.value = 0
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
}
|
||||
rootView.playBtn.isSelected ? pausePlayback() : startPlayback()
|
||||
}
|
||||
|
||||
@objc private func progressSliderTouchDown() {
|
||||
pausePlayback()
|
||||
}
|
||||
|
||||
@objc private func progressSliderValueChanged() {
|
||||
playbackProgress = Double(rootView.progressSlider.value)
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
}
|
||||
|
||||
@objc private func progressSliderTouchEnded() {
|
||||
playbackProgress = Double(rootView.progressSlider.value)
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
}
|
||||
|
||||
private func startPlayback() {
|
||||
let totalDistance = playbackPathDistance()
|
||||
guard totalDistance > 0 else { return }
|
||||
guard playbackProgress < 1 else { return }
|
||||
playbackDuration = max(minPlaybackDuration, min(maxPlaybackDuration, totalDistance / playbackReplaySpeed))
|
||||
rootView.playBtn.isSelected = true
|
||||
playbackLastTick = Date()
|
||||
playbackLastCenterUpdate = nil
|
||||
playbackInterpolationFrame = playbackInterpolationSteps
|
||||
if rootView.mapView.zoomLevel < 16 {
|
||||
rootView.mapView.setZoomLevel(16, animated: false)
|
||||
}
|
||||
displayLink?.invalidate()
|
||||
displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink))
|
||||
displayLink?.add(to: .main, forMode: .common)
|
||||
}
|
||||
|
||||
private func pausePlayback() {
|
||||
rootView.playBtn.isSelected = false
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
playbackLastTick = nil
|
||||
playbackLastCenterUpdate = nil
|
||||
}
|
||||
|
||||
private func stopPlayback(resetProgress: Bool) {
|
||||
pausePlayback()
|
||||
if resetProgress {
|
||||
playbackProgress = 0
|
||||
rootView.progressSlider.value = 0
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleDisplayLink() {
|
||||
let now = Date()
|
||||
let delta = playbackLastTick.map { now.timeIntervalSince($0) } ?? 0
|
||||
playbackLastTick = now
|
||||
playbackProgress = min(1, playbackProgress + delta / playbackDuration)
|
||||
rootView.progressSlider.value = Float(playbackProgress)
|
||||
|
||||
playbackInterpolationFrame += 1
|
||||
if playbackInterpolationFrame >= playbackInterpolationSteps {
|
||||
playbackInterpolationFrame = 0
|
||||
playbackPrevCoordinate = playbackTargetCoordinate
|
||||
playbackTargetCoordinate = playbackCoordinate(at: playbackProgress)
|
||||
}
|
||||
if let target = playbackTargetCoordinate {
|
||||
if let prev = playbackPrevCoordinate {
|
||||
let t = Double(playbackInterpolationFrame) / Double(playbackInterpolationSteps)
|
||||
let coord = prev.interpolate(to: target, ratio: t)
|
||||
setPlaybackAnnotationCoordinate(coord)
|
||||
followPlaybackCoordinateIfNeeded(coord)
|
||||
} else {
|
||||
setPlaybackAnnotationCoordinate(target)
|
||||
followPlaybackCoordinateIfNeeded(target)
|
||||
}
|
||||
}
|
||||
|
||||
if playbackProgress >= 1 {
|
||||
finishPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
private func finishPlayback() {
|
||||
playbackProgress = 1
|
||||
rootView.progressSlider.value = 1
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
pausePlayback()
|
||||
}
|
||||
|
||||
private func updatePlaybackAnnotation(progress: Double, interpolated: Bool = false) {
|
||||
guard let coordinate = playbackCoordinate(at: progress) else { return }
|
||||
|
||||
if !interpolated {
|
||||
playbackPrevCoordinate = coordinate
|
||||
playbackTargetCoordinate = coordinate
|
||||
playbackInterpolationFrame = playbackInterpolationSteps
|
||||
}
|
||||
setPlaybackAnnotationCoordinate(coordinate)
|
||||
followPlaybackCoordinateIfNeeded(coordinate)
|
||||
}
|
||||
|
||||
private func setPlaybackAnnotationCoordinate(_ coordinate: CLLocationCoordinate2D) {
|
||||
if let annotation = playbackAnnotation {
|
||||
annotation.coordinate = coordinate
|
||||
} else {
|
||||
let annotation = TracePlaybackAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
playbackAnnotation = annotation
|
||||
rootView.mapView.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
|
||||
private func followPlaybackCoordinateIfNeeded(_ coordinate: CLLocationCoordinate2D) {
|
||||
guard rootView.playBtn.isSelected else { return }
|
||||
|
||||
let now = Date()
|
||||
if let lastUpdate = playbackLastCenterUpdate,
|
||||
now.timeIntervalSince(lastUpdate) < playbackFollowInterval {
|
||||
return
|
||||
}
|
||||
|
||||
playbackLastCenterUpdate = now
|
||||
rootView.mapView.setCenter(coordinate, animated: true)
|
||||
}
|
||||
|
||||
private func playbackCoordinate(at progress: Double) -> CLLocationCoordinate2D? {
|
||||
let clampedProgress = min(max(progress, 0), 1)
|
||||
let path = playbackPath
|
||||
guard !path.isEmpty, playbackTotalDistance > 0 else { return path.first?.coordinate }
|
||||
|
||||
let targetDistance = playbackTotalDistance * clampedProgress
|
||||
|
||||
var lo = 0, hi = path.count - 1
|
||||
while lo < hi {
|
||||
let mid = (lo + hi) / 2
|
||||
if path[mid].distance < targetDistance {
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
let idx = lo
|
||||
let prev = idx > 0 ? path[idx - 1] : path[0]
|
||||
let curr = path[idx]
|
||||
let segDist = curr.distance - prev.distance
|
||||
let ratio = segDist > 0 ? max(0, min(1, (targetDistance - prev.distance) / segDist)) : 0
|
||||
return prev.coordinate.interpolate(to: curr.coordinate, ratio: ratio)
|
||||
}
|
||||
|
||||
private func playbackPathDistance() -> CLLocationDistance {
|
||||
playbackTotalDistance
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func addPolyline(_ coordinates: [CLLocationCoordinate2D]) -> MAPolyline? {
|
||||
guard coordinates.count > 1 else { return nil }
|
||||
var mutableCoords = coordinates
|
||||
guard let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(mutableCoords.count)) else { return nil }
|
||||
rootView.mapView.add(polyline)
|
||||
routeOverlays.append(polyline)
|
||||
return polyline
|
||||
}
|
||||
|
||||
private func coordinates(from path: AMapPath) -> [CLLocationCoordinate2D] {
|
||||
var coords: [CLLocationCoordinate2D] = []
|
||||
for step in path.steps {
|
||||
guard let polylineStr = step.polyline else { continue }
|
||||
for point in polylineStr.components(separatedBy: ";") {
|
||||
let latLon = point.components(separatedBy: ",")
|
||||
if latLon.count == 2,
|
||||
let lon = Double(latLon[0]),
|
||||
let lat = Double(latLon[1]) {
|
||||
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||
if CLLocationCoordinate2DIsValid(coordinate) {
|
||||
coords.append(coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return coords
|
||||
}
|
||||
|
||||
private func eventTimeText(_ timestamp: Int64) -> String {
|
||||
let seconds = timestamp > 10_000_000_000 ? Double(timestamp) / 1000.0 : Double(timestamp)
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter.string(from: Date(timeIntervalSince1970: seconds))
|
||||
}
|
||||
|
||||
private static func playbackAvatarImage(_ image: UIImage) -> UIImage? {
|
||||
let size = CGSize(width: 34, height: 34)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
UIColor.white.setFill()
|
||||
UIBezierPath(ovalIn: rect).fill()
|
||||
|
||||
let imageRect = rect.insetBy(dx: 3, dy: 3)
|
||||
let clipPath = UIBezierPath(ovalIn: imageRect)
|
||||
clipPath.addClip()
|
||||
image.draw(in: imageRect)
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
}
|
||||
|
||||
private extension CLLocationCoordinate2D {
|
||||
func distance(to coordinate: CLLocationCoordinate2D) -> CLLocationDistance {
|
||||
let fromLocation = CLLocation(latitude: latitude, longitude: longitude)
|
||||
let toLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
return fromLocation.distance(from: toLocation)
|
||||
}
|
||||
|
||||
func interpolate(to coordinate: CLLocationCoordinate2D, ratio: Double) -> CLLocationCoordinate2D {
|
||||
let clampedRatio = min(max(ratio, 0), 1)
|
||||
return CLLocationCoordinate2D(latitude: latitude + (coordinate.latitude - latitude) * clampedRatio,
|
||||
longitude: longitude + (coordinate.longitude - longitude) * clampedRatio)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == CLLocationCoordinate2D {
|
||||
var totalDistance: CLLocationDistance {
|
||||
guard count > 1 else { return 0 }
|
||||
var distance: CLLocationDistance = 0
|
||||
for i in 0..<(count - 1) {
|
||||
distance += self[i].distance(to: self[i + 1])
|
||||
}
|
||||
return distance
|
||||
}
|
||||
}
|
||||
|
||||
private final class TracePlaybackAnnotation: MAPointAnnotation {}
|
||||
|
||||
// MARK: - MAMapViewDelegate
|
||||
extension ItineraryTraceVC: MAMapViewDelegate {
|
||||
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
|
||||
guard !(annotation is MAUserLocation) else { return nil }
|
||||
guard let pointAnn = annotation as? MAPointAnnotation else { return nil }
|
||||
|
||||
if annotation is TracePlaybackAnnotation {
|
||||
let id = "PlaybackUserPin"
|
||||
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
|
||||
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
|
||||
else { view?.annotation = annotation }
|
||||
view?.image = Self.playbackAvatarImage(memberModel.userIcon)
|
||||
view?.centerOffset = CGPoint(x: 0, y: -16)
|
||||
return view
|
||||
}
|
||||
|
||||
if pointAnn.title == "start" {
|
||||
let id = "StartPin"
|
||||
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
|
||||
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
|
||||
else { view?.annotation = annotation }
|
||||
view?.image = UIImage(named: "ItineraryTrace/start")
|
||||
view?.centerOffset = CGPoint(x: 0, y: -12)
|
||||
return view
|
||||
}
|
||||
|
||||
if pointAnn.title == "end" {
|
||||
let id = "EndPin"
|
||||
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
|
||||
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
|
||||
else { view?.annotation = annotation }
|
||||
view?.image = UIImage(named: "ItineraryTrace/end")
|
||||
view?.centerOffset = CGPoint(x: 0, y: -12)
|
||||
return view
|
||||
}
|
||||
|
||||
if pointAnn.title == "event" {
|
||||
let id = "EventPin"
|
||||
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
|
||||
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
|
||||
else { view?.annotation = annotation }
|
||||
view?.viewWithTag(eventCalloutTag)?.removeFromSuperview()
|
||||
view?.image = UIImage(named: "ItineraryTrace/event")
|
||||
view?.bounds.size = CGSize(width: 20, height: 20)
|
||||
view?.clipsToBounds = false
|
||||
return view
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MAMapView!, didSelect view: MAAnnotationView!) {
|
||||
guard let annotation = view.annotation as? TraceEventAnnotation else { return }
|
||||
view.viewWithTag(eventCalloutTag)?.removeFromSuperview()
|
||||
|
||||
let calloutWidth = TraceEventCalloutView.preferredWidth(type: annotation.eventType,
|
||||
time: annotation.eventTime,
|
||||
address: annotation.eventAddress)
|
||||
let callout = TraceEventCalloutView(frame: CGRect(x: 0, y: 0, width: calloutWidth, height: 72))
|
||||
callout.tag = eventCalloutTag
|
||||
callout.configure(icon: annotation.eventTypeIcon,
|
||||
type: annotation.eventTypeText,
|
||||
time: annotation.eventTime,
|
||||
address: annotation.eventAddress)
|
||||
callout.frame.origin = CGPoint(x: (view.bounds.width - callout.bounds.width) / 2, y: -callout.bounds.height - 8)
|
||||
view.addSubview(callout)
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MAMapView!, didDeselect view: MAAnnotationView!) {
|
||||
view.viewWithTag(eventCalloutTag)?.removeFromSuperview()
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
|
||||
if let polyline = overlay as? MAPolyline {
|
||||
let r = MAPolylineRenderer(polyline: polyline)
|
||||
r?.strokeColor = UIColor(hexStr: "#16B3FF")
|
||||
r?.lineWidth = 3
|
||||
r?.lineJoinType = kMALineJoinRound
|
||||
r?.lineCapType = kMALineCapRound
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AMapSearchDelegate
|
||||
extension ItineraryTraceVC: AMapSearchDelegate {
|
||||
func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) {
|
||||
let requestId = ObjectIdentifier(request)
|
||||
let segmentIndex = drivingSegmentIndexes.removeValue(forKey: requestId)
|
||||
|
||||
guard let path = response.route?.paths?.first as? AMapPath else {
|
||||
return
|
||||
}
|
||||
|
||||
let coords = coordinates(from: path)
|
||||
if coords.count > 1 {
|
||||
addPolyline(coords)
|
||||
if let segmentIndex, playbackSegments.indices.contains(segmentIndex) {
|
||||
playbackSegments[segmentIndex] = coords
|
||||
buildPlaybackPath()
|
||||
updatePlaybackAnnotation(progress: playbackProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
|
||||
if let request = request as? AMapRouteSearchBaseRequest {
|
||||
let requestId = ObjectIdentifier(request)
|
||||
drivingSegmentIndexes.removeValue(forKey: requestId)
|
||||
}
|
||||
print("Trace route search error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource & Delegate (事件列表)
|
||||
extension ItineraryTraceVC: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return 8
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell: ItineraryTraceCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
guard let stats = model.statistics else { return cell }
|
||||
let all: [(String, String, Int)] = [
|
||||
("急加速", "GroupMemberList/1", stats.hard_acceleration),
|
||||
("急转向", "GroupMemberList/3", stats.sharp_turn),
|
||||
("急刹", "GroupMemberList/5", stats.hard_braking),
|
||||
("手机干扰", "GroupMemberList/7", stats.signal_loss),
|
||||
("超速", "GroupMemberList/2", stats.speeding),
|
||||
("低速", "GroupMemberList/4", stats.low_speeding),
|
||||
("频繁变道", "GroupMemberList/6", stats.frequent_lane_change),
|
||||
("长时间驾驶", "GroupMemberList/8", stats.long_driving)
|
||||
]
|
||||
let item = all[indexPath.row]
|
||||
cell.iconView.image = UIImage(named: item.1)
|
||||
cell.nameLab.text = item.0
|
||||
cell.countLab.text = "\(item.2)"
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 事件点击气泡
|
||||
private final class TraceEventAnnotation: MAPointAnnotation {
|
||||
var eventType = ""
|
||||
var eventTypeText: String {
|
||||
get {
|
||||
switch eventType {
|
||||
case "hard_acceleration":
|
||||
return "急加速"
|
||||
case "sharp_turn":
|
||||
return "急转向"
|
||||
case "hard_braking":
|
||||
return "急刹"
|
||||
case "signal_loss":
|
||||
return "手机干扰"
|
||||
case "speeding":
|
||||
return "超速"
|
||||
case "low_speeding":
|
||||
return "低速"
|
||||
case "frequent_lane_change":
|
||||
return "频繁变道"
|
||||
case "long_driving":
|
||||
return "长时间驾驶"
|
||||
default:
|
||||
return eventType
|
||||
}
|
||||
}
|
||||
}
|
||||
var eventTypeIcon: UIImage? {
|
||||
get {
|
||||
switch eventType {
|
||||
case "hard_acceleration":
|
||||
return UIImage(named: "GroupMemberList/1")
|
||||
case "sharp_turn":
|
||||
return UIImage(named: "GroupMemberList/3")
|
||||
case "hard_braking":
|
||||
return UIImage(named: "GroupMemberList/5")
|
||||
case "signal_loss":
|
||||
return UIImage(named: "GroupMemberList/7")
|
||||
case "speeding":
|
||||
return UIImage(named: "GroupMemberList/2")
|
||||
case "low_speeding":
|
||||
return UIImage(named: "GroupMemberList/4")
|
||||
case "frequent_lane_change":
|
||||
return UIImage(named: "GroupMemberList/6")
|
||||
case "long_driving":
|
||||
return UIImage(named: "GroupMemberList/8")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
var eventTime = ""
|
||||
var eventAddress = ""
|
||||
}
|
||||
|
||||
private final class TraceEventCalloutView: UIView {
|
||||
private let bubbleView = UIView()
|
||||
private let arrowView = UIView()
|
||||
|
||||
private let typeIcon: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
return view
|
||||
}()
|
||||
|
||||
private let typeLab: UILabel = {
|
||||
let lab = UILabel()
|
||||
lab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
lab.textColor = UIColor(hexStr: "#333333")
|
||||
lab.numberOfLines = 1
|
||||
return lab
|
||||
}()
|
||||
|
||||
private let detailLab: UILabel = {
|
||||
let lab = UILabel()
|
||||
lab.font = .systemFont(ofSize: 12)
|
||||
lab.textColor = UIColor(hexStr: "#666666")
|
||||
lab.numberOfLines = 1
|
||||
return lab
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
|
||||
bubbleView.backgroundColor = .white
|
||||
bubbleView.layer.cornerRadius = 8
|
||||
bubbleView.layer.shadowColor = UIColor.black.withAlphaComponent(0.16).cgColor
|
||||
bubbleView.layer.shadowOpacity = 1
|
||||
bubbleView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
bubbleView.layer.shadowRadius = 8
|
||||
|
||||
arrowView.backgroundColor = .white
|
||||
arrowView.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||
|
||||
addSubview(bubbleView)
|
||||
addSubview(arrowView)
|
||||
bubbleView.addSubview(typeIcon)
|
||||
bubbleView.addSubview(typeLab)
|
||||
bubbleView.addSubview(detailLab)
|
||||
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let arrowSize: CGFloat = 12
|
||||
bubbleView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height - arrowSize)
|
||||
arrowView.bounds = CGRect(x: 0, y: 0, width: arrowSize, height: arrowSize)
|
||||
arrowView.center = CGPoint(x: bounds.midX, y: bubbleView.frame.maxY - 1)
|
||||
typeIcon.frame = CGRect(x: 9, y: 12, width: 16, height: 16)
|
||||
typeLab.frame = CGRect(x: 28, y: 10, width: bubbleView.bounds.width - 24, height: 20)
|
||||
detailLab.frame = CGRect(x: 9, y: 34, width: bubbleView.bounds.width - 24, height: 18)
|
||||
}
|
||||
|
||||
func configure(icon: UIImage?, type: String, time: String, address: String) {
|
||||
if let img = icon {
|
||||
typeIcon.image = img
|
||||
}
|
||||
typeLab.text = type
|
||||
let detail = address.isEmpty ? time : "\(time) 在 \(address)"
|
||||
let attr = NSMutableAttributedString(string: detail)
|
||||
attr.addAttributes([
|
||||
.foregroundColor: UIColor(hexStr: "#16B3FF"),
|
||||
.font: UIFont.systemFont(ofSize: 12, weight: .medium)
|
||||
], range: NSRange(location: 0, length: min(time.count, detail.count)))
|
||||
detailLab.attributedText = attr
|
||||
}
|
||||
|
||||
static func preferredWidth(type: String, time: String, address: String) -> CGFloat {
|
||||
let title = type.isEmpty ? "驾驶事件" : type
|
||||
let detail = address.isEmpty ? time : "\(time) 在 \(address)"
|
||||
let titleWidth = (title as NSString).size(withAttributes: [.font: UIFont.systemFont(ofSize: 14, weight: .medium)]).width
|
||||
let detailWidth = (detail as NSString).size(withAttributes: [.font: UIFont.systemFont(ofSize: 12)]).width + 5
|
||||
return min(max(max(titleWidth, detailWidth) + 24, 120), 280)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ class ItineraryTraceView: UIView {
|
|||
navBarView.addSubview(backBtn)
|
||||
navBarView.addSubview(navTitleLabel)
|
||||
|
||||
addSubview(playView)
|
||||
addSubview(bottomView)
|
||||
|
||||
mapView.layoutChain
|
||||
.edges()
|
||||
.edges(excludingEdge: .bottom)
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
|
|
@ -48,7 +51,17 @@ class ItineraryTraceView: UIView {
|
|||
.top(kStatusBarHeight + 12)
|
||||
.centerX()
|
||||
|
||||
bottomView.layoutChain
|
||||
.edgesHorzontal()
|
||||
.bottom()
|
||||
.height(320 + kSafeBottomMargin)
|
||||
|
||||
playView.layoutChain
|
||||
.bottomToTopOfView(bottomView, offset: -10)
|
||||
.edgesHorzontal(20)
|
||||
.height(35)
|
||||
|
||||
mapView.layoutChain.bottomToTopOfView(bottomView, offset: 20)
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
|
@ -129,14 +142,104 @@ class ItineraryTraceView: UIView {
|
|||
.centerY(startTimeLab)
|
||||
.leftToRightOfView(dotView, offset: 12)
|
||||
|
||||
let dashedLine = UIImageView(image: UIImage(named: "ItineraryTrace/dashed"))
|
||||
view.addSubview(dashedLine)
|
||||
dashedLine.layoutChain
|
||||
.topToBottomOfView(dotView, offset: 7)
|
||||
.width(2)
|
||||
.height(22)
|
||||
.centerX(dotView)
|
||||
|
||||
view.addSubview(endTimeLab)
|
||||
view.addSubview(endAddressLab)
|
||||
|
||||
let endIcon = UIImageView(image: UIImage(named: "ItineraryTrace/end_small"))
|
||||
view.addSubview(endIcon)
|
||||
endIcon.layoutChain
|
||||
.width(10)
|
||||
.height(12)
|
||||
.centerX(dashedLine)
|
||||
.centerY(endTimeLab)
|
||||
|
||||
endTimeLab.layoutChain
|
||||
.topToBottomOfView(startTimeLab, offset: 24)
|
||||
.leftToView(startTimeLab)
|
||||
|
||||
endAddressLab.layoutChain
|
||||
.leftToRightOfView(endIcon, offset: 10)
|
||||
.centerY(endTimeLab)
|
||||
|
||||
let eventTitleLab = UILabel()
|
||||
eventTitleLab.text = "行程事件"
|
||||
eventTitleLab.textColor = UIColor(hexStr: "#0F2846")
|
||||
eventTitleLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
view.addSubview(eventTitleLab)
|
||||
eventTitleLab.layoutChain
|
||||
.topToBottomOfView(endTimeLab, offset: 15)
|
||||
.left(20)
|
||||
|
||||
view.addSubview(eventCollectionView)
|
||||
eventCollectionView.layoutChain
|
||||
.topToBottomOfView(eventTitleLab, offset: 10)
|
||||
.edgesHorzontal()
|
||||
.bottom(kSafeBottomMargin)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
/// 播放
|
||||
lazy var playView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
|
||||
view.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
view.layer.shadowOpacity = 1
|
||||
view.layer.shadowRadius = 4
|
||||
|
||||
let cornerRadiusView = UIView()
|
||||
cornerRadiusView.backgroundColor = .white
|
||||
cornerRadiusView.cornerRadius = 6
|
||||
view.addSubview(cornerRadiusView)
|
||||
cornerRadiusView.layoutChain.edges()
|
||||
|
||||
cornerRadiusView.addSubview(playBtn)
|
||||
cornerRadiusView.addSubview(progressSlider)
|
||||
|
||||
playBtn.layoutChain
|
||||
.left(12)
|
||||
.centerY()
|
||||
.width(20)
|
||||
.height(20)
|
||||
|
||||
progressSlider.layoutChain
|
||||
.leftToRightOfView(playBtn, offset: 12)
|
||||
.right(12)
|
||||
.centerY()
|
||||
.height(30)
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var playBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setImage(UIImage(named: "ItineraryTrace/play"), for: .normal)
|
||||
btn.setImage(UIImage(named: "ItineraryTrace/pause"), for: .selected)
|
||||
btn.adjustsImageWhenHighlighted = false
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var progressSlider: UISlider = {
|
||||
let slider = UISlider()
|
||||
slider.minimumValue = 0
|
||||
slider.maximumValue = 1
|
||||
slider.value = 0
|
||||
slider.minimumTrackTintColor = UIColor(hexStr: "#16B3FF")
|
||||
slider.maximumTrackTintColor = UIColor(hexStr: "#E5E5E5")
|
||||
slider.setThumbImage(UIImage(named: "ItineraryTrace/slider"), for: .normal)
|
||||
slider.setThumbImage(UIImage(named: "ItineraryTrace/slider"), for: .highlighted)
|
||||
return slider
|
||||
}()
|
||||
|
||||
/// xxx 的行程详细信息
|
||||
lazy var titleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
|
|
@ -147,6 +250,7 @@ class ItineraryTraceView: UIView {
|
|||
|
||||
lazy var startTimeLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
return label
|
||||
|
|
@ -154,6 +258,7 @@ class ItineraryTraceView: UIView {
|
|||
|
||||
lazy var startAddressLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
return label
|
||||
|
|
@ -161,6 +266,7 @@ class ItineraryTraceView: UIView {
|
|||
|
||||
lazy var endTimeLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.textColor = UIColor(hexStr: "#666666")
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
return label
|
||||
|
|
@ -168,11 +274,36 @@ class ItineraryTraceView: UIView {
|
|||
|
||||
lazy var endAddressLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = " "
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
// 行程事件
|
||||
lazy var eventCollectionView: UICollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.itemSize = CGSize(width: 65, height: 68)
|
||||
layout.minimumLineSpacing = 15
|
||||
layout.scrollDirection = .vertical
|
||||
layout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
|
||||
|
||||
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
cv.backgroundColor = .clear
|
||||
cv.showsHorizontalScrollIndicator = false
|
||||
cv.isScrollEnabled = false
|
||||
cv.register(ItineraryTraceCell.self)
|
||||
return cv
|
||||
}()
|
||||
|
||||
func cleanupMap() {
|
||||
#if !targetEnvironment(simulator)
|
||||
mapView?.delegate = nil
|
||||
mapView?.removeFromSuperview()
|
||||
mapView = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
|
|
@ -184,3 +315,62 @@ class ItineraryTraceView: UIView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ItineraryTraceCell 驾驶事件
|
||||
class ItineraryTraceCell: UICollectionViewCell {
|
||||
|
||||
func configure(_ item: DrivingEventItem) {
|
||||
nameLab.text = item.title
|
||||
countLab.text = item.count.string
|
||||
}
|
||||
|
||||
private let bgView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = UIColor(hexStr: "#EEF9FF")
|
||||
v.cornerRadius = 10
|
||||
return v
|
||||
}()
|
||||
|
||||
lazy var iconView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var nameLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#333333")
|
||||
l.textAlignment = .center
|
||||
return l
|
||||
}()
|
||||
|
||||
lazy var countLab: UILabel = {
|
||||
let l = UILabel()
|
||||
l.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
l.textColor = UIColor(hexStr: "#38ABFF")
|
||||
l.textAlignment = .center
|
||||
return l
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.addSubview(bgView)
|
||||
bgView.addSubview(countLab)
|
||||
contentView.addSubview(nameLab)
|
||||
|
||||
bgView.layoutChain
|
||||
.top()
|
||||
.centerX()
|
||||
.width(50)
|
||||
.height(50)
|
||||
|
||||
countLab.layoutChain.edges()
|
||||
|
||||
nameLab.layoutChain
|
||||
.topToBottomOfView(bgView, offset: 6)
|
||||
.edgesHorzontal()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ struct ScheduleRecordModel: Mappable, Equatable {
|
|||
/// 结束时间
|
||||
var end_time: String = ""
|
||||
/// 公里数
|
||||
var distance_km: String = ""
|
||||
var distance_km: Double = 0
|
||||
/// 驾驶时间
|
||||
var duration_minutes: Int = 0
|
||||
/// 平均时速
|
||||
var avg_speed: String = ""
|
||||
var avg_speed: Double = 0
|
||||
/// 最高时速
|
||||
var max_speed: Double = 0
|
||||
/// 评分
|
||||
|
|
@ -58,7 +58,7 @@ struct ScheduleRecordModel: Mappable, Equatable {
|
|||
var trajectory_path: [TrackPoint] = []
|
||||
var driving_events: [TrackEvent] = []
|
||||
var stay_points: [StayPoint] = []
|
||||
var statistics: TripStatistics?
|
||||
var statistics: DrivingStatsModel?
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ extension ScheduleRecordModel: IdentifiableType {
|
|||
public typealias Identity = String
|
||||
|
||||
public var identity: String {
|
||||
return id
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +100,8 @@ struct TripLocation: Mappable, Equatable {
|
|||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
lat <- map["latitude"]
|
||||
lng <- map["longitude"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +153,8 @@ struct TrackPoint: Mappable, Equatable {
|
|||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
lat <- map["location.latitude"]
|
||||
lng <- map["location.longitude"]
|
||||
timestamp <- map["timestamp"]
|
||||
}
|
||||
}
|
||||
|
|
@ -170,16 +170,23 @@ extension TrackPoint: IdentifiableType {
|
|||
struct TrackEvent: Mappable, Equatable {
|
||||
var uuid: String = UUID().uuidString
|
||||
|
||||
var type: String = ""
|
||||
var event_type: String = ""
|
||||
var timestamp: Int64 = 0
|
||||
var value: Int = 0
|
||||
|
||||
var latitude: Double = 0
|
||||
var longitude: Double = 0
|
||||
|
||||
var address: TripAddress?
|
||||
|
||||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
type <- map["type"]
|
||||
event_type <- map["event_type"]
|
||||
timestamp <- map["timestamp"]
|
||||
value <- map["value"]
|
||||
latitude <- map["location.latitude"]
|
||||
longitude <- map["location.longitude"]
|
||||
address <- map["address"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,8 +210,8 @@ struct StayPoint: Mappable, Equatable {
|
|||
init?(map: Map) {}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
lat <- map["lat"]
|
||||
lng <- map["lng"]
|
||||
lat <- map["latitude"]
|
||||
lng <- map["longitude"]
|
||||
start_time <- map["start_time"]
|
||||
end_time <- map["end_time"]
|
||||
address <- map["address"]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class GroupListPopView: UIView {
|
|||
defaultGroupKey = model.default_group_key
|
||||
groupList = model.groups
|
||||
|
||||
let viewHeight = CGFloat(model.groups.count * 68)
|
||||
let viewHeight = CGFloat(model.groups.prefix(5).count * 68)
|
||||
tableView.layoutChain.height(viewHeight, relation: .greaterThanOrEqual)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,6 @@ class GroupListPopView: UIView {
|
|||
tableView.estimatedRowHeight = 68
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.bounces = false
|
||||
tableView.isScrollEnabled = false
|
||||
tableView.register(GroupListCell.self)
|
||||
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
|
||||
tableView.dataSource = self
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ class GroupMemberCell: UITableViewCell {
|
|||
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
||||
|
||||
// 会员权益
|
||||
batteryInfoView.isHidden = true
|
||||
if AppContextManager.shared.vip > 1, model.is_online {
|
||||
batteryInfoView.isHidden = model.battery.int == 0
|
||||
// 电量 16是电池图标宽度,右边有电池造型需要减去
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ class HomeView: UIView {
|
|||
/// 内嵌 tableView 是否可滑动(最大化时才可)
|
||||
private var isSubCanScroll = false
|
||||
|
||||
private var groupMemberTableTopOffset: CGFloat {
|
||||
-groupMemberView.tableView.adjustedContentInset.top
|
||||
}
|
||||
|
||||
// MARK: - Map
|
||||
#if !targetEnvironment(simulator)
|
||||
lazy var mapView: MAMapView! = {
|
||||
|
|
@ -54,7 +58,7 @@ class HomeView: UIView {
|
|||
mv.showsScale = false
|
||||
mv.isRotateEnabled = false
|
||||
mv.isRotateCameraEnabled = false
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: mv.bounds.width - 55, y: kStatusBarHeight + 30) }
|
||||
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: 15, y: mv.bounds.height / 2) }
|
||||
return mv
|
||||
}()
|
||||
#else
|
||||
|
|
@ -89,14 +93,7 @@ class HomeView: UIView {
|
|||
.disposed(by: disposeBag)
|
||||
|
||||
|
||||
// 嵌套滑动协调:contentOffset 只复位位置,scroll 状态由 pan handler + panVelocity 管理
|
||||
groupMemberView.tableView.rx.contentOffset
|
||||
.subscribe(onNext: { [weak self] offset in
|
||||
guard let self = self else { return }
|
||||
if !self.isSubCanScroll && offset.y != 0 {
|
||||
self.groupMemberView.tableView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
})
|
||||
groupMemberView.tableView.rx.setDelegate(self)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
// 查找位置
|
||||
|
|
@ -226,6 +223,7 @@ class HomeView: UIView {
|
|||
let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGroupMemberPan(_:)))
|
||||
pan.delegate = self
|
||||
groupMemberView.addGestureRecognizer(pan)
|
||||
groupMemberView.tableView.isScrollEnabled = false
|
||||
|
||||
// 底部弹出面板(quickMessageView 在上,interactionView 在下)
|
||||
addSubview(interactionView)
|
||||
|
|
@ -288,21 +286,23 @@ class HomeView: UIView {
|
|||
|
||||
switch pan.state {
|
||||
case .began:
|
||||
layoutIfNeeded()
|
||||
panStartTop = groupMemberView.frame.minY
|
||||
|
||||
case .changed:
|
||||
let newTop = panStartTop + pan.translation(in: self).y
|
||||
let scrollOffset = groupMemberView.tableView.contentOffset.y
|
||||
|
||||
// 最大化状态下,tableView 可以向下滑时,不干扰自身滑动;
|
||||
// 只有 tableView 到达顶部继续下拉时,才切换为 view 拖拽
|
||||
if isSubCanScroll {
|
||||
let tableViewOffset = self.groupMemberView.tableView.contentOffset.y
|
||||
if tableViewOffset > 0 {
|
||||
if scrollOffset > groupMemberTableTopOffset + 0.5 {
|
||||
// 内容正在滑动,不移动 GroupMemberView
|
||||
return
|
||||
}
|
||||
// 内容滑到顶部 → 切回 Pan 拖拽(用户下拉时 velocity > 0)
|
||||
|
||||
if pan.velocity(in: self).y > 0 || groupMemberView.frame.minY > groupMemberUpLimit + 1 {
|
||||
// 内容真正到顶并继续下拉 → 切回面板拖拽
|
||||
isSubCanScroll = false
|
||||
panStartTop = groupMemberView.frame.minY
|
||||
}
|
||||
|
|
@ -323,23 +323,28 @@ class HomeView: UIView {
|
|||
let velocity = pan.velocity(in: self)
|
||||
let isNearUp = abs(groupMemberView.frame.minY - groupMemberUpLimit) < abs(groupMemberView.frame.minY - groupMemberDownLimit)
|
||||
let target: CGFloat
|
||||
if abs(velocity.y) > 200 {
|
||||
if groupMemberView.frame.minY <= groupMemberUpLimit + 5 {
|
||||
// 顶部附近由位置决定去向,避免 tableView 松手速度触发父面板误回收
|
||||
target = isNearUp ? groupMemberUpLimit : groupMemberDownLimit
|
||||
} else if abs(velocity.y) > 200 {
|
||||
target = velocity.y < 0 ? groupMemberUpLimit : groupMemberDownLimit
|
||||
} else {
|
||||
target = isNearUp ? groupMemberUpLimit : groupMemberDownLimit
|
||||
}
|
||||
topConstraint.constant = target - groupMemberDownLimit + 10
|
||||
let atTop = target == self.groupMemberUpLimit
|
||||
if !atTop {
|
||||
isSubCanScroll = false
|
||||
groupMemberView.tableView.isScrollEnabled = false
|
||||
groupMemberView.tableView.setContentOffset(CGPoint(x: 0, y: groupMemberTableTopOffset), animated: false)
|
||||
}
|
||||
isSubCanScroll = atTop
|
||||
|
||||
UIView.animate(withDuration: 0.25, delay: 0,
|
||||
options: [.curveEaseOut, .allowUserInteraction]) {
|
||||
self.layoutIfNeeded()
|
||||
} completion: { _ in
|
||||
let atTop = target == self.groupMemberUpLimit
|
||||
self.isSubCanScroll = atTop
|
||||
// 未最大化时重置 tableView 到顶部
|
||||
if !atTop {
|
||||
self.groupMemberView.tableView.contentOffset.y = 0
|
||||
}
|
||||
self.groupMemberView.tableView.isScrollEnabled = atTop
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
@ -351,8 +356,9 @@ class HomeView: UIView {
|
|||
func dismissGroupMemberView() {
|
||||
guard isGroupMemberLimitsSet, let topConstraint = groupMemberTopConstraint else { return }
|
||||
isSubCanScroll = false
|
||||
groupMemberView.tableView.isScrollEnabled = false
|
||||
topConstraint.constant = groupMemberDownLimit - groupMemberDownLimit + 10
|
||||
groupMemberView.tableView.setContentOffset(.zero, animated: false)
|
||||
groupMemberView.tableView.setContentOffset(CGPoint(x: 0, y: groupMemberTableTopOffset), animated: false)
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
|
|
@ -412,7 +418,8 @@ class HomeView: UIView {
|
|||
view.backgroundColor = .white
|
||||
view.cornerRadius = 18
|
||||
view.clipsToBounds = false
|
||||
|
||||
view.isHidden = true
|
||||
|
||||
view.addSubview(messageIcon)
|
||||
view.addSubview(messageDotView)
|
||||
|
||||
|
|
@ -830,6 +837,28 @@ class HomeView: UIView {
|
|||
}()
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension HomeView: UIScrollViewDelegate {
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
guard scrollView == groupMemberView.tableView else { return }
|
||||
if scrollView.contentOffset.y > groupMemberTableTopOffset {
|
||||
isSubCanScroll = true
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard scrollView == groupMemberView.tableView else { return }
|
||||
if isSubCanScroll {
|
||||
if scrollView.contentOffset.y <= groupMemberTableTopOffset {
|
||||
isSubCanScroll = false
|
||||
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberTableTopOffset)
|
||||
}
|
||||
} else {
|
||||
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberTableTopOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
extension HomeView: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ class HomeViewController: BaseViewController {
|
|||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
startLocationTimer()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
PopupWindow.show(popupType: .register)
|
||||
}
|
||||
|
||||
// MARK: - MQTT 位置上报
|
||||
private let geocoder = CLGeocoder()
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class HomeViewModel {
|
|||
var groupModel: GroupModel? {
|
||||
didSet {
|
||||
guard let model = groupModel else { return }
|
||||
self.memberList = model.select_group_employee
|
||||
setMemberList(model.select_group_employee)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,16 +86,30 @@ class HomeViewModel {
|
|||
memberList.first { $0.user_id == id }?.nick_name ?? ""
|
||||
}
|
||||
|
||||
var memberList: [GroupMemberModel] = [] {
|
||||
didSet {
|
||||
// 排列顺序 圈主 -> 自己 -> 在线
|
||||
var tempList = memberList
|
||||
tempList.moveToFirst { $0.is_online == true } // 在线
|
||||
tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId } // 自己
|
||||
tempList.moveToFirst { isGroupOwn(id: $0.user_id) } // 圈主
|
||||
memberList = tempList
|
||||
sectionedItems.onNext(memberList.mapSection())
|
||||
}
|
||||
private(set) var memberList: [GroupMemberModel] = []
|
||||
|
||||
private func setMemberList(_ list: [GroupMemberModel]) {
|
||||
memberList = sortedMemberList(list)
|
||||
sectionedItems.onNext(memberList.mapSection())
|
||||
}
|
||||
|
||||
private func sortedMemberList(_ list: [GroupMemberModel]) -> [GroupMemberModel] {
|
||||
// 排列顺序:圈主 -> 自己 -> 在线;同优先级保持接口原始顺序
|
||||
list.enumerated()
|
||||
.sorted { lhs, rhs in
|
||||
let lhsPriority = memberSortPriority(lhs.element)
|
||||
let rhsPriority = memberSortPriority(rhs.element)
|
||||
if lhsPriority != rhsPriority { return lhsPriority < rhsPriority }
|
||||
return lhs.offset < rhs.offset
|
||||
}
|
||||
.map(\.element)
|
||||
}
|
||||
|
||||
private func memberSortPriority(_ member: GroupMemberModel) -> Int {
|
||||
if isGroupOwn(id: member.user_id) { return 0 }
|
||||
if member.user_id == AppContextManager.shared.userId { return 1 }
|
||||
if member.is_online { return 2 }
|
||||
return 3
|
||||
}
|
||||
|
||||
/// 标记成员离线(保留 last_active_time 用于显示"X分钟前")
|
||||
|
|
@ -103,7 +117,7 @@ class HomeViewModel {
|
|||
guard let idx = memberList.firstIndex(where: { $0.user_id == userId }) else { return }
|
||||
var updated = memberList
|
||||
updated[idx].is_online = false
|
||||
memberList = updated
|
||||
setMemberList(updated)
|
||||
}
|
||||
|
||||
/// MQTT 位置更新后刷新成员的地址、电量、在线状态
|
||||
|
|
@ -114,7 +128,7 @@ class HomeViewModel {
|
|||
updated[idx].battery = battery
|
||||
updated[idx].is_online = isOnline
|
||||
updated[idx].last_active_time = lastUpdateTime
|
||||
memberList = updated // 触发 didSet → 排序 + 重发 sectionedItems
|
||||
setMemberList(updated)
|
||||
}
|
||||
|
||||
/// 成员是否存在
|
||||
|
|
@ -127,7 +141,7 @@ class HomeViewModel {
|
|||
guard let idx = memberList.firstIndex(where: { $0.user_id == userId }) else { return }
|
||||
var updated = memberList
|
||||
updated.remove(at: idx)
|
||||
memberList = updated
|
||||
setMemberList(updated)
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
|
|
|||
|
|
@ -45,22 +45,20 @@ class InteractionView: UIView {
|
|||
|
||||
/// 当前表情列表(normal / fun)
|
||||
private let emojiRelay = BehaviorRelay<[String]>(value: UIView.emojiFileNames)
|
||||
private var emojiPlaybackGeneration = 0
|
||||
private var isShowingFunEmoji = false
|
||||
|
||||
private func setupRx() {
|
||||
// 切换表情分类
|
||||
emojiNormalBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.normalBg.isHidden = true
|
||||
self?.interestBg.isHidden = false
|
||||
self?.emojiRelay.accept(UIView.emojiFileNames)
|
||||
self?.switchEmojiCategory(isFun: false)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
emojiFunBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.normalBg.isHidden = false
|
||||
self?.interestBg.isHidden = true
|
||||
self?.emojiRelay.accept(UIView.funEmojiFileNames)
|
||||
self?.switchEmojiCategory(isFun: true)
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -100,6 +98,7 @@ class InteractionView: UIView {
|
|||
emojiRelay
|
||||
.subscribe(onNext: { [weak self] items in
|
||||
guard let self = self else { return }
|
||||
self.stopVisibleEmojiAnimations()
|
||||
let pages = (items.count + InteractionView.emojiPerPage - 1) / InteractionView.emojiPerPage
|
||||
self.emojiPageControl.numberOfPages = max(pages, 1)
|
||||
self.emojiPageControl.currentPage = 0
|
||||
|
|
@ -107,17 +106,30 @@ class InteractionView: UIView {
|
|||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func switchEmojiCategory(isFun: Bool) {
|
||||
guard isShowingFunEmoji != isFun else { return }
|
||||
isShowingFunEmoji = isFun
|
||||
stopVisibleEmojiAnimations()
|
||||
normalBg.isHidden = !isFun
|
||||
interestBg.isHidden = isFun
|
||||
emojiRelay.accept(isFun ? UIView.funEmojiFileNames : UIView.emojiFileNames)
|
||||
}
|
||||
|
||||
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||||
cell.configure(path: path)
|
||||
cell.playAnimation()
|
||||
}
|
||||
cell.configure(name: name)
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
|
||||
private func stopVisibleEmojiAnimations() {
|
||||
emojiPlaybackGeneration += 1
|
||||
emojiCollectionView.visibleCells.forEach { cell in
|
||||
(cell as? EmojiPanelCell)?.stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(infoView)
|
||||
|
|
@ -475,13 +487,6 @@ class InteractionView: UIView {
|
|||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
// 同步预加载 fun 表情到缓存,避免首次切换卡顿
|
||||
for name in UIView.funEmojiFileNames {
|
||||
guard EmojiPanelCell.animationCache[name] == nil,
|
||||
let path = Bundle.main.path(forResource: name, ofType: "json"),
|
||||
let anim = LottieAnimation.filepath(path) else { continue }
|
||||
EmojiPanelCell.animationCache[name] = anim
|
||||
}
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
|
@ -504,17 +509,54 @@ extension InteractionView: UICollectionViewDelegate {
|
|||
let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)
|
||||
emojiPageControl.currentPage = page
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard collectionView == emojiCollectionView else { return }
|
||||
(cell as? EmojiPanelCell)?.stopAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - EmojiPanelCell
|
||||
final class EmojiPanelCell: UICollectionViewCell {
|
||||
|
||||
static var animationCache: [String: LottieAnimation] = [:]
|
||||
private static var animationCache: [String: LottieAnimation] = [:]
|
||||
private static var loadingCompletions: [String: [(LottieAnimation?) -> Void]] = [:]
|
||||
private static let animationQueue = DispatchQueue(label: "com.quicklocation.emoji.animation", qos: .userInitiated)
|
||||
|
||||
static func loadAnimation(for name: String, completion: @escaping (LottieAnimation?) -> Void) {
|
||||
animationQueue.async {
|
||||
if let cached = animationCache[name] {
|
||||
DispatchQueue.main.async { completion(cached) }
|
||||
return
|
||||
}
|
||||
|
||||
if loadingCompletions[name] != nil {
|
||||
loadingCompletions[name]?.append(completion)
|
||||
return
|
||||
}
|
||||
|
||||
loadingCompletions[name] = [completion]
|
||||
let animation: LottieAnimation?
|
||||
if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||||
animation = LottieAnimation.filepath(path)
|
||||
} else {
|
||||
animation = nil
|
||||
}
|
||||
animationCache[name] = animation
|
||||
let completions = loadingCompletions.removeValue(forKey: name) ?? []
|
||||
DispatchQueue.main.async {
|
||||
completions.forEach { $0(animation) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var animationName: String?
|
||||
private var shouldPlayAnimation = false
|
||||
|
||||
private let lottieView: LottieAnimationView = {
|
||||
let v = LottieAnimationView()
|
||||
v.contentMode = .scaleAspectFit
|
||||
// v.loopMode = .loop
|
||||
v.loopMode = .loop
|
||||
return v
|
||||
}()
|
||||
|
||||
|
|
@ -547,28 +589,38 @@ final class EmojiPanelCell: UICollectionViewCell {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(path: String) {
|
||||
func configure(name: String) {
|
||||
animationName = name
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
if let cached = Self.animationCache[path] {
|
||||
lottieView.animation = cached
|
||||
} else {
|
||||
lottieView.animation = LottieAnimation.filepath(path)
|
||||
}
|
||||
lottieView.animation = nil
|
||||
lottieView.currentProgress = 0
|
||||
Self.loadAnimation(for: name) { [weak self] animation in
|
||||
guard let self = self, self.animationName == name else { return }
|
||||
self.lottieView.animation = animation
|
||||
self.lottieView.currentProgress = 0
|
||||
if self.shouldPlayAnimation {
|
||||
self.lottieView.play()
|
||||
}
|
||||
}
|
||||
|
||||
lockView.isHidden = AppContextManager.shared.vip > 1
|
||||
}
|
||||
|
||||
func playAnimation() {
|
||||
shouldPlayAnimation = true
|
||||
lottieView.play()
|
||||
}
|
||||
|
||||
func stopAnimation() {
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
animationName = nil
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ class LaunchViewController: BaseViewController {
|
|||
view.backgroundColor = UIColor(hexStr: "#E0F2FF")
|
||||
setupLayout()
|
||||
|
||||
GroupIMService.shared.initSDK()
|
||||
|
||||
experienceBtn.rx.tap.subscribe(onNext: { _ in
|
||||
// Defaults[\.guideShowVersion] = self.showVersion
|
||||
AppDelegate.shared.showMainViewController()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ final class MineView: UIView {
|
|||
guard model.vip > 1 else {
|
||||
vipTitleLab.text = "开通会员"
|
||||
vipContentLab.text = "会员尊享10+VIP特权"
|
||||
vipActivateButton.isHidden = false
|
||||
vipActivateButton.setTitle("立即开通", for: .normal)
|
||||
return
|
||||
}
|
||||
|
|
@ -40,6 +41,7 @@ final class MineView: UIView {
|
|||
vipContentLab.text = model.vip == 3 ? "会员终身有效"
|
||||
: "会员到期:\(self.getDateInterval2String(date: model.vip_expire_time, dateFormat: "yyyy.MM.dd"))"
|
||||
vipActivateButton.setTitle("点击续费", for: .normal)
|
||||
vipActivateButton.isHidden = model.vip == 3
|
||||
}
|
||||
|
||||
private func setupRx() {
|
||||
|
|
@ -142,6 +144,7 @@ final class MineView: UIView {
|
|||
btn.setTitleColor(UIColor(hexStr: "#00343B"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .bold)
|
||||
btn.cornerRadius = 14
|
||||
btn.isHidden = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
|
|
|
|||