diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 87df0f4..257271a 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -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 = ""; }; 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = ""; }; 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = ""; }; + 30ACD54B2FF6336000174861 /* PopupWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupWindow.swift; sourceTree = ""; }; 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = ""; }; 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = ""; }; 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = ""; }; @@ -1062,6 +1064,7 @@ 305A762C2FCA8C7000227D26 /* Launch */, 3062E8B32FCE6BA400CEF511 /* Scan */, 30DC18592FD11E7A0041DCD1 /* Web */, + 30ACD54A2FF6332200174861 /* PopupWindow */, 30EFF3AD2FD7FF1400EB35D4 /* TextInput */, ); path = Section; @@ -1338,6 +1341,14 @@ path = SearchLocation; sourceTree = ""; }; + 30ACD54A2FF6332200174861 /* PopupWindow */ = { + isa = PBXGroup; + children = ( + 30ACD54B2FF6336000174861 /* PopupWindow.swift */, + ); + path = PopupWindow; + sourceTree = ""; + }; 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 */, diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate index ca1061a..8c6ceb2 100644 Binary files a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate and b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 445796d..a5519e7 100644 --- a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,22 +3,4 @@ uuid = "AE547311-A826-480F-B1ED-BD6B815729FA" type = "0" version = "2.0"> - - - - - - diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Contents.json new file mode 100644 index 0000000..fdf7778 --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@2x.png new file mode 100644 index 0000000..4438da0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@3x.png new file mode 100644 index 0000000..08495b0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/dashed.imageset/Slice 3@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Contents.json new file mode 100644 index 0000000..ae4cddf --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@2x.png new file mode 100644 index 0000000..4487190 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@3x.png new file mode 100644 index 0000000..38e7f96 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/end.imageset/Group_327@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Contents.json new file mode 100644 index 0000000..1f7510e --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@2x.png new file mode 100644 index 0000000..5fc4253 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@3x.png new file mode 100644 index 0000000..7e6ef2c Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/end_small.imageset/Group_2159@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Contents.json new file mode 100644 index 0000000..ebba19e --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@2x.png new file mode 100644 index 0000000..9e9521b Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@3x.png new file mode 100644 index 0000000..f4958c8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/event.imageset/Group_326@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/Contents.json new file mode 100644 index 0000000..20f7a8c --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@2x.png new file mode 100644 index 0000000..750ec8c Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@3x.png new file mode 100644 index 0000000..f751007 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/pause.imageset/pause@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Contents.json new file mode 100644 index 0000000..98f88ba --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@2x.png new file mode 100644 index 0000000..099fc42 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@3x.png new file mode 100644 index 0000000..338ef38 Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/play.imageset/Group_2174@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/Contents.json new file mode 100644 index 0000000..06edc7f --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@2x.png new file mode 100644 index 0000000..4de389d Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@3x.png new file mode 100644 index 0000000..d31315e Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/slider.imageset/slide@3x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/Contents.json b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/Contents.json new file mode 100644 index 0000000..1efab98 --- /dev/null +++ b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@2x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@2x.png new file mode 100644 index 0000000..9b3225d Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@2x.png differ diff --git a/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@3x.png b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@3x.png new file mode 100644 index 0000000..a7e9ead Binary files /dev/null and b/QuickLocation/Assets.xcassets/ItineraryTrace/start.imageset/start@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Contents.json new file mode 100644 index 0000000..da6de61 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@2x.png new file mode 100644 index 0000000..b3f4381 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@3x.png new file mode 100644 index 0000000..eabe01e Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/close.imageset/Group_1545@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Contents.json new file mode 100644 index 0000000..7c09cc2 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@2x.png new file mode 100644 index 0000000..af63779 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@3x.png new file mode 100644 index 0000000..cb7b048 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register2_logo.imageset/Group 2127@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Contents.json new file mode 100644 index 0000000..954bc1e --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@2x.png new file mode 100644 index 0000000..e897cf8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@3x.png new file mode 100644 index 0000000..efd7d78 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_bg.imageset/Group 2188@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Contents.json new file mode 100644 index 0000000..13a9575 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@2x.png new file mode 100644 index 0000000..f282fd4 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@3x.png new file mode 100644 index 0000000..946b144 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_heart.imageset/Group_2323@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Contents.json new file mode 100644 index 0000000..41fa3fc --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@2x.png new file mode 100644 index 0000000..ab19150 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@3x.png new file mode 100644 index 0000000..32f2b53 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_logo.imageset/Group_2331@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/Contents.json new file mode 100644 index 0000000..84061b4 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@2x.png new file mode 100644 index 0000000..9f2a6a2 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@3x.png new file mode 100644 index 0000000..67b967d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/register_text.imageset/register_text@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/Contents.json new file mode 100644 index 0000000..4b17b66 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@2x.png new file mode 100644 index 0000000..bce9db0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@3x.png new file mode 100644 index 0000000..d3adbfd Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_bg.imageset/search_bg@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/Contents.json new file mode 100644 index 0000000..dff4989 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@2x.png new file mode 100644 index 0000000..a2aa960 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@3x.png new file mode 100644 index 0000000..be279c7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_earth.imageset/组 3 2@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/Contents.json new file mode 100644 index 0000000..fd3fe5c --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@2x.png new file mode 100644 index 0000000..2aa52d6 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@3x.png new file mode 100644 index 0000000..c8cf832 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/search_people.imageset/image 111@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Contents.json new file mode 100644 index 0000000..b0f7bfe --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@2x.png new file mode 100644 index 0000000..de94af5 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@3x.png new file mode 100644 index 0000000..557752e Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_1.imageset/Group_2322@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/Contents.json new file mode 100644 index 0000000..ed17d5b --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@2x.png new file mode 100644 index 0000000..14c740d Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@3x.png new file mode 100644 index 0000000..6f22792 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_2.imageset/image 107@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/Contents.json new file mode 100644 index 0000000..b723876 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@2x.png new file mode 100644 index 0000000..b6f6e5a Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@3x.png new file mode 100644 index 0000000..5e9adc0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/star_3.imageset/image 108@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/Contents.json new file mode 100644 index 0000000..3cb5df7 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@2x.png new file mode 100644 index 0000000..e2afa04 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@3x.png new file mode 100644 index 0000000..94cf3e7 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_heart.imageset/vip1_unlock@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/Contents.json new file mode 100644 index 0000000..7813070 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@2x.png new file mode 100644 index 0000000..dd8d26a Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@3x.png new file mode 100644 index 0000000..cd90132 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_logo.imageset/vip1_logo@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Contents.json new file mode 100644 index 0000000..d47fa0d --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@2x.png new file mode 100644 index 0000000..7b55fc1 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@3x.png new file mode 100644 index 0000000..4c1a7fe Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_1.imageset/Group 2086@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Contents.json new file mode 100644 index 0000000..b70c164 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@2x.png new file mode 100644 index 0000000..aa2a6dd Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@3x.png new file mode 100644 index 0000000..6cdc00c Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_2.imageset/Group 2087@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Contents.json new file mode 100644 index 0000000..f3081e8 --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@2x.png new file mode 100644 index 0000000..33b22ff Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@3x.png new file mode 100644 index 0000000..d156f14 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip1_tips_3.imageset/Group 2088@3x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Contents.json new file mode 100644 index 0000000..c3e014e --- /dev/null +++ b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@2x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@2x.png new file mode 100644 index 0000000..05d6225 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@2x.png differ diff --git a/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@3x.png b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@3x.png new file mode 100644 index 0000000..c75acc9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/PopupWindow/vip_bg.imageset/Group 2209@3x.png differ diff --git a/QuickLocation/Manager/App/RouterManager.swift b/QuickLocation/Manager/App/RouterManager.swift index 6fd8030..0420fe3 100644 --- a/QuickLocation/Manager/App/RouterManager.swift +++ b/QuickLocation/Manager/App/RouterManager.swift @@ -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]) + } } } diff --git a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift index eafe769..34375cc 100644 --- a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift +++ b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift @@ -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> = { RxCollectionViewSectionedReloadDataSource> { _, 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 diff --git a/QuickLocation/Section/Group/GroupChat/GroupChatView.swift b/QuickLocation/Section/Group/GroupChat/GroupChatView.swift index cb4adcb..183da40 100644 --- a/QuickLocation/Section/Group/GroupChat/GroupChatView.swift +++ b/QuickLocation/Section/Group/GroupChat/GroupChatView.swift @@ -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.. 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() } } diff --git a/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift b/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift index 9a92759..07465a1 100644 --- a/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift +++ b/QuickLocation/Section/Group/GroupMemberList/DrivingStatsModel.swift @@ -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 + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift index 429c4d5..35eff5c 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVC.swift @@ -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))") diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift index 45676e6..0e13adc 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListVM.swift @@ -59,6 +59,15 @@ class GroupMemberListVM { DrivingEventItem(title: "频繁变道", iconName: "GroupMemberList/6"), DrivingEventItem(title: "长时间驾驶", iconName: "GroupMemberList/8") ] + + lazy var scheduleCellRecordAction: Action = { 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 { diff --git a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift index 4d6787a..98b7264 100644 --- a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift +++ b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceVC.swift @@ -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) + } } diff --git a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift index 07a9746..d11c0a2 100644 --- a/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift +++ b/QuickLocation/Section/Group/GroupMemberList/ItineraryTraceView.swift @@ -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") } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift b/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift index 887b5d9..b70c32b 100644 --- a/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift +++ b/QuickLocation/Section/Group/GroupMemberList/ScheduleRecordModel.swift @@ -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"] diff --git a/QuickLocation/Section/Home/GroupListPopView.swift b/QuickLocation/Section/Home/GroupListPopView.swift index a75f8a4..2604102 100644 --- a/QuickLocation/Section/Home/GroupListPopView.swift +++ b/QuickLocation/Section/Home/GroupListPopView.swift @@ -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 diff --git a/QuickLocation/Section/Home/GroupMemberView.swift b/QuickLocation/Section/Home/GroupMemberView.swift index a846872..5e679de 100644 --- a/QuickLocation/Section/Home/GroupMemberView.swift +++ b/QuickLocation/Section/Home/GroupMemberView.swift @@ -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是电池图标宽度,右边有电池造型需要减去 diff --git a/QuickLocation/Section/Home/HomeView.swift b/QuickLocation/Section/Home/HomeView.swift index 36a46ad..2cb6980 100644 --- a/QuickLocation/Section/Home/HomeView.swift +++ b/QuickLocation/Section/Home/HomeView.swift @@ -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, diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index 5670f23..f4a16c6 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -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() diff --git a/QuickLocation/Section/Home/HomeViewModel.swift b/QuickLocation/Section/Home/HomeViewModel.swift index 49fb0f3..41921bc 100644 --- a/QuickLocation/Section/Home/HomeViewModel.swift +++ b/QuickLocation/Section/Home/HomeViewModel.swift @@ -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 diff --git a/QuickLocation/Section/Home/InteractionView.swift b/QuickLocation/Section/Home/InteractionView.swift index d1f6a54..ddc459d 100644 --- a/QuickLocation/Section/Home/InteractionView.swift +++ b/QuickLocation/Section/Home/InteractionView.swift @@ -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> = { RxCollectionViewSectionedReloadDataSource> { _, 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 } diff --git a/QuickLocation/Section/Launch/LaunchViewController.swift b/QuickLocation/Section/Launch/LaunchViewController.swift index 78b3908..131f6a2 100644 --- a/QuickLocation/Section/Launch/LaunchViewController.swift +++ b/QuickLocation/Section/Launch/LaunchViewController.swift @@ -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() diff --git a/QuickLocation/Section/Mine/MineView.swift b/QuickLocation/Section/Mine/MineView.swift index c5ef738..7637134 100644 --- a/QuickLocation/Section/Mine/MineView.swift +++ b/QuickLocation/Section/Mine/MineView.swift @@ -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 }() diff --git a/QuickLocation/Section/PopupWindow/PopupWindow.swift b/QuickLocation/Section/PopupWindow/PopupWindow.swift new file mode 100644 index 0000000..f28baae --- /dev/null +++ b/QuickLocation/Section/PopupWindow/PopupWindow.swift @@ -0,0 +1,825 @@ +// +// PopupWindow.swift +// QuickLocation +// +// Created by 八条 on 2026/7/2. +// + +import UIKit +import RxSwift +import RxCocoa +import Lottie + +enum PopupType { + /// 搜索位置 + case search + /// 注册 + case register + /// 会员 + case vip +} + +class PopupWindow: UIView { + + private static let shared = PopupWindow(frame: CGRect(origin: .zero, size: kScreenSize)) + + var disposeBag = DisposeBag() + + private var popupType: PopupType? { + didSet { + switch popupType { + case .search: + searchPopupView.isHidden = false + earthLottieView.play() + case .register: + if Bool.random() { + registerPopupView.isHidden = true + register2PopupView.isHidden = false + slideInRegister2Popup() + } else { + register2PopupView.isHidden = true + registerPopupView.isHidden = false + slideInRegisterPopup() + } + case .vip: + slideInUnlockVip1Popup() + default: + break + } + } + } + + static func show(popupType: PopupType) { + guard let superView = kKeyWindow else { + return + } + + if PopupWindow.shared.superview != nil { + PopupWindow.shared.removeFromSuperview() + PopupWindow.shared.bgView.frame = .zero + } + + PopupWindow.shared.resetPopupState() + + PopupWindow.shared.popupType = popupType + PopupWindow.shared.bgView.alpha = 1 + PopupWindow.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) + superView.addSubview(PopupWindow.shared) + superView.bringSubviewToFront(PopupWindow.shared) + + UIView.animate(withDuration: 0.25) { + PopupWindow.shared.bgView.alpha = 1 + } + } + + private func resetPopupState() { + stopRegisterStarFlash() + stopRegister1LottieAlternate() + stopUnlockVip1Animations() + registerPopupView.transform = .identity + register2PopupView.transform = .identity + unlockVip1View.transform = .identity + registerStar1.transform = .identity +// register2Star1.transform = .identity + + searchPopupView.isHidden = true + registerPopupView.isHidden = true + register2PopupView.isHidden = true + unlockVip1View.isHidden = true + bgView.isUserInteractionEnabled = true + } + + /// 关闭 + static func dismiss() { + guard PopupWindow.shared.superview != nil else { return } + + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { + PopupWindow.shared.bgView.alpha = 0 + } completion: { _ in + PopupWindow.shared.resetPopupState() + PopupWindow.shared.removeFromSuperview() + } + } + + private func setupRx() { + registerCloseBtn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.activePopupView == 2 ? self?.slideOutRegister2Popup() : self?.slideOutRegisterPopup() + }) + .disposed(by: disposeBag) + + registerBtn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.activePopupView == 2 ? self?.slideOutRegister2Popup() : self?.slideOutRegisterPopup() + AppRouter.push(Route.login) + }) + .disposed(by: disposeBag) + + register2CloseBtn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.slideOutRegister2Popup() + }) + .disposed(by: disposeBag) + + register2Btn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.slideOutRegister2Popup() + AppRouter.push(Route.login) + }) + .disposed(by: disposeBag) + + vip1CloseBtn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.slideOutUnlockVip1Popup() + }) + .disposed(by: disposeBag) + + vip1UnlockBtn.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.slideOutUnlockVip1Popup() + }) + .disposed(by: disposeBag) + } + + private lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + return view + }() + + // MARK: - 实时查位置 + lazy var searchPopupView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.isHidden = true + + let bgImgView = UIImageView(image: UIImage(named: "PopupWindow/search_bg")) + bgImgView.contentMode = .scaleAspectFill + view.addSubview(bgImgView) + bgImgView.layoutChain + .edges(excludingEdge: .bottom) + .heightToWidth(828/770) + + let btn = UIButton(type: .custom) + btn.backgroundColor = .clear + btn.setBackgroundImage(UIImage(named: "Group/close"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 100, bottom: 100, right: 100) + btn.rx.tap.subscribe(onNext: { _ in + PopupWindow.dismiss() + }).disposed(by: disposeBag) + view.addSubview(btn) + btn.layoutChain + .topToBottomOfView(bgImgView, offset: 0) + .centerX() + .width(22) + .height(22) + .bottom() + +// let peopleIcon = UIImageView(image: UIImage(named: "PopupWindow/search_people")) +// peopleIcon.contentMode = .scaleAspectFill +// view.addSubview(peopleIcon) +// peopleIcon.layoutChain +// .top(20) +// .width(73) +// .height(95) +// .rightToCenterXOfView(view) + +// let eartIcon = UIImageView(image: UIImage(named: "PopupWindow/search_earth")) +// eartIcon.contentMode = .scaleAspectFill +// view.addSubview(eartIcon) +// eartIcon.layoutChain +// .topToView(peopleIcon, offset: 12) +// .width(88) +// .height(81) +// .leftToCenterXOfView(view) + + view.addSubview(earthLottieView) + earthLottieView.layoutChain + .top() + .edgesHorzontal(50) + .heightToWidth(95/155) + + return view + }() + + lazy var earthLottieView: LottieAnimationView = { + let view = LottieAnimationView(name: "dialog_phone_search") + view.loopMode = .loop + return view + }() + + // MARK: - 解锁会员 1 + lazy var unlockVip1View: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.isHidden = true + + let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip_bg")) + bgImg.contentMode = .scaleAspectFill + view.addSubview(bgImg) + bgImg.layoutChain + .top(45) + .edgesHorzontal() + .bottom() + + view.addSubview(vip1HeartIcon) + vip1HeartIcon.layoutChain + .top() + .left(21) + .width(116) + .height(99) + + let logoImg = UIImageView(image: UIImage(named: "PopupWindow/vip1_logo")) + logoImg.contentMode = .scaleAspectFill + view.addSubview(logoImg) + logoImg.layoutChain + .topToView(bgImg, offset: 40) + .centerX() + .width(125) + .height(42) + + view.addSubview(vip1Tips2Img) + vip1Tips2Img.layoutChain + .topToBottomOfView(logoImg, offset: 30) + .centerX() + .width(71) + .height(26) + + view.addSubview(vip1Tips1Img) + vip1Tips1Img.layoutChain + .rightToLeftOfView(vip1Tips2Img, offset: -24) + .centerY(vip1Tips2Img) + .width(71) + .height(26) + + view.addSubview(vip1Tips3Img) + vip1Tips3Img.layoutChain + .leftToRightOfView(vip1Tips2Img, offset: 24) + .centerY(vip1Tips2Img) + .width(71) + .height(26) + + let textLab = UILabel() + textLab.text = "看看TA真实走过哪里" + textLab.font = .systemFont(ofSize: 14, weight: .medium) + textLab.textColor = UIColor(hexStr: "#16B3FF") + view.addSubview(textLab) + textLab.layoutChain + .topToBottomOfView(vip1Tips2Img, offset: 20) + .centerX() + + view.addSubview(vip1CloseBtn) + vip1CloseBtn.layoutChain + .topToView(bgImg, offset: 32) + .right(17) + + view.addSubview(vip1UnlockBtn) + vip1UnlockBtn.layoutChain + .width(224) + .height(50) + .centerX() + .bottom(kSafeBottomMargin + 15) + + return view + }() + + lazy var vip1HeartIcon: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/vip1_heart") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var vip1Tips1Img: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/vip1_tips_1") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var vip1Tips2Img: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/vip1_tips_2") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var vip1Tips3Img: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/vip1_tips_3") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var vip1CloseBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 17) + return btn + }() + + lazy var vip1UnlockBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("立即解锁", for: .normal) + btn.setTitleColor(.white, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) + btn.cornerRadius = 25 + return btn + }() + + private func startUnlockVip1Animations() { + stopUnlockVip1Animations() + + let heartAnimation = CABasicAnimation(keyPath: "transform.scale") + heartAnimation.fromValue = 1.0 + heartAnimation.toValue = 1.2 + heartAnimation.duration = 0.55 + heartAnimation.autoreverses = true + heartAnimation.repeatCount = .infinity + heartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + vip1HeartIcon.layer.add(heartAnimation, forKey: "vip1HeartBeat") + + addVip1TipSwingAnimation(to: vip1Tips1Img, from: -0.12, to: 0.12, duration: 0.55) + addVip1TipSwingAnimation(to: vip1Tips2Img, from: -0.12, to: 0.12, duration: 0.55) + addVip1TipSwingAnimation(to: vip1Tips3Img, from: -0.12, to: 0.12, duration: 0.55) + } + + private func addVip1TipSwingAnimation(to imageView: UIImageView, from: CGFloat, to: CGFloat, duration: CFTimeInterval) { + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = from + animation.toValue = to + animation.duration = duration + animation.autoreverses = true + animation.repeatCount = .infinity + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + imageView.layer.add(animation, forKey: "vip1TipSwing") + } + + private func stopUnlockVip1Animations() { + vip1HeartIcon.layer.removeAnimation(forKey: "vip1HeartBeat") + vip1Tips1Img.layer.removeAnimation(forKey: "vip1TipSwing") + vip1Tips2Img.layer.removeAnimation(forKey: "vip1TipSwing") + vip1Tips3Img.layer.removeAnimation(forKey: "vip1TipSwing") + } + + private let unlockVip1PopupHeight: CGFloat = 339 + + private func slideInUnlockVip1Popup() { + stopUnlockVip1Animations() + bgView.isUserInteractionEnabled = false + unlockVip1View.isHidden = false + unlockVip1View.layoutIfNeeded() + unlockVip1View.transform = CGAffineTransform(translationX: 0, y: unlockVip1PopupHeight) + UIView.animate(withDuration: 0.45, delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut) { + self.unlockVip1View.transform = .identity + } completion: { _ in + self.bgView.isUserInteractionEnabled = true + self.startUnlockVip1Animations() + } + } + + private func slideOutUnlockVip1Popup() { + stopUnlockVip1Animations() + bgView.isUserInteractionEnabled = false + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { + self.unlockVip1View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip1PopupHeight) + } completion: { _ in + Self.dismiss() + } + } + + // MARK: - 注册账号 1 + lazy var registerPopupView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.isHidden = true + + let bgImg = UIImageView(image: UIImage(named: "PopupWindow/register_bg")) + bgImg.contentMode = .scaleAspectFill + view.addSubview(bgImg) + bgImg.layoutChain + .top(51) + .edgesHorzontal() + .bottom() + + let logoImg = UIImageView(image: UIImage(named: "PopupWindow/register_logo")) + logoImg.contentMode = .scaleAspectFill + view.addSubview(logoImg) + logoImg.layoutChain + .top() + .centerX() + .width(250) + .height(123) + + let textImg = UIImageView(image: UIImage(named: "PopupWindow/register_text")) + textImg.contentMode = .scaleAspectFill + view.addSubview(textImg) + textImg.layoutChain + .topToBottomOfView(logoImg, offset: 20) + .width(272) + .height(50) + .centerX() + + view.addSubview(register1UpLottieView) + register1UpLottieView.layoutChain + .bottomToTopOfView(logoImg, offset: 120) + .edgesHorzontal() + .height(200) + + view.addSubview(register1DownLottieView) + register1DownLottieView.layoutChain + .topToBottomOfView(logoImg, offset: -140) + .edgesHorzontal() + .height(200) + +// view.addSubview(registerStar1) +// registerStar1.layoutChain +// .top(19) +// .leftToView(logoImg, offset: -31) +// .width(68) +// .heightToWidth(1.0) + + view.addSubview(registerStar3) + registerStar3.layoutChain + .top(23) + .rightToView(logoImg, offset: -9) + .width(26) + .height(20) + + view.addSubview(registerStar2) + registerStar2.layoutChain + .top(31) + .rightToView(logoImg, offset: 20) + .width(48) + .height(44) + + view.addSubview(registerCloseBtn) + registerCloseBtn.layoutChain + .topToBottomOfView(textImg, offset: 30) + .left(30) + .height(50) + + view.addSubview(registerBtn) + registerBtn.layoutChain + .topToBottomOfView(textImg, offset: 30) + .right(30) + .height(50) + .widthToView(registerCloseBtn) + + registerCloseBtn.layoutChain.rightToLeftOfView(registerBtn, offset: -10) + + return view + }() + + lazy var register1UpLottieView: LottieAnimationView = { + let view = LottieAnimationView(name: "dialog_register_tip1_1") + return view + }() + + lazy var register1DownLottieView: LottieAnimationView = { + let view = LottieAnimationView(name: "dialog_register_tip1_2") + return view + }() + + lazy var registerStar1: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/star_1") + return view + }() + + lazy var registerStar2: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/star_2") + return view + }() + + lazy var registerStar3: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/star_3") + return view + }() + + lazy var registerCloseBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("稍后再说", for: .normal) + btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.backgroundColor = .white + btn.cornerRadius = 25 + btn.borderWidth = 0.5 + btn.borderColor = UIColor(hexStr: "#16B3FF") + return btn + }() + + lazy var registerBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("免费注册", for: .normal) + btn.setTitleColor(.white, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) + btn.cornerRadius = 25 + return btn + }() + + private var register1LottieAlternating = false + + private func startRegister1LottieAlternate() { + stopRegister1LottieAlternate() + register1LottieAlternating = true + playRegister1Up() + } + + private func playRegister1Up() { + guard register1LottieAlternating else { return } + register1UpLottieView.isHidden = false + register1DownLottieView.isHidden = true + register1UpLottieView.play { [weak self] _ in + self?.playRegister1Down() + } + } + + private func playRegister1Down() { + guard register1LottieAlternating else { return } + register1UpLottieView.isHidden = true + register1DownLottieView.isHidden = false + register1DownLottieView.play { [weak self] _ in + self?.playRegister1Up() + } + } + + private func stopRegister1LottieAlternate() { + register1LottieAlternating = false + register1UpLottieView.stop() + register1DownLottieView.stop() + register1UpLottieView.isHidden = true + register1DownLottieView.isHidden = true + } + + private func startRegisterStarFlash() { + stopRegisterStarFlash() + let (star2, star3) = activePopupView == 2 ? (register2Star2, register2Star3) : (registerStar2, registerStar3) + + let animation2 = CABasicAnimation(keyPath: "opacity") + animation2.fromValue = 1.0 + animation2.toValue = 0.0 + animation2.duration = 0.4 + animation2.autoreverses = true + animation2.repeatCount = .infinity + star2.layer.add(animation2, forKey: "starFlash") + + let animation3 = CABasicAnimation(keyPath: "opacity") + animation3.fromValue = 0.0 + animation3.toValue = 1.0 + animation3.duration = 0.4 + animation3.autoreverses = true + animation3.repeatCount = .infinity + star3.layer.add(animation3, forKey: "starFlash") + } + + private func stopRegisterStarFlash() { + registerStar2.layer.removeAnimation(forKey: "starFlash") + registerStar3.layer.removeAnimation(forKey: "starFlash") + register2Star2.layer.removeAnimation(forKey: "starFlash") + register2Star3.layer.removeAnimation(forKey: "starFlash") + } + + private func animateRegisterStarSlideIn() { + UIView.animate(withDuration: 0.5, delay: 0, + usingSpringWithDamping: 0.7, + initialSpringVelocity: 0.6, + options: .curveEaseOut) { + self.registerStar1.transform = .identity + } + } + + private let registerPopupHeight: CGFloat = 308 + + private var activePopupView = 1 + + private func slideInRegisterPopup() { + activePopupView = 1 + stopRegisterStarFlash() + bgView.isUserInteractionEnabled = false + registerPopupView.isHidden = false + registerPopupView.layoutIfNeeded() +// registerStar1.transform = CGAffineTransform(translationX: -(registerStar1.frame.maxX + 30), y: 0) + registerPopupView.transform = CGAffineTransform(translationX: 0, y: registerPopupHeight) + UIView.animate(withDuration: 0.45, delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut) { + self.registerPopupView.transform = .identity + } completion: { _ in + self.bgView.isUserInteractionEnabled = true + self.startRegisterStarFlash() + self.startRegister1LottieAlternate() +// self.animateRegisterStarSlideIn() + } + } + + private func slideOutRegisterPopup() { + stopRegisterStarFlash() + stopRegister1LottieAlternate() + bgView.isUserInteractionEnabled = false + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { + self.registerPopupView.transform = CGAffineTransform(translationX: 0, y: self.registerPopupHeight) + } completion: { _ in + Self.dismiss() + } + } + + private func slideInRegister2Popup() { + activePopupView = 2 + stopRegisterStarFlash() + bgView.isUserInteractionEnabled = false + register2PopupView.isHidden = false + register2PopupView.layoutIfNeeded() +// register2Star1.transform = CGAffineTransform(translationX: -(register2Star1.frame.maxX + 30), y: 0) + register2PopupView.transform = CGAffineTransform(translationX: 0, y: register2PopupView.bounds.height) + UIView.animate(withDuration: 0.45, delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut) { + self.register2PopupView.transform = .identity + } completion: { _ in + self.bgView.isUserInteractionEnabled = true + self.startRegisterStarFlash() + self.register2LottieView.play() +// self.animateRegister2StarSlideIn() + } + } + + private func slideOutRegister2Popup() { + stopRegisterStarFlash() + bgView.isUserInteractionEnabled = false + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { + self.register2PopupView.transform = CGAffineTransform(translationX: 0, y: self.register2PopupView.bounds.height) + } completion: { _ in + Self.dismiss() + } + } + + private func animateRegister2StarSlideIn() { + UIView.animate(withDuration: 0.55, delay: 0.1, + usingSpringWithDamping: 0.65, + initialSpringVelocity: 0.4, + options: .curveEaseOut) { + self.register2Star1.transform = .identity + } + } + + // MARK: - 注册账号 2 + lazy var register2PopupView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.isHidden = true + + let bgImg = UIImageView(image: UIImage(named: "PopupWindow/register_bg")) + bgImg.contentMode = .scaleAspectFill + view.addSubview(bgImg) + bgImg.layoutChain + .top(58) + .edgesHorzontal() + .bottom() + + let logoImg = UIImageView(image: UIImage(named: "PopupWindow/register2_logo")) + logoImg.contentMode = .scaleAspectFill + view.addSubview(register2LottieView) + view.addSubview(logoImg) + logoImg.layoutChain + .top() + .centerX() + .width(243) + .height(97) + + let textLab = UILabel() + textLab.text = "注册后即可保存历史轨迹、多人 共享地图、跨设备同步。" + textLab.font = .systemFont(ofSize: 16, weight: .medium) + textLab.textColor = UIColor(hexStr: "#16B3FF") + textLab.numberOfLines = 0 + view.addSubview(textLab) + textLab.layoutChain + .topToBottomOfView(logoImg, offset: 33) + .edgesHorzontal(75) + + register2LottieView.layoutChain + .top() + .rightToLeftOfView(logoImg, offset: 45) + .width(100) + .heightToWidth(1.0) + + view.addSubview(register2Star3) + register2Star3.layoutChain + .top(25) + .rightToView(logoImg, offset: -6) + .width(26) + .height(20) + + view.addSubview(register2Star2) + register2Star2.layoutChain + .top(32) + .rightToView(logoImg, offset: 25) + .width(48) + .height(44) + + view.addSubview(register2Btn) + register2Btn.layoutChain + .topToBottomOfView(textLab, offset: 30) + .width(224) + .height(50) + .centerX() + + view.addSubview(register2CloseBtn) + register2CloseBtn.layoutChain + .topToView(bgImg, offset: 32) + .right(15) + + return view + }() + + lazy var register2LottieView: LottieAnimationView = { + let view = LottieAnimationView(name: "dialog_register_tip2") + view.loopMode = .loop + return view + }() + + lazy var register2Star1: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/register_heart") + return view + }() + + lazy var register2Star2: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/star_2") + return view + }() + + lazy var register2Star3: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "PopupWindow/star_3") + return view + }() + + lazy var register2CloseBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 15) + return btn + }() + + lazy var register2Btn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("免费注册", for: .normal) + btn.setTitleColor(.white, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) + btn.cornerRadius = 25 + return btn + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + addSubview(bgView) + bgView.addSubview(searchPopupView) + bgView.addSubview(unlockVip1View) + bgView.addSubview(registerPopupView) + bgView.addSubview(register2PopupView) + + searchPopupView.layoutChain + .centerY() + .centerX() + + registerPopupView.layoutChain + .edgesHorzontal() + .bottom() + .height(308) + + register2PopupView.layoutChain + .edgesHorzontal() + .bottom() + .height(295) + + unlockVip1View.layoutChain + .edgesHorzontal() + .bottom() + .height(unlockVip1PopupHeight) + + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + } +} diff --git a/QuickLocation/UIKit/CollectionHFlowLayout.swift b/QuickLocation/UIKit/CollectionHFlowLayout.swift index 46cd6c7..4d61b91 100644 --- a/QuickLocation/UIKit/CollectionHFlowLayout.swift +++ b/QuickLocation/UIKit/CollectionHFlowLayout.swift @@ -54,7 +54,7 @@ class CollectionHFlowLayout: UICollectionViewLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { attributesArr.compactMap { - if rect.contains($0.frame) { + if rect.intersects($0.frame) { return $0 } return nil