对搜索结果展示页的完善
This commit is contained in:
parent
c9b7e2d935
commit
971cad0358
@ -131,10 +131,12 @@
|
|||||||
CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD3135E2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift */; };
|
CBD3135F2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD3135E2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift */; };
|
||||||
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD313602BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift */; };
|
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD313602BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift */; };
|
||||||
CBD5AEE12BBBE45300BF5A43 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */; };
|
CBD5AEE12BBBE45300BF5A43 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */; };
|
||||||
|
CBD5E80C2BF33D0200A3EBED /* MPPositive_SearchResultTypeShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD5E80B2BF33D0200A3EBED /* MPPositive_SearchResultTypeShowView.swift */; };
|
||||||
CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */; };
|
CBD958D22BB6600500666B0D /* MP_PlayerSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */; };
|
||||||
CBD958D42BB6942F00666B0D /* MPSideA_VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */; };
|
CBD958D42BB6942F00666B0D /* MPSideA_VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */; };
|
||||||
CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */; };
|
CBDD516D2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */; };
|
||||||
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */; };
|
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */; };
|
||||||
|
CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */; };
|
||||||
CBE1CB442BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */; };
|
CBE1CB442BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */; };
|
||||||
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */; };
|
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */; };
|
||||||
CBE1CB4C2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */; };
|
CBE1CB4C2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */; };
|
||||||
@ -153,10 +155,14 @@
|
|||||||
CBF456DD2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */; };
|
CBF456DD2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */; };
|
||||||
CBF456DF2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456DE2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift */; };
|
CBF456DF2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456DE2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift */; };
|
||||||
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */; };
|
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */; };
|
||||||
|
CBF456E32BF2086600ABF761 /* MPPositive_LoadSearchResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456E22BF2086600ABF761 /* MPPositive_LoadSearchResultsViewModel.swift */; };
|
||||||
|
CBF456E72BF20BBD00ABF761 /* MPPositive_SearchResultShowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456E62BF20BBD00ABF761 /* MPPositive_SearchResultShowTableViewCell.swift */; };
|
||||||
|
CBF456E92BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456E82BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift */; };
|
||||||
|
CBF456EB2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */; };
|
||||||
|
CBF456ED2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */; };
|
||||||
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */; };
|
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */; };
|
||||||
CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */; };
|
CBFECE372BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */; };
|
||||||
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */; };
|
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */; };
|
||||||
CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */; };
|
|
||||||
CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */; };
|
CBFECE3D2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */; };
|
||||||
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */; };
|
CBFECE3F2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -289,10 +295,12 @@
|
|||||||
CBD3135E2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeListFourthCollectionViewCell.swift; sourceTree = "<group>"; };
|
CBD3135E2BD642D90015D227 /* MPPositive_HomeListFourthCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeListFourthCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
CBD313602BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeListFifthCollectionViewCell.swift; sourceTree = "<group>"; };
|
CBD313602BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_HomeListFifthCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
CBD5AEE02BBBE45300BF5A43 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||||
|
CBD5E80B2BF33D0200A3EBED /* MPPositive_SearchResultTypeShowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultTypeShowView.swift; sourceTree = "<group>"; };
|
||||||
CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_PlayerSlider.swift; sourceTree = "<group>"; };
|
CBD958D12BB6600500666B0D /* MP_PlayerSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MP_PlayerSlider.swift; sourceTree = "<group>"; };
|
||||||
CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_VolumeManager.swift; sourceTree = "<group>"; };
|
CBD958D32BB6942F00666B0D /* MPSideA_VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSideA_VolumeManager.swift; sourceTree = "<group>"; };
|
||||||
CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonNext.swift; sourceTree = "<group>"; };
|
CBDD516C2BEC6AFE000F12C5 /* MPPositive_JsonNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonNext.swift; sourceTree = "<group>"; };
|
||||||
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLoadViewModel.swift; sourceTree = "<group>"; };
|
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_PlayerLoadViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemListModel.swift; sourceTree = "<group>"; };
|
||||||
CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreContentViewController.swift; sourceTree = "<group>"; };
|
CBE1CB432BDDEAAD00701D57 /* MPPositive_MoreContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreContentViewController.swift; sourceTree = "<group>"; };
|
||||||
CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreListContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
CBE1CB492BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_MoreListContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_ListHeaderModel.swift; sourceTree = "<group>"; };
|
CBE1CB4B2BDE440E00701D57 /* MPPositive_ListHeaderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_ListHeaderModel.swift; sourceTree = "<group>"; };
|
||||||
@ -311,10 +319,14 @@
|
|||||||
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultListViewModel.swift; sourceTree = "<group>"; };
|
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultListViewModel.swift; sourceTree = "<group>"; };
|
||||||
CBF456DE2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultItemModel.swift; sourceTree = "<group>"; };
|
CBF456DE2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultItemModel.swift; sourceTree = "<group>"; };
|
||||||
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultItemViewModel.swift; sourceTree = "<group>"; };
|
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultItemViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
CBF456E22BF2086600ABF761 /* MPPositive_LoadSearchResultsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_LoadSearchResultsViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
CBF456E62BF20BBD00ABF761 /* MPPositive_SearchResultShowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultShowTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
CBF456E82BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultPreviewShowView.swift; sourceTree = "<group>"; };
|
||||||
|
CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionsView.swift; sourceTree = "<group>"; };
|
||||||
|
CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultsShowView.swift; sourceTree = "<group>"; };
|
||||||
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemTableViewCell.swift; sourceTree = "<group>"; };
|
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchSuggestions.swift; sourceTree = "<group>"; };
|
CBFECE362BF0C11000E07DC4 /* MPPositive_JsonSearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchSuggestions.swift; sourceTree = "<group>"; };
|
||||||
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemModel.swift; sourceTree = "<group>"; };
|
CBFECE382BF0CFF900E07DC4 /* MPPositive_SearchSuggestionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionItemModel.swift; sourceTree = "<group>"; };
|
||||||
CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchSuggestionListTableViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultShowViewController.swift; sourceTree = "<group>"; };
|
CBFECE3C2BF112D800E07DC4 /* MPPositive_SearchResultShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_SearchResultShowViewController.swift; sourceTree = "<group>"; };
|
||||||
CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchResults.swift; sourceTree = "<group>"; };
|
CBFECE3E2BF1176B00E07DC4 /* MPPositive_JsonSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPPositive_JsonSearchResults.swift; sourceTree = "<group>"; };
|
||||||
E2C6C85BFD4CD80DBA96D149 /* Pods-MusicPlayer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MusicPlayer.release.xcconfig"; path = "Target Support Files/Pods-MusicPlayer/Pods-MusicPlayer.release.xcconfig"; sourceTree = "<group>"; };
|
E2C6C85BFD4CD80DBA96D149 /* Pods-MusicPlayer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MusicPlayer.release.xcconfig"; path = "Target Support Files/Pods-MusicPlayer/Pods-MusicPlayer.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
@ -678,6 +690,7 @@
|
|||||||
CBE1CB4D2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift */,
|
CBE1CB4D2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift */,
|
||||||
CBE1CB4F2BDE4CC500701D57 /* MPPositive_ListHeaderViewModel.swift */,
|
CBE1CB4F2BDE4CC500701D57 /* MPPositive_ListHeaderViewModel.swift */,
|
||||||
CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */,
|
CBEE8E372BEB92CC007DA798 /* MPPositive_SongViewModel.swift */,
|
||||||
|
CBE16B942BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift */,
|
||||||
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */,
|
CBF456DC2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift */,
|
||||||
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */,
|
CBF456E02BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift */,
|
||||||
);
|
);
|
||||||
@ -689,6 +702,7 @@
|
|||||||
children = (
|
children = (
|
||||||
CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */,
|
CBD0CC582BDA238100C4B64D /* MPPositive_BrowseLoadViewModel.swift */,
|
||||||
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */,
|
CBDD516E2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift */,
|
||||||
|
CBF456E22BF2086600ABF761 /* MPPositive_LoadSearchResultsViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = LoadViewModels;
|
path = LoadViewModels;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -794,8 +808,12 @@
|
|||||||
CBCB50212BD118BB009760B3 /* Search */ = {
|
CBCB50212BD118BB009760B3 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
CBF456EA2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift */,
|
||||||
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */,
|
CBFECE342BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift */,
|
||||||
CBFECE3A2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift */,
|
CBF456EC2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift */,
|
||||||
|
CBF456E82BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift */,
|
||||||
|
CBD5E80B2BF33D0200A3EBED /* MPPositive_SearchResultTypeShowView.swift */,
|
||||||
|
CBF456E62BF20BBD00ABF761 /* MPPositive_SearchResultShowTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = Search;
|
path = Search;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1078,10 +1096,12 @@
|
|||||||
CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */,
|
CBCAFB5A2BB3C2A000BC6520 /* LayoutConstraint.swift in Sources */,
|
||||||
CBCB4FEF2BD11402009760B3 /* MPSideA_NavigationController.swift in Sources */,
|
CBCB4FEF2BD11402009760B3 /* MPSideA_NavigationController.swift in Sources */,
|
||||||
CBF456DD2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift in Sources */,
|
CBF456DD2BF1E72F00ABF761 /* MPPositive_SearchResultListViewModel.swift in Sources */,
|
||||||
|
CBD5E80C2BF33D0200A3EBED /* MPPositive_SearchResultTypeShowView.swift in Sources */,
|
||||||
CBCB35212BD7ACE900802900 /* MPPositive_JsonBrowse.swift in Sources */,
|
CBCB35212BD7ACE900802900 /* MPPositive_JsonBrowse.swift in Sources */,
|
||||||
CBCB4FF62BD11402009760B3 /* MPSideA_DeleteViewController.swift in Sources */,
|
CBCB4FF62BD11402009760B3 /* MPSideA_DeleteViewController.swift in Sources */,
|
||||||
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */,
|
CBFECE392BF0CFFA00E07DC4 /* MPPositive_SearchSuggestionItemModel.swift in Sources */,
|
||||||
CBF456DF2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift in Sources */,
|
CBF456DF2BF1E8A500ABF761 /* MPPositive_SearchResultItemModel.swift in Sources */,
|
||||||
|
CBF456E72BF20BBD00ABF761 /* MPPositive_SearchResultShowTableViewCell.swift in Sources */,
|
||||||
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */,
|
CBDD516F2BECBA6E000F12C5 /* MPPositive_PlayerLoadViewModel.swift in Sources */,
|
||||||
CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */,
|
CBEE8E382BEB92CC007DA798 /* MPPositive_SongViewModel.swift in Sources */,
|
||||||
CBCB50122BD11402009760B3 /* MPSideA_Home_FirstListCollectionViewCell.swift in Sources */,
|
CBCB50122BD11402009760B3 /* MPSideA_Home_FirstListCollectionViewCell.swift in Sources */,
|
||||||
@ -1100,6 +1120,7 @@
|
|||||||
CBCAFB662BB3C82C00BC6520 /* MP_LunchViewController.swift in Sources */,
|
CBCAFB662BB3C82C00BC6520 /* MP_LunchViewController.swift in Sources */,
|
||||||
CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */,
|
CBCB4FEA2BD11402009760B3 /* MPSideA_MusicModel.swift in Sources */,
|
||||||
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */,
|
CBBFA91A2BBA846600057FD5 /* CoreDataDelegete.swift in Sources */,
|
||||||
|
CBF456E32BF2086600ABF761 /* MPPositive_LoadSearchResultsViewModel.swift in Sources */,
|
||||||
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */,
|
CB56612D2BE0DF8C00CFD014 /* MP_WebWork.swift in Sources */,
|
||||||
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */,
|
CBFECE352BF0847F00E07DC4 /* MPPositive_SearchSuggestionItemTableViewCell.swift in Sources */,
|
||||||
CBCB50042BD11402009760B3 /* MPSideA_HomeViewController.swift in Sources */,
|
CBCB50042BD11402009760B3 /* MPSideA_HomeViewController.swift in Sources */,
|
||||||
@ -1118,6 +1139,7 @@
|
|||||||
CBCB50082BD11402009760B3 /* MPSideA_BottomShowView.swift in Sources */,
|
CBCB50082BD11402009760B3 /* MPSideA_BottomShowView.swift in Sources */,
|
||||||
CBEE8E342BEB16BB007DA798 /* MPPositive_PlayerSilder.swift in Sources */,
|
CBEE8E342BEB16BB007DA798 /* MPPositive_PlayerSilder.swift in Sources */,
|
||||||
CB09189F2BD26AFC006D2B39 /* MPPositive_HomeViewController.swift in Sources */,
|
CB09189F2BD26AFC006D2B39 /* MPPositive_HomeViewController.swift in Sources */,
|
||||||
|
CBF456ED2BF2253D00ABF761 /* MPPositive_SearchResultsShowView.swift in Sources */,
|
||||||
CBCAFB612BB3C59500BC6520 /* InstanceFromNib.swift in Sources */,
|
CBCAFB612BB3C59500BC6520 /* InstanceFromNib.swift in Sources */,
|
||||||
CBCB4FFA2BD11402009760B3 /* MPSideA_PrivacyViewController.swift in Sources */,
|
CBCB4FFA2BD11402009760B3 /* MPSideA_PrivacyViewController.swift in Sources */,
|
||||||
CBCB500E2BD11402009760B3 /* MPSideA_CenterTableViewCell.swift in Sources */,
|
CBCB500E2BD11402009760B3 /* MPSideA_CenterTableViewCell.swift in Sources */,
|
||||||
@ -1128,11 +1150,13 @@
|
|||||||
CB0918A32BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift in Sources */,
|
CB0918A32BD26B2F006D2B39 /* MPPositive_LibraryViewController.swift in Sources */,
|
||||||
CBEE8E322BEB0FC0007DA798 /* MPPositive_PlayerCoverView.swift in Sources */,
|
CBEE8E322BEB0FC0007DA798 /* MPPositive_PlayerCoverView.swift in Sources */,
|
||||||
CBC32A552BD8DFB900687171 /* MPPositive_BrowseModuleListViewModel.swift in Sources */,
|
CBC32A552BD8DFB900687171 /* MPPositive_BrowseModuleListViewModel.swift in Sources */,
|
||||||
|
CBF456E92BF21E0E00ABF761 /* MPPositive_SearchResultPreviewShowView.swift in Sources */,
|
||||||
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */,
|
CBF456E12BF1EB4300ABF761 /* MPPositive_SearchResultItemViewModel.swift in Sources */,
|
||||||
CBE1CB4E2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift in Sources */,
|
CBE1CB4E2BDE4BD800701D57 /* MPPositive_ListAlbumListViewModel.swift in Sources */,
|
||||||
CBD313572BD63B390015D227 /* MPPositive_HomeListSecondCollectionViewCell.swift in Sources */,
|
CBD313572BD63B390015D227 /* MPPositive_HomeListSecondCollectionViewCell.swift in Sources */,
|
||||||
0096622D2BB14A5A00FCA65F /* AppDelegate.swift in Sources */,
|
0096622D2BB14A5A00FCA65F /* AppDelegate.swift in Sources */,
|
||||||
CBC32A532BD8D9F300687171 /* MPPositive_BrowseItemModel.swift in Sources */,
|
CBC32A532BD8D9F300687171 /* MPPositive_BrowseItemModel.swift in Sources */,
|
||||||
|
CBE16B952BF251FF005B7EE6 /* MPPositive_SearchSuggestionItemListModel.swift in Sources */,
|
||||||
CBCB4FEC2BD11402009760B3 /* MPSideA_AddViewController.swift in Sources */,
|
CBCB4FEC2BD11402009760B3 /* MPSideA_AddViewController.swift in Sources */,
|
||||||
CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */,
|
CBB75B0B2BEF0BC400B3FF9A /* MPPositive_DownloadItemModel.swift in Sources */,
|
||||||
CBCB50172BD11402009760B3 /* MPSideA_Home_RowListsTableViewCell.swift in Sources */,
|
CBCB50172BD11402009760B3 /* MPSideA_Home_RowListsTableViewCell.swift in Sources */,
|
||||||
@ -1141,7 +1165,6 @@
|
|||||||
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */,
|
CBD313612BD6453A0015D227 /* MPPositive_HomeListFifthCollectionViewCell.swift in Sources */,
|
||||||
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */,
|
CBE1CB4A2BDDEBF000701D57 /* MPPositive_MoreListContentCollectionViewCell.swift in Sources */,
|
||||||
CBCB4FF82BD11402009760B3 /* MPSideA_MoreViewController.swift in Sources */,
|
CBCB4FF82BD11402009760B3 /* MPSideA_MoreViewController.swift in Sources */,
|
||||||
CBFECE3B2BF0E51800E07DC4 /* MPPositive_SearchSuggestionListTableViewCell.swift in Sources */,
|
|
||||||
CBCB50142BD11402009760B3 /* MPSideA_Home_FourthListCollectionViewCell.swift in Sources */,
|
CBCB50142BD11402009760B3 /* MPSideA_Home_FourthListCollectionViewCell.swift in Sources */,
|
||||||
CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */,
|
CB5661292BE09D0500CFD014 /* MPPositive_JsonPlayer.swift in Sources */,
|
||||||
CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */,
|
CBCB50062BD11402009760B3 /* MPSideA_PlayerViewController.swift in Sources */,
|
||||||
@ -1171,6 +1194,7 @@
|
|||||||
CBE477B12BB16CCC0031C14B /* Macro.swift in Sources */,
|
CBE477B12BB16CCC0031C14B /* Macro.swift in Sources */,
|
||||||
CBCB4FFE2BD11402009760B3 /* MPSideA_ServiceViewController.swift in Sources */,
|
CBCB4FFE2BD11402009760B3 /* MPSideA_ServiceViewController.swift in Sources */,
|
||||||
CBE2C4CB2BC7BE5D00F283A7 /* MP_NetWorkManager.swift in Sources */,
|
CBE2C4CB2BC7BE5D00F283A7 /* MP_NetWorkManager.swift in Sources */,
|
||||||
|
CBF456EB2BF222EC00ABF761 /* MPPositive_SearchSuggestionsView.swift in Sources */,
|
||||||
CBBFA91E2BBA9B5C00057FD5 /* Notification.swift in Sources */,
|
CBBFA91E2BBA9B5C00057FD5 /* Notification.swift in Sources */,
|
||||||
CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */,
|
CBCB321A2BD7578500802900 /* MP_LocationManager.swift in Sources */,
|
||||||
CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */,
|
CBCB4FEB2BD11402009760B3 /* MPSideA_MusicViewModel.swift in Sources */,
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
22
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Contents.json
vendored
Normal file
22
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Group_1597880643@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Group_1597880643@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Group_1597880643@2x.png
vendored
Normal file
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Group_1597880643@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Group_1597880643@3x.png
vendored
Normal file
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_Delete'logo.imageset/Group_1597880643@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 B |
22
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Contents.json
vendored
Normal file
22
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Frame@2x.png
vendored
Normal file
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 608 B |
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Frame@3x.png
vendored
Normal file
BIN
MusicPlayer/Assets.xcassets/Positive/Search/Search_ICON'logo.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 929 B |
@ -33,12 +33,26 @@ class MP_LunchViewController: UIViewController {
|
|||||||
MP_LocationManager.shared.setLocationPermission(self, complete: nil)
|
MP_LocationManager.shared.setLocationPermission(self, complete: nil)
|
||||||
//获取youtube网站信息
|
//获取youtube网站信息
|
||||||
MP_WebWork.shared.pingYoutubeHome()
|
MP_WebWork.shared.pingYoutubeHome()
|
||||||
|
|
||||||
|
NotificationCenter.notificationKey.add(observer: self, selector: #selector(jumpAction(_:)), notificationName: .js_edit_completion)
|
||||||
}
|
}
|
||||||
deinit {
|
deinit {
|
||||||
//销毁计时器
|
//销毁计时器
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
timer = nil
|
timer = nil
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
@objc private func jumpAction(_ sender:Notification) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
//停止计时器
|
||||||
|
timer.isPaused = true
|
||||||
|
//加载完毕,判断并跳转
|
||||||
|
accessAppdelegate.switch_positive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//计时器执行事件
|
//计时器执行事件
|
||||||
@objc fileprivate func timerActionClick(_ link:CADisplayLink) {
|
@objc fileprivate func timerActionClick(_ link:CADisplayLink) {
|
||||||
if maxTimes > currentTimes {
|
if maxTimes > currentTimes {
|
||||||
@ -52,15 +66,15 @@ class MP_LunchViewController: UIViewController {
|
|||||||
progressView.setProgress(value)
|
progressView.setProgress(value)
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
DispatchQueue.main.async {
|
print("进度已满")
|
||||||
[weak self] in
|
// DispatchQueue.main.async {
|
||||||
guard let self = self else {return}
|
// [weak self] in
|
||||||
//停止计时器
|
// guard let self = self else {return}
|
||||||
timer.isPaused = true
|
// //停止计时器
|
||||||
//加载完毕,判断并跳转
|
// timer.isPaused = true
|
||||||
// accessAppdelegate.switch_aSide()
|
// //加载完毕,判断并跳转
|
||||||
accessAppdelegate.switch_positive()
|
// accessAppdelegate.switch_positive()
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,8 @@ extension NotificationCenter{
|
|||||||
///A面音乐实体重命名
|
///A面音乐实体重命名
|
||||||
case sideA_rename_music
|
case sideA_rename_music
|
||||||
//MARK: - b面通知内容
|
//MARK: - b面通知内容
|
||||||
|
///JS代码注入完成
|
||||||
|
case js_edit_completion
|
||||||
///预览数据已更新
|
///预览数据已更新
|
||||||
case positive_browses_reload
|
case positive_browses_reload
|
||||||
///列表数据已更新
|
///列表数据已更新
|
||||||
|
|||||||
@ -21,3 +21,10 @@ extension String {
|
|||||||
return anyClass
|
return anyClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
extension Range where Bound == String.Index {
|
||||||
|
func toNSRange(in string: String) -> NSRange {
|
||||||
|
let utf16IndexStart = string.utf16.distance(from: string.utf16.startIndex, to: lowerBound)
|
||||||
|
let utf16IndexEnd = string.utf16.distance(from: string.utf16.startIndex, to: upperBound)
|
||||||
|
return NSRange(location: utf16IndexStart, length: utf16IndexEnd - utf16IndexStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
@_exported import JXSegmentedView
|
||||||
|
@_exported import JXPagingView
|
||||||
|
//给JXPagingListContainerView添加extension,表示遵从JXSegmentedViewListContainer的协议
|
||||||
|
extension JXPagingListContainerView: JXSegmentedViewListContainer {}
|
||||||
//MARK: - 常用宏定义
|
//MARK: - 常用宏定义
|
||||||
///屏幕宽
|
///屏幕宽
|
||||||
let screen_Width = UIScreen.main.bounds.width
|
let screen_Width = UIScreen.main.bounds.width
|
||||||
|
|||||||
@ -519,9 +519,9 @@ extension MP_NetWorkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求搜索结果
|
/// 请求搜索预览结果
|
||||||
/// - Parameter text: 用户请求文本
|
/// - Parameter text: 用户请求文本
|
||||||
func requestSearchResults(_ text:String) {
|
func requestSearchPreviewResults(_ text:String, completion:@escaping (([MPPositive_SearchResultListViewModel]) -> Void)) {
|
||||||
//拼接路径
|
//拼接路径
|
||||||
let path = header+point+search
|
let path = header+point+search
|
||||||
//设置url
|
//设置url
|
||||||
@ -548,17 +548,75 @@ extension MP_NetWorkManager {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
//
|
requestPostSearchPreviewResults(url, parameters: parameters) { result in
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//请求搜索结果
|
//请求搜索预览结果
|
||||||
private func requestPostSearchResults(_ url:URL, parameters:Parameters) {
|
private func requestPostSearchPreviewResults(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SearchResultListViewModel]) -> Void)) {
|
||||||
//发送post请求
|
//发送post请求
|
||||||
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchResults.self) { [weak self] (response) in
|
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchPreviewResults.self) { [weak self] (response) in
|
||||||
guard let self = self else {return}
|
guard let self = self else {return}
|
||||||
switch response.result {
|
switch response.result {
|
||||||
case .success(let value):
|
case .success(let value):
|
||||||
if let contents = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents {
|
if let contents = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents {
|
||||||
parsingSearchResults(contents)
|
let result = parsingSearchPreviewResults(contents)
|
||||||
|
print("一共搜索到\(result.count)相关内容模块")
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
// 请求失败,处理错误
|
||||||
|
print("Request failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 请求搜索分类结果
|
||||||
|
/// - Parameters:
|
||||||
|
/// - query: 搜索文本
|
||||||
|
/// - params: 事件参数码
|
||||||
|
func requestSearchTypeResults(_ query:String, params:String, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) {
|
||||||
|
//拼接路径
|
||||||
|
let path = header+point+search
|
||||||
|
//设置url
|
||||||
|
guard let url = URL(string: path) else {
|
||||||
|
print("Url is Incorrect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//设置参数
|
||||||
|
let parameters:[String:Any] = [
|
||||||
|
"query":query,
|
||||||
|
"prettyPrint":"false",
|
||||||
|
"params":params,
|
||||||
|
"context":[
|
||||||
|
"client":[
|
||||||
|
//web端
|
||||||
|
"clientName": "WEB_REMIX",
|
||||||
|
"visitorData":visitorData,
|
||||||
|
//当前访问版本(日期值)
|
||||||
|
"clientVersion": "1.\(Date().timeZone().toString(.custom("YYYYMMdd"))).01.00",
|
||||||
|
"platform":"DESKTOP",
|
||||||
|
//语言
|
||||||
|
"hl":Language_first_local,
|
||||||
|
//地址
|
||||||
|
"gl":Location_First
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
requestPostSearchTypeResults(url, parameters: parameters) { result in
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//请求搜索分页结果
|
||||||
|
private func requestPostSearchTypeResults(_ url:URL, parameters:Parameters, completion:@escaping (([MPPositive_SearchResultItemViewModel], String?, String?))->Void) {
|
||||||
|
//发送post请求
|
||||||
|
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseDecodable(of: JsonSearchTypeResults.self) { [weak self] (response) in
|
||||||
|
guard let self = self else {return}
|
||||||
|
switch response.result {
|
||||||
|
case .success(let value):
|
||||||
|
if let musicShelfRenderer = value.contents?.tabbedSearchResultsRenderer?.tabs?.first?.tabRenderer?.content?.sectionListRenderer?.contents?.first?.musicShelfRenderer {
|
||||||
|
completion(parsingSearchTypeResults(musicShelfRenderer))
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// 请求失败,处理错误
|
// 请求失败,处理错误
|
||||||
@ -869,21 +927,187 @@ extension MP_NetWorkManager {
|
|||||||
completion(sections)
|
completion(sections)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
///解析搜索结果_SearchResults
|
///解析搜索预览结果_SearchPreviewResults
|
||||||
private func parsingSearchResults(_ contents:[JsonSearchResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) {
|
private func parsingSearchPreviewResults(_ contents:[JsonSearchPreviewResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content]) -> [MPPositive_SearchResultListViewModel]{
|
||||||
|
var resultListSections:[MPPositive_SearchResultListViewModel] = []
|
||||||
contents.forEach { content in
|
contents.forEach { content in
|
||||||
let resultList = MPPositive_SearchResultListViewModel()
|
let resultList = MPPositive_SearchResultListViewModel()
|
||||||
//判断当前模块是最佳结果还是其它模块
|
//判断当前模块是最佳结果还是其它模块
|
||||||
if let musicCardShelfRenderer = content.musicCardShelfRenderer {
|
if let musicCardShelfRenderer = content.musicCardShelfRenderer {
|
||||||
//当前是最佳结果
|
//当前是最佳结果
|
||||||
|
//设置模块标题
|
||||||
|
resultList.title = musicCardShelfRenderer.header?.musicCardShelfHeaderBasicRenderer?.title?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
//生成第一个内容块(根据内容不同,可能是单曲/歌手/专辑)
|
||||||
|
let item = MPPositive_SearchResultItemModel()
|
||||||
|
//设置第一个内容块的预览图
|
||||||
|
item.reviewUrls = musicCardShelfRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||||
|
//设置第一个内容块的一级标题和类型和ID
|
||||||
if let title = musicCardShelfRenderer.title {
|
if let title = musicCardShelfRenderer.title {
|
||||||
//设置模块标题
|
title.runs?.forEach({ run in
|
||||||
resultList.title = title.runs?.reduce("", { $0 + ($1.text ?? "")})
|
item.title = run.text
|
||||||
|
if run.navigationEndpoint?.watchEndpoint != nil {
|
||||||
|
//这个内容块是单曲/视频
|
||||||
|
item.videoId = run.navigationEndpoint?.watchEndpoint?.videoId
|
||||||
|
item.itemType = .single
|
||||||
|
}else {
|
||||||
|
//这个内容块是艺术家/列表
|
||||||
|
item.browseId = run.navigationEndpoint?.browseEndpoint?.browseId
|
||||||
|
if run.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == "MUSIC_PAGE_TYPE_ARTIST" {
|
||||||
|
//是艺术家
|
||||||
|
item.itemType = .artist
|
||||||
|
}else {
|
||||||
|
//是列表专辑
|
||||||
|
item.itemType = .list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//设置第一个内容块的二级标题
|
||||||
|
if let subtitle = musicCardShelfRenderer.subtitle {
|
||||||
|
item.subtitle = subtitle.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
}
|
||||||
|
//第一个内容块填充完毕
|
||||||
|
resultList.previewItemViews.append(.init(item))
|
||||||
|
//设置最佳结果的其他内容块
|
||||||
|
if let contents = musicCardShelfRenderer.contents {
|
||||||
|
contents.forEach { content in
|
||||||
|
//生成更多内容块
|
||||||
|
let item = MPPositive_SearchResultItemModel()
|
||||||
|
if let musicResponsiveListItemRenderer = content.musicResponsiveListItemRenderer {
|
||||||
|
//设置预览图
|
||||||
|
item.reviewUrls = musicResponsiveListItemRenderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||||
|
//设置一级标题与二级标题
|
||||||
|
for (index, flexColumn) in (musicResponsiveListItemRenderer.flexColumns ?? []).enumerated() {
|
||||||
|
if index == 0 {
|
||||||
|
//一级标题
|
||||||
|
item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
}else {
|
||||||
|
//二级标题
|
||||||
|
item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//设置id和类型
|
||||||
|
if musicResponsiveListItemRenderer.playlistItemData != nil {
|
||||||
|
//是单曲
|
||||||
|
item.itemType = .single
|
||||||
|
item.videoId = musicResponsiveListItemRenderer.playlistItemData?.videoId
|
||||||
|
}else {
|
||||||
|
//是专辑列表/艺术家
|
||||||
|
item.browseId = musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseId
|
||||||
|
if musicResponsiveListItemRenderer.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == "MUSIC_PAGE_TYPE_ARTIST" {
|
||||||
|
//是艺术家
|
||||||
|
item.itemType = .artist
|
||||||
|
}else {
|
||||||
|
//是专辑/列表
|
||||||
|
item.itemType = .list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item.title != nil {
|
||||||
|
resultList.previewItemViews.append(.init(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//由于是最佳结果模块,所以没有query和params值
|
||||||
|
resultList.query = nil
|
||||||
|
resultList.params = nil
|
||||||
|
}else if let musicShelfRenderer = content.musicShelfRenderer {
|
||||||
|
//当前是其他结果,不存在首选内容块
|
||||||
|
//设置模块标题
|
||||||
|
resultList.title = musicShelfRenderer.title?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
//设置内容块
|
||||||
|
musicShelfRenderer.contents?.forEach({ content in
|
||||||
|
//专辑/列表/单曲视频/艺术家
|
||||||
|
let item = MPPositive_SearchResultItemModel()
|
||||||
|
//设置内容块的封面
|
||||||
|
item.reviewUrls = content.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||||
|
//设置一级标题与二级标题
|
||||||
|
for (index, flexColumn) in (content.musicResponsiveListItemRenderer?.flexColumns ?? []).enumerated() {
|
||||||
|
if index == 0 {
|
||||||
|
//一级标题
|
||||||
|
item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
}else {
|
||||||
|
//二级标题
|
||||||
|
item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//设置id和类型
|
||||||
|
if content.musicResponsiveListItemRenderer?.playlistItemData != nil {
|
||||||
|
//是单曲
|
||||||
|
item.itemType = .single
|
||||||
|
item.videoId = content.musicResponsiveListItemRenderer?.playlistItemData?.videoId
|
||||||
|
}else {
|
||||||
|
//是专辑列表/艺术家
|
||||||
|
item.browseId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId
|
||||||
|
if content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == "MUSIC_PAGE_TYPE_ARTIST" {
|
||||||
|
//是艺术家
|
||||||
|
item.itemType = .artist
|
||||||
|
}else {
|
||||||
|
//是专辑/列表
|
||||||
|
item.itemType = .list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultList.previewItemViews.append(.init(item))
|
||||||
|
})
|
||||||
|
//设置query和params值
|
||||||
|
if let searchEndpoint = musicShelfRenderer.bottomEndpoint?.searchEndpoint {
|
||||||
|
resultList.query = searchEndpoint.query
|
||||||
|
resultList.params = searchEndpoint.params
|
||||||
}
|
}
|
||||||
}else {
|
|
||||||
//当前是其他结果
|
|
||||||
}
|
}
|
||||||
|
resultListSections.append(resultList)
|
||||||
}
|
}
|
||||||
|
resultListSections = resultListSections.filter({$0.previewItemViews.count != 0})
|
||||||
|
return resultListSections
|
||||||
|
}
|
||||||
|
///解析搜索分页结果
|
||||||
|
private func parsingSearchTypeResults(_ musicShelfRenderer:JsonSearchTypeResults.Contents.TabbedSearchResultsRenderer.Tab.TabRenderer.Content.SectionListRenderer.Content.MusicShelfRenderer) -> ([MPPositive_SearchResultItemViewModel], String?, String?) {
|
||||||
|
var array:[MPPositive_SearchResultItemViewModel] = []
|
||||||
|
if let contents = musicShelfRenderer.contents {
|
||||||
|
///解析内容块
|
||||||
|
contents.forEach({ content in
|
||||||
|
//专辑/列表/单曲视频/艺术家
|
||||||
|
let item = MPPositive_SearchResultItemModel()
|
||||||
|
//设置内容块的封面
|
||||||
|
item.reviewUrls = content.musicResponsiveListItemRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.map({$0.url ?? ""})
|
||||||
|
//设置一级标题与二级标题
|
||||||
|
for (index, flexColumn) in (content.musicResponsiveListItemRenderer?.flexColumns ?? []).enumerated() {
|
||||||
|
if index == 0 {
|
||||||
|
//一级标题
|
||||||
|
item.title = flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")})
|
||||||
|
}else {
|
||||||
|
//二级标题
|
||||||
|
item.subtitle = (item.subtitle ?? "") + (flexColumn.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.reduce("", { $0 + ($1.text ?? "")}) ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//设置id和类型
|
||||||
|
if content.musicResponsiveListItemRenderer?.playlistItemData != nil {
|
||||||
|
//是单曲
|
||||||
|
item.itemType = .single
|
||||||
|
item.videoId = content.musicResponsiveListItemRenderer?.playlistItemData?.videoId
|
||||||
|
}else {
|
||||||
|
//是专辑列表/艺术家
|
||||||
|
item.browseId = content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId
|
||||||
|
if content.musicResponsiveListItemRenderer?.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == "MUSIC_PAGE_TYPE_ARTIST" {
|
||||||
|
//是艺术家
|
||||||
|
item.itemType = .artist
|
||||||
|
}else {
|
||||||
|
//是专辑/列表
|
||||||
|
item.itemType = .list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//添加内容块
|
||||||
|
array.append(.init(item))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var continuation:String?
|
||||||
|
var clickTrackingParams:String?
|
||||||
|
if let continuations = musicShelfRenderer.continuations {
|
||||||
|
///解析继续编码
|
||||||
|
continuation = continuations.nextContinuationData?.continuation
|
||||||
|
clickTrackingParams = continuations.nextContinuationData?.clickTrackingParams
|
||||||
|
}
|
||||||
|
return (array, continuation, clickTrackingParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - 解析具体内容形式
|
//MARK: - 解析具体内容形式
|
||||||
|
|||||||
@ -174,6 +174,8 @@ extension MP_WebWork: WKNavigationDelegate, WKUIDelegate {
|
|||||||
print("注入代码失败:\(error)")
|
print("注入代码失败:\(error)")
|
||||||
}else {
|
}else {
|
||||||
print("注入代码完成")
|
print("注入代码完成")
|
||||||
|
//发布切换启动页
|
||||||
|
NotificationCenter.notificationKey.post(notificationName: .js_edit_completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
//MARK: - 搜索结果结构
|
||||||
///搜索结果结构
|
///搜索预览结果结构
|
||||||
struct JsonSearchResults: Codable {
|
struct JsonSearchPreviewResults: Codable {
|
||||||
let contents:Contents?
|
let contents:Contents?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case contents = "contents"
|
case contents = "contents"
|
||||||
@ -76,7 +76,7 @@ struct JsonSearchResults: Codable {
|
|||||||
struct Content: Codable {
|
struct Content: Codable {
|
||||||
///最佳结果
|
///最佳结果
|
||||||
let musicCardShelfRenderer:MusicCardShelfRenderer?
|
let musicCardShelfRenderer:MusicCardShelfRenderer?
|
||||||
///其他结果
|
///其他结果(同时还有更多内容键值)
|
||||||
let musicShelfRenderer:MusicShelfRenderer?
|
let musicShelfRenderer:MusicShelfRenderer?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case musicCardShelfRenderer = "musicCardShelfRenderer"
|
case musicCardShelfRenderer = "musicCardShelfRenderer"
|
||||||
@ -182,7 +182,7 @@ struct JsonSearchResults: Codable {
|
|||||||
struct NavigationEndpoint: Codable {
|
struct NavigationEndpoint: Codable {
|
||||||
///这个值存在就是音乐单曲
|
///这个值存在就是音乐单曲
|
||||||
let watchEndpoint:WatchEndpoint?
|
let watchEndpoint:WatchEndpoint?
|
||||||
///这个值存在就是艺术家
|
///这个值存在就是艺术家/专辑/列表
|
||||||
let browseEndpoint:BrowseEndpoint?
|
let browseEndpoint:BrowseEndpoint?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case watchEndpoint = "watchEndpoint"
|
case watchEndpoint = "watchEndpoint"
|
||||||
@ -205,13 +205,35 @@ struct JsonSearchResults: Codable {
|
|||||||
}
|
}
|
||||||
struct BrowseEndpoint: Codable {
|
struct BrowseEndpoint: Codable {
|
||||||
let browseId:String?
|
let browseId:String?
|
||||||
|
let browseEndpointContextSupportedConfigs:BrowseEndpointContextSupportedConfigs?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case browseId = "browseId"
|
case browseId = "browseId"
|
||||||
|
case browseEndpointContextSupportedConfigs = "browseEndpointContextSupportedConfigs"
|
||||||
}
|
}
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||||
|
browseEndpointContextSupportedConfigs = try values.decodeIfPresent(BrowseEndpointContextSupportedConfigs.self, forKey: .browseEndpointContextSupportedConfigs)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextSupportedConfigs: Codable {
|
||||||
|
let browseEndpointContextMusicConfig:BrowseEndpointContextMusicConfig?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseEndpointContextMusicConfig = "browseEndpointContextMusicConfig"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseEndpointContextMusicConfig = try values.decodeIfPresent(BrowseEndpointContextMusicConfig.self, forKey: .browseEndpointContextMusicConfig)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextMusicConfig: Codable {
|
||||||
|
let pageType:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pageType = "pageType"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
pageType = try values.decodeIfPresent(String.self, forKey: .pageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -299,7 +321,7 @@ struct JsonSearchResults: Codable {
|
|||||||
let flexColumns:[FlexColumn]?
|
let flexColumns:[FlexColumn]?
|
||||||
///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频)
|
///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频)
|
||||||
let playlistItemData:PlaylistItemData?
|
let playlistItemData:PlaylistItemData?
|
||||||
///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单)
|
///专辑/歌单ID/歌手(navigationEndpoint存在说明这条数据是专辑/歌单/歌手)
|
||||||
let navigationEndpoint:NavigationEndpoint?
|
let navigationEndpoint:NavigationEndpoint?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case thumbnail = "thumbnail"
|
case thumbnail = "thumbnail"
|
||||||
@ -416,13 +438,35 @@ struct JsonSearchResults: Codable {
|
|||||||
}
|
}
|
||||||
struct BrowseEndpoint: Codable {
|
struct BrowseEndpoint: Codable {
|
||||||
let browseId:String?
|
let browseId:String?
|
||||||
|
let browseEndpointContextSupportedConfigs:BrowseEndpointContextSupportedConfigs?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case browseId = "browseId"
|
case browseId = "browseId"
|
||||||
|
case browseEndpointContextSupportedConfigs = "browseEndpointContextSupportedConfigs"
|
||||||
}
|
}
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||||
|
browseEndpointContextSupportedConfigs = try values.decodeIfPresent(BrowseEndpointContextSupportedConfigs.self, forKey: .browseEndpointContextSupportedConfigs)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextSupportedConfigs: Codable {
|
||||||
|
let browseEndpointContextMusicConfig:BrowseEndpointContextMusicConfig?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseEndpointContextMusicConfig = "browseEndpointContextMusicConfig"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseEndpointContextMusicConfig = try values.decodeIfPresent(BrowseEndpointContextMusicConfig.self, forKey: .browseEndpointContextMusicConfig)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextMusicConfig: Codable {
|
||||||
|
let pageType:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pageType = "pageType"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
pageType = try values.decodeIfPresent(String.self, forKey: .pageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -435,14 +479,18 @@ struct JsonSearchResults: Codable {
|
|||||||
let title:Title?
|
let title:Title?
|
||||||
///模块内容
|
///模块内容
|
||||||
let contents:[Content]?
|
let contents:[Content]?
|
||||||
|
///更多内容
|
||||||
|
let bottomEndpoint:BottomEndpoint?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case title = "title"
|
case title = "title"
|
||||||
case contents = "contents"
|
case contents = "contents"
|
||||||
|
case bottomEndpoint = "bottomEndpoint"
|
||||||
}
|
}
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
title = try values.decodeIfPresent(Title.self, forKey: .title)
|
title = try values.decodeIfPresent(Title.self, forKey: .title)
|
||||||
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||||
|
bottomEndpoint = try values.decodeIfPresent(BottomEndpoint.self, forKey: .bottomEndpoint)
|
||||||
}
|
}
|
||||||
struct Title:Codable {
|
struct Title:Codable {
|
||||||
let runs:[Run]?
|
let runs:[Run]?
|
||||||
@ -599,18 +647,357 @@ struct JsonSearchResults: Codable {
|
|||||||
}
|
}
|
||||||
struct BrowseEndpoint: Codable {
|
struct BrowseEndpoint: Codable {
|
||||||
let browseId:String?
|
let browseId:String?
|
||||||
|
let browseEndpointContextSupportedConfigs:BrowseEndpointContextSupportedConfigs?
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case browseId = "browseId"
|
case browseId = "browseId"
|
||||||
|
case browseEndpointContextSupportedConfigs = "browseEndpointContextSupportedConfigs"
|
||||||
}
|
}
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||||
|
browseEndpointContextSupportedConfigs = try values.decodeIfPresent(BrowseEndpointContextSupportedConfigs.self, forKey: .browseEndpointContextSupportedConfigs)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextSupportedConfigs: Codable {
|
||||||
|
let browseEndpointContextMusicConfig:BrowseEndpointContextMusicConfig?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseEndpointContextMusicConfig = "browseEndpointContextMusicConfig"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseEndpointContextMusicConfig = try values.decodeIfPresent(BrowseEndpointContextMusicConfig.self, forKey: .browseEndpointContextMusicConfig)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextMusicConfig: Codable {
|
||||||
|
let pageType:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pageType = "pageType"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
pageType = try values.decodeIfPresent(String.self, forKey: .pageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
struct BottomEndpoint: Codable {
|
||||||
|
let searchEndpoint:SearchEndpoint?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case searchEndpoint = "searchEndpoint"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
searchEndpoint = try values.decodeIfPresent(SearchEndpoint.self, forKey: .searchEndpoint)
|
||||||
|
}
|
||||||
|
struct SearchEndpoint: Codable {
|
||||||
|
///当前搜索文本
|
||||||
|
let query:String?
|
||||||
|
///更多内容参数
|
||||||
|
let params:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case query = "query"
|
||||||
|
case params = "params"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
query = try values.decodeIfPresent(String.self, forKey: .query)
|
||||||
|
params = try values.decodeIfPresent(String.self, forKey: .params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///搜索分类结果结构(该分类详情)
|
||||||
|
struct JsonSearchTypeResults: Codable {
|
||||||
|
let contents:Contents?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case contents = "contents"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
contents = try values.decodeIfPresent(Contents.self, forKey: .contents)
|
||||||
|
}
|
||||||
|
struct Contents: Codable {
|
||||||
|
let tabbedSearchResultsRenderer:TabbedSearchResultsRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case tabbedSearchResultsRenderer = "tabbedSearchResultsRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
tabbedSearchResultsRenderer = try values.decodeIfPresent(TabbedSearchResultsRenderer.self, forKey: .tabbedSearchResultsRenderer)
|
||||||
|
}
|
||||||
|
struct TabbedSearchResultsRenderer: Codable {
|
||||||
|
let tabs:[Tab]?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case tabs = "tabs"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
tabs = try values.decodeIfPresent([Tab].self, forKey: .tabs)
|
||||||
|
}
|
||||||
|
struct Tab:Codable {
|
||||||
|
let tabRenderer:TabRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case tabRenderer = "tabRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
tabRenderer = try values.decodeIfPresent(TabRenderer.self, forKey: .tabRenderer)
|
||||||
|
}
|
||||||
|
struct TabRenderer: Codable {
|
||||||
|
let content:Content?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case content = "content"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
content = try values.decodeIfPresent(Content.self, forKey: .content)
|
||||||
|
}
|
||||||
|
struct Content: Codable {
|
||||||
|
let sectionListRenderer:SectionListRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case sectionListRenderer = "sectionListRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
sectionListRenderer = try values.decodeIfPresent(SectionListRenderer.self, forKey: .sectionListRenderer)
|
||||||
|
}
|
||||||
|
struct SectionListRenderer: Codable {
|
||||||
|
///模块(每个模块内容不一样)
|
||||||
|
let contents:[Content]?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case contents = "contents"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||||
|
}
|
||||||
|
struct Content:Codable {
|
||||||
|
let musicShelfRenderer:MusicShelfRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case musicShelfRenderer = "musicShelfRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
musicShelfRenderer = try values.decodeIfPresent(MusicShelfRenderer.self, forKey: .musicShelfRenderer)
|
||||||
|
}
|
||||||
|
struct MusicShelfRenderer: Codable {
|
||||||
|
///内容块
|
||||||
|
let contents:[Content]?
|
||||||
|
///继续请求块
|
||||||
|
let continuations:Continuations?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case contents = "contents"
|
||||||
|
case continuations = "continuations"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
contents = try values.decodeIfPresent([Content].self, forKey: .contents)
|
||||||
|
continuations = try values.decodeIfPresent(Continuations.self, forKey: .continuations)
|
||||||
|
}
|
||||||
|
///内容块
|
||||||
|
struct Content: Codable {
|
||||||
|
let musicResponsiveListItemRenderer:MusicResponsiveListItemRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case musicResponsiveListItemRenderer = "musicResponsiveListItemRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
musicResponsiveListItemRenderer = try values.decodeIfPresent(MusicResponsiveListItemRenderer.self, forKey: .musicResponsiveListItemRenderer)
|
||||||
|
}
|
||||||
|
struct MusicResponsiveListItemRenderer: Codable {
|
||||||
|
///封面
|
||||||
|
let thumbnail:Thumbnail?
|
||||||
|
///文本内容(第0位是标题,其他拼成副标题)
|
||||||
|
let flexColumns:[FlexColumn]?
|
||||||
|
///单曲/视频ID(playlistItemData存在说明这条数据单曲/视频)
|
||||||
|
let playlistItemData:PlaylistItemData?
|
||||||
|
///专辑/歌单ID(navigationEndpoint存在说明这条数据是专辑/歌单)
|
||||||
|
let navigationEndpoint:NavigationEndpoint?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case thumbnail = "thumbnail"
|
||||||
|
case flexColumns = "flexColumns"
|
||||||
|
case playlistItemData = "playlistItemData"
|
||||||
|
case navigationEndpoint = "navigationEndpoint"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||||
|
flexColumns = try values.decodeIfPresent([FlexColumn].self, forKey: .flexColumns)
|
||||||
|
playlistItemData = try values.decodeIfPresent(PlaylistItemData.self, forKey: .playlistItemData)
|
||||||
|
navigationEndpoint = try values.decodeIfPresent(NavigationEndpoint.self, forKey: .navigationEndpoint)
|
||||||
|
}
|
||||||
|
struct Thumbnail: Codable {
|
||||||
|
let musicThumbnailRenderer:MusicThumbnailRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case musicThumbnailRenderer = "musicThumbnailRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
musicThumbnailRenderer = try values.decodeIfPresent(MusicThumbnailRenderer.self, forKey: .musicThumbnailRenderer)
|
||||||
|
}
|
||||||
|
struct MusicThumbnailRenderer: Codable {
|
||||||
|
let thumbnail:Thumbnail?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case thumbnail = "thumbnail"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
thumbnail = try values.decodeIfPresent(Thumbnail.self, forKey: .thumbnail)
|
||||||
|
}
|
||||||
|
struct Thumbnail: Codable {
|
||||||
|
let thumbnails:[Thumbnails]?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case thumbnails = "thumbnails"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
thumbnails = try values.decodeIfPresent([Thumbnails].self, forKey: .thumbnails)
|
||||||
|
}
|
||||||
|
struct Thumbnails: Codable {
|
||||||
|
let url:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case url = "url"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
url = try values.decodeIfPresent(String.self, forKey: .url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct FlexColumn: Codable {
|
||||||
|
let musicResponsiveListItemFlexColumnRenderer:MusicResponsiveListItemFlexColumnRenderer?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case musicResponsiveListItemFlexColumnRenderer = "musicResponsiveListItemFlexColumnRenderer"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
musicResponsiveListItemFlexColumnRenderer = try values.decodeIfPresent(MusicResponsiveListItemFlexColumnRenderer.self, forKey: .musicResponsiveListItemFlexColumnRenderer)
|
||||||
|
}
|
||||||
|
struct MusicResponsiveListItemFlexColumnRenderer: Codable {
|
||||||
|
let text:Text?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case text = "text"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
text = try values.decodeIfPresent(Text.self, forKey: .text)
|
||||||
|
}
|
||||||
|
struct Text: Codable {
|
||||||
|
let runs:[Run]?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case runs = "runs"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
runs = try values.decodeIfPresent([Run].self, forKey: .runs)
|
||||||
|
}
|
||||||
|
struct Run: Codable {
|
||||||
|
///标题文本
|
||||||
|
let text:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case text = "text"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
text = try values.decodeIfPresent(String.self, forKey: .text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct PlaylistItemData: Codable {
|
||||||
|
let videoId:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case videoId = "videoId"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
videoId = try values.decodeIfPresent(String.self, forKey: .videoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct NavigationEndpoint: Codable {
|
||||||
|
let browseEndpoint:BrowseEndpoint?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseEndpoint = "browseEndpoint"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseEndpoint = try values.decodeIfPresent(BrowseEndpoint.self, forKey: .browseEndpoint)
|
||||||
|
}
|
||||||
|
struct BrowseEndpoint: Codable {
|
||||||
|
let browseId:String?
|
||||||
|
let browseEndpointContextSupportedConfigs:BrowseEndpointContextSupportedConfigs?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseId = "browseId"
|
||||||
|
case browseEndpointContextSupportedConfigs = "browseEndpointContextSupportedConfigs"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseId = try values.decodeIfPresent(String.self, forKey: .browseId)
|
||||||
|
browseEndpointContextSupportedConfigs = try values.decodeIfPresent(BrowseEndpointContextSupportedConfigs.self, forKey: .browseEndpointContextSupportedConfigs)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextSupportedConfigs: Codable {
|
||||||
|
let browseEndpointContextMusicConfig:BrowseEndpointContextMusicConfig?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case browseEndpointContextMusicConfig = "browseEndpointContextMusicConfig"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
browseEndpointContextMusicConfig = try values.decodeIfPresent(BrowseEndpointContextMusicConfig.self, forKey: .browseEndpointContextMusicConfig)
|
||||||
|
}
|
||||||
|
struct BrowseEndpointContextMusicConfig: Codable {
|
||||||
|
let pageType:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pageType = "pageType"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
pageType = try values.decodeIfPresent(String.self, forKey: .pageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///继续请求块
|
||||||
|
struct Continuations: Codable {
|
||||||
|
let nextContinuationData:NextContinuationData?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case nextContinuationData = "nextContinuationData"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
nextContinuationData = try values.decodeIfPresent(NextContinuationData.self, forKey: .nextContinuationData)
|
||||||
|
}
|
||||||
|
struct NextContinuationData: Codable {
|
||||||
|
///继续请求编码
|
||||||
|
let continuation:String?
|
||||||
|
///继续请求参数
|
||||||
|
let clickTrackingParams:String?
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case continuation = "continuation"
|
||||||
|
case clickTrackingParams = "clickTrackingParams"
|
||||||
|
}
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
continuation = try values.decodeIfPresent(String.self, forKey: .continuation)
|
||||||
|
clickTrackingParams = try values.decodeIfPresent(String.self, forKey: .clickTrackingParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class MPPositive_SearchResultItemModel: NSObject {
|
|||||||
var subtitle:String?
|
var subtitle:String?
|
||||||
///单曲视频/VideoID
|
///单曲视频/VideoID
|
||||||
var videoId:String?
|
var videoId:String?
|
||||||
///歌单专辑/browseID
|
///预览ID(可以是歌单/专辑/艺术家)
|
||||||
var browseId:String?
|
var browseId:String?
|
||||||
///类型
|
///类型
|
||||||
var itemType:BrowseItemType?
|
var itemType:BrowseItemType?
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Kingfisher
|
||||||
class MPPositive_SearchResultItemViewModel: NSObject {
|
class MPPositive_SearchResultItemViewModel: NSObject {
|
||||||
///预览图路径
|
///预览图路径
|
||||||
var reviewUrl:URL?
|
var reviewUrl:URL?
|
||||||
@ -18,5 +18,29 @@ class MPPositive_SearchResultItemViewModel: NSObject {
|
|||||||
var item:MPPositive_SearchResultItemModel!
|
var item:MPPositive_SearchResultItemModel!
|
||||||
init(_ resultItem:MPPositive_SearchResultItemModel) {
|
init(_ resultItem:MPPositive_SearchResultItemModel) {
|
||||||
super.init()
|
super.init()
|
||||||
|
item = resultItem
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
private func configure() {
|
||||||
|
if let url = URL(string: item.reviewUrls?.last ?? "") {
|
||||||
|
reviewUrl = url
|
||||||
|
}
|
||||||
|
if item.title != nil {
|
||||||
|
title = item.title
|
||||||
|
}
|
||||||
|
if item.subtitle != nil {
|
||||||
|
subtitle = item.subtitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//设置预览图
|
||||||
|
func setImage(_ imageView:UIImageView) {
|
||||||
|
if reviewUrl != nil {
|
||||||
|
imageView.kf.setImage(with: reviewUrl, placeholder: placeholderImage)
|
||||||
|
}
|
||||||
|
if item.itemType == .artist {
|
||||||
|
imageView.layer.cornerRadius = 30*width
|
||||||
|
}else {
|
||||||
|
imageView.layer.cornerRadius = 10*width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,41 @@ import UIKit
|
|||||||
class MPPositive_SearchResultListViewModel: NSObject {
|
class MPPositive_SearchResultListViewModel: NSObject {
|
||||||
///模块标题
|
///模块标题
|
||||||
var title:String!
|
var title:String!
|
||||||
///模块内容
|
///预览内容模块组(只展示预览部分内容/通常是3-4个)
|
||||||
var itemViews:[MPPositive_SearchResultItemViewModel] = []
|
var previewItemViews:[MPPositive_SearchResultItemViewModel] = []
|
||||||
|
///搜索文本(非最佳结果模块才有)
|
||||||
|
var query:String?
|
||||||
|
///搜索键值(非最佳结果模块才有)
|
||||||
|
var params:String?
|
||||||
|
///当前键值下全部的内容模块组
|
||||||
|
var allItemViews:[MPPositive_SearchResultItemViewModel] = []{
|
||||||
|
didSet{
|
||||||
|
print("用户加载\(title ?? "")的新内容")
|
||||||
|
if completionBlock != nil {
|
||||||
|
completionBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///当前分页信息
|
||||||
|
var continuation:String?
|
||||||
|
///当前分页键值
|
||||||
|
var itct:String?
|
||||||
|
///请求完成
|
||||||
|
var completionBlock:(() -> Void)?
|
||||||
|
///请求分类详情数据
|
||||||
|
func requestSearchType() {
|
||||||
|
guard let query = self.query, let params = self.params else {
|
||||||
|
print("当前模块已是全部内容")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//调用搜索分页结果接口补全全部的内容模块组
|
||||||
|
MP_NetWorkManager.shared.requestSearchTypeResults(query, params: params) { [weak self] (items, continuation, itct) in
|
||||||
|
guard let self = self else {return}
|
||||||
|
//对全部的内容模块组进行追加
|
||||||
|
allItemViews.append(contentsOf: items)
|
||||||
|
//记录分页内容
|
||||||
|
self.continuation = continuation
|
||||||
|
self.itct = itct
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchSuggestionItemListModel.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class MPPositive_SearchSuggestionItemListModel: NSObject {
|
||||||
|
///用户搜索的文本
|
||||||
|
var searchText:String?
|
||||||
|
///展示的富文本内容
|
||||||
|
var attributedTexts:[NSAttributedString]?
|
||||||
|
init(_ text:String, suggestions:[[MPPositive_SearchSuggestionItemModel]]) {
|
||||||
|
super.init()
|
||||||
|
searchText = text
|
||||||
|
//将suggestions拆开拼成一组字符串
|
||||||
|
let titles:[String] = suggestions.flatMap({$0.compactMap({($0.title)})})
|
||||||
|
self.attributedTexts = titles.map({createAttributedString(for: $0)})
|
||||||
|
}
|
||||||
|
|
||||||
|
//将与搜索文本相同的字符串转变颜色
|
||||||
|
func createAttributedString(for string: String) -> NSAttributedString {
|
||||||
|
let attributedString = NSMutableAttributedString(string: string, attributes: [.foregroundColor: UIColor.white, .font:UIFont.systemFont(ofSize: 14*width, weight: .medium)])
|
||||||
|
if let range = string.range(of: searchText ?? "") {
|
||||||
|
let narange = range.toNSRange(in: string)
|
||||||
|
attributedString.addAttributes([.foregroundColor:UIColor(hex: "#80F988")], range: narange)
|
||||||
|
}
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_LoadSearchResultsViewModel.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
///搜索结果数据管理模型
|
||||||
|
class MPPositive_LoadSearchResultsViewModel: NSObject {
|
||||||
|
///模块组
|
||||||
|
var sectionLists:[MPPositive_SearchResultListViewModel]!{
|
||||||
|
didSet{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
//得到了搜索结果
|
||||||
|
if resultReloadBlock != nil {
|
||||||
|
resultReloadBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var resultReloadBlock:(() -> Void)?
|
||||||
|
//用户输入的搜索文本
|
||||||
|
init(_ text:String){
|
||||||
|
super.init()
|
||||||
|
getSearchResults(text)
|
||||||
|
}
|
||||||
|
//根据用户输入文本内容请求搜索接口
|
||||||
|
private func getSearchResults(_ text:String) {
|
||||||
|
MP_NetWorkManager.shared.requestSearchPreviewResults(text) { [weak self] results in
|
||||||
|
self?.sectionLists = results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,9 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
|
|||||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(reloadAction(_ :)), notificationName: .positive_browses_reload)
|
NotificationCenter.notificationKey.add(observer: self, selector: #selector(reloadAction(_ :)), notificationName: .positive_browses_reload)
|
||||||
NotificationCenter.notificationKey.add(observer: self, selector: #selector(listAction(_ :)), notificationName: .positive_list_reload)
|
NotificationCenter.notificationKey.add(observer: self, selector: #selector(listAction(_ :)), notificationName: .positive_list_reload)
|
||||||
|
|
||||||
|
// let item = MPPositive_DownloadItemModel.create()
|
||||||
|
// item.resourcePath = "11"
|
||||||
|
// MPPositive_DownloadItemModel.save()
|
||||||
}
|
}
|
||||||
deinit {
|
deinit {
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
@ -87,7 +90,8 @@ class MPPositive_HomeViewController: MPPositive_BaseViewController{
|
|||||||
//MARK: - 点击
|
//MARK: - 点击
|
||||||
//点击顶部右侧弹出菜单
|
//点击顶部右侧弹出菜单
|
||||||
@objc private func menuRightClick(_ sender:UIButton) {
|
@objc private func menuRightClick(_ sender:UIButton) {
|
||||||
|
// let array = MPPositive_DownloadItemModel.fetchAll()
|
||||||
|
// print(array)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - tableView
|
//MARK: - tableView
|
||||||
|
|||||||
@ -82,9 +82,9 @@ class MPPositive_ListShowViewController: MPPositive_BaseViewController {
|
|||||||
}
|
}
|
||||||
//页面刷新
|
//页面刷新
|
||||||
private func reload() {
|
private func reload() {
|
||||||
listOrAlbum.header.setUrltoImage(coverImageView)
|
listOrAlbum.header?.setUrltoImage(coverImageView)
|
||||||
titleLabel.text = listOrAlbum.header.title
|
titleLabel.text = listOrAlbum.header?.title
|
||||||
descriptionLabel.text = listOrAlbum.header._description
|
descriptionLabel.text = listOrAlbum.header?._description
|
||||||
playListNumberLabel.text = "Play all (\(listOrAlbum.items.count))"
|
playListNumberLabel.text = "Play all (\(listOrAlbum.items.count))"
|
||||||
}
|
}
|
||||||
//MARK: - UI生成与配置
|
//MARK: - UI生成与配置
|
||||||
|
|||||||
@ -8,11 +8,205 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
///搜索结果控制器
|
///搜索结果控制器
|
||||||
class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
|
class MPPositive_SearchResultShowViewController: MPPositive_BaseViewController {
|
||||||
|
//MARK: - 导航View中的控件
|
||||||
|
//顶部搜索textField
|
||||||
|
private lazy var searchTextField:UITextField = {
|
||||||
|
let textField = UITextField()
|
||||||
|
textField.delegate = self
|
||||||
|
textField.font = .systemFont(ofSize: 14*width, weight: .regular)
|
||||||
|
textField.textColor = .white
|
||||||
|
//设置一个富文本占位符
|
||||||
|
let attributedText = NSAttributedString(string: "Search songs,artists,playlists", attributes: [.font:UIFont.systemFont(ofSize: 14*width, weight: .regular), .foregroundColor:UIColor(hex: "#666666")])
|
||||||
|
textField.attributedPlaceholder = attributedText
|
||||||
|
return textField
|
||||||
|
}()
|
||||||
|
//删除文本按钮
|
||||||
|
private lazy var deleteBtn:UIButton = {
|
||||||
|
let btn:UIButton = .init()
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Search_Delete'logo"), for: .normal)
|
||||||
|
btn.addTarget(self, action: #selector(deleteTextClick(_ :)), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
//cancel按钮
|
||||||
|
private lazy var cancelBtn:UIButton = {
|
||||||
|
let btn:UIButton = .init()
|
||||||
|
btn.setTitle("Cancel", for: .normal)
|
||||||
|
btn.setTitleColor(.init(hex: "#FFFFFF", alpha: 0.85), for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 14*width, weight: .regular)
|
||||||
|
btn.addTarget(self, action: #selector(backPopClick(_ :)), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
//搜索限定计时器
|
||||||
|
private var debounceTimer: Timer?
|
||||||
|
//对用户展示的搜索建议组
|
||||||
|
private var suggestionList:MPPositive_SearchSuggestionItemListModel!{
|
||||||
|
didSet{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if suggestionList != nil {
|
||||||
|
//搜索建议组存在,将其显现
|
||||||
|
suggestionView.isHidden = false
|
||||||
|
suggestionView.suggestions = suggestionList.attributedTexts
|
||||||
|
}else {
|
||||||
|
suggestionView.isHidden = true
|
||||||
|
suggestionView.suggestions = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//搜索文本
|
||||||
|
private var searchText:String?
|
||||||
|
//MARK: - 搜索建议View
|
||||||
|
private lazy var suggestionView:MPPositive_SearchSuggestionsView = .init(frame: .zero)
|
||||||
|
//MARK: - 结果展示View
|
||||||
|
private lazy var resultsShowView:MPPositive_SearchResultsShowView = .init(frame: .zero)
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
setTitle("Result")
|
setTitle("Result")
|
||||||
setPopBtn()
|
setPopBtn()
|
||||||
|
configure()
|
||||||
|
suggestionView.selectedTextBlock = {
|
||||||
|
[weak self] (text) in
|
||||||
|
guard let self = self else {return}
|
||||||
|
searchText = text
|
||||||
|
searchTextField.text = text
|
||||||
|
resultsShowView.loadModel = .init(text)
|
||||||
|
suggestionView.isHidden = true
|
||||||
|
}
|
||||||
|
resultsShowView.scrollBlock = {
|
||||||
|
[weak self] in
|
||||||
|
self?.view?.endEditing(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deinit{
|
||||||
|
debounceTimer = nil
|
||||||
|
}
|
||||||
|
private func configure() {
|
||||||
|
//配置导航栏
|
||||||
|
let searchView = createSearchView()
|
||||||
|
navView.addSubview(searchView)
|
||||||
|
searchView.snp.makeConstraints { make in
|
||||||
|
make.width.equalTo(275*width)
|
||||||
|
make.height.equalTo(40*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.left.equalToSuperview().offset(18*width)
|
||||||
|
}
|
||||||
|
navView.addSubview(cancelBtn)
|
||||||
|
cancelBtn.snp.makeConstraints { make in
|
||||||
|
make.right.equalToSuperview().offset(-18*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
//添加suggestionView
|
||||||
|
view.addSubview(suggestionView)
|
||||||
|
suggestionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalTo(navView.snp.bottom)
|
||||||
|
}
|
||||||
|
suggestionView.isHidden = true
|
||||||
|
//添加resultsShowView
|
||||||
|
view.addSubview(resultsShowView)
|
||||||
|
resultsShowView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalTo(navView.snp.bottom)
|
||||||
|
}
|
||||||
|
resultsShowView.isHidden = true
|
||||||
|
}
|
||||||
|
//生成一个顶部搜索框
|
||||||
|
private func createSearchView() -> UIView{
|
||||||
|
let searchView:UIView = UIView()
|
||||||
|
searchView.backgroundColor = .init(hex: "#000000")
|
||||||
|
searchView.isUserInteractionEnabled = true
|
||||||
|
searchView.layer.masksToBounds = true
|
||||||
|
searchView.layer.cornerRadius = 20*width
|
||||||
|
//添加一个icon
|
||||||
|
let iconImageView = UIImageView(image: .init(named: "Search_ICON'logo"))
|
||||||
|
searchView.addSubview(iconImageView)
|
||||||
|
iconImageView.snp.makeConstraints { make in
|
||||||
|
make.height.width.equalTo(24*width)
|
||||||
|
make.left.equalToSuperview().offset(8*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
searchView.addSubview(deleteBtn)
|
||||||
|
deleteBtn.snp.makeConstraints { make in
|
||||||
|
make.height.width.equalTo(24*width)
|
||||||
|
make.right.equalToSuperview().offset(-12*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
//添加textField
|
||||||
|
searchView.addSubview(searchTextField)
|
||||||
|
searchTextField.snp.makeConstraints { make in
|
||||||
|
make.top.bottom.equalToSuperview()
|
||||||
|
make.left.equalTo(iconImageView.snp.right).offset(14*width)
|
||||||
|
make.right.equalTo(deleteBtn.snp.left).offset(-12*width)
|
||||||
|
}
|
||||||
|
return searchView
|
||||||
|
}
|
||||||
|
|
||||||
|
//删除输入框文本
|
||||||
|
@objc private func deleteTextClick(_ sender:UIButton) {
|
||||||
|
//清空文本框内容,并隐藏两个View
|
||||||
|
searchTextField.text = ""
|
||||||
|
searchText = ""
|
||||||
|
suggestionView.suggestions = nil
|
||||||
|
resultsShowView.loadModel = nil
|
||||||
|
}
|
||||||
|
//取消并返回上一页
|
||||||
|
@objc private func backPopClick(_ sender:UIButton) {
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
//MARK: - UITextFieldDelegate
|
||||||
|
extension MPPositive_SearchResultShowViewController:UITextFieldDelegate {
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
let text = (textField.text! as NSString).replacingCharacters(in: range, with: string)
|
||||||
|
guard text.count <= 30 else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//文本出现变化,隐藏结果View
|
||||||
|
resultsShowView.isHidden = true
|
||||||
|
if text.isEmpty {
|
||||||
|
self.suggestionList = nil
|
||||||
|
cancelDebounceTimer()
|
||||||
|
}else {
|
||||||
|
//触发网络请求
|
||||||
|
loadSearchSuggestions(text)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
//避免重复请求
|
||||||
|
private func cancelDebounceTimer() {
|
||||||
|
debounceTimer?.invalidate()
|
||||||
|
debounceTimer = nil
|
||||||
|
}
|
||||||
|
//当用户输入文本通过后进行检索
|
||||||
|
private func loadSearchSuggestions(_ text:String) {
|
||||||
|
cancelDebounceTimer()
|
||||||
|
//停止输入0.8秒后调用
|
||||||
|
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||||
|
self?.searchText = text
|
||||||
|
self?.fetchSearchSuggestions(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//获取建议组
|
||||||
|
private func fetchSearchSuggestions(_ text:String) {
|
||||||
|
MP_NetWorkManager.shared.requestSearchSuggestions(text) { [weak self] (result) in
|
||||||
|
self?.suggestionList = .init(self?.searchText ?? "", suggestions: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
//判断textField是否存在文本
|
||||||
|
if let text = textField.text, text.isEmpty != true {
|
||||||
|
self.searchText = text
|
||||||
|
//用户输入了文本
|
||||||
|
resultsShowView.loadModel = .init(text)
|
||||||
|
suggestionView.isHidden = true
|
||||||
|
//停止输入
|
||||||
|
view.endEditing(true)
|
||||||
|
return true
|
||||||
|
}else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,67 +14,6 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
|||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
//顶部搜索textField
|
|
||||||
private lazy var searchTextField:UITextField = {
|
|
||||||
let textField = UITextField()
|
|
||||||
textField.delegate = self
|
|
||||||
textField.font = .systemFont(ofSize: 14*width, weight: .regular)
|
|
||||||
textField.textColor = .white
|
|
||||||
//设置一个富文本占位符
|
|
||||||
let attributedText = NSAttributedString(string: "Search songs,artists,playlists", attributes: [.font:UIFont.systemFont(ofSize: 14*width, weight: .regular), .foregroundColor:UIColor(hex: "#666666")])
|
|
||||||
textField.attributedPlaceholder = attributedText
|
|
||||||
return textField
|
|
||||||
}()
|
|
||||||
//搜索建议tableView
|
|
||||||
private lazy var suggestionsTableView:UITableView = {
|
|
||||||
let tableView = UITableView()
|
|
||||||
tableView.backgroundColor = .init(hex: "#151718")
|
|
||||||
tableView.separatorStyle = .none
|
|
||||||
//设置一个边框
|
|
||||||
tableView.layer.borderWidth = 1*width
|
|
||||||
tableView.layer.borderColor = UIColor(hex: "#FFFFFF", alpha: 0.35).cgColor
|
|
||||||
tableView.layer.masksToBounds = true
|
|
||||||
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
||||||
tableView.layer.cornerRadius = 10*width
|
|
||||||
|
|
||||||
tableView.rowHeight = 60*width
|
|
||||||
tableView.dataSource = self
|
|
||||||
tableView.delegate = self
|
|
||||||
tableView.register(MPPositive_SearchSuggestionItemTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionItemTableViewCellID)
|
|
||||||
tableView.register(MPPositive_SearchSuggestionListTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionListTableViewCellID)
|
|
||||||
return tableView
|
|
||||||
}()
|
|
||||||
private let MPPositive_SearchSuggestionItemTableViewCellID = "MPPositive_SearchSuggestionItemTableViewCell"
|
|
||||||
private let MPPositive_SearchSuggestionListTableViewCellID = "MPPositive_SearchSuggestionListTableViewCell"
|
|
||||||
//对用户展示的搜索建议组
|
|
||||||
private var suggestions:[[MPPositive_SearchSuggestionItemModel]]!{
|
|
||||||
willSet{
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
[weak self] in
|
|
||||||
guard let self = self else {return}
|
|
||||||
if newValue != nil {
|
|
||||||
//搜索到了
|
|
||||||
var count:Int = 0
|
|
||||||
newValue.forEach { items in
|
|
||||||
count += items.count
|
|
||||||
}
|
|
||||||
suggestionsTableView.isHidden = false
|
|
||||||
suggestionsTableView.snp.updateConstraints { make in
|
|
||||||
make.height.equalTo(CGFloat(count*60)*width)
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
//没有搜索到
|
|
||||||
suggestionsTableView.isHidden = true
|
|
||||||
suggestionsTableView.snp.updateConstraints { make in
|
|
||||||
make.height.equalTo(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suggestionsTableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//搜索限定计时器
|
|
||||||
private var debounceTimer: Timer?
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
setTitle("")
|
setTitle("")
|
||||||
@ -86,9 +25,6 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
|||||||
}
|
}
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
searchTextField.text = ""
|
|
||||||
suggestionsTableView.isHidden = true
|
|
||||||
suggestions = nil
|
|
||||||
}
|
}
|
||||||
//配置
|
//配置
|
||||||
private func configure() {
|
private func configure() {
|
||||||
@ -104,14 +40,6 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
|||||||
make.top.right.left.equalToSuperview()
|
make.top.right.left.equalToSuperview()
|
||||||
make.height.equalTo(981*width)
|
make.height.equalTo(981*width)
|
||||||
}
|
}
|
||||||
view.addSubview(suggestionsTableView)
|
|
||||||
suggestionsTableView.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(searchView.snp.bottom)
|
|
||||||
make.left.equalTo(searchView.snp.left).offset(16*width)
|
|
||||||
make.right.equalTo(searchView.snp.right).offset(-16*width)
|
|
||||||
make.height.equalTo(240*width)
|
|
||||||
}
|
|
||||||
suggestionsTableView.isHidden = true
|
|
||||||
}
|
}
|
||||||
//生成一个顶部搜索框
|
//生成一个顶部搜索框
|
||||||
private func createSearchView() -> UIView{
|
private func createSearchView() -> UIView{
|
||||||
@ -121,88 +49,27 @@ class MPPositive_SearchViewController: MPPositive_BaseViewController {
|
|||||||
searchView.layer.masksToBounds = true
|
searchView.layer.masksToBounds = true
|
||||||
searchView.layer.cornerRadius = 16*width
|
searchView.layer.cornerRadius = 16*width
|
||||||
//添加一个icon
|
//添加一个icon
|
||||||
let iconImageView = UIImageView(image: .init(named: "B_Seach"))
|
let iconImageView = UIImageView(image: .init(named: "Search_ICON'logo"))
|
||||||
searchView.addSubview(iconImageView)
|
searchView.addSubview(iconImageView)
|
||||||
iconImageView.snp.makeConstraints { make in
|
iconImageView.snp.makeConstraints { make in
|
||||||
make.height.width.equalTo(16*width)
|
make.height.width.equalTo(16*width)
|
||||||
make.left.equalToSuperview().offset(16*width)
|
make.left.equalToSuperview().offset(16*width)
|
||||||
make.centerY.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
}
|
}
|
||||||
//添加textField
|
let label = createLabel("Search songs,artists,playlists", font: .systemFont(ofSize: 14*width, weight: .regular), textColor: .init(hex: "#666666"), textAlignment: .left)
|
||||||
searchView.addSubview(searchTextField)
|
searchView.addSubview(label)
|
||||||
searchTextField.snp.makeConstraints { make in
|
label.snp.makeConstraints { make in
|
||||||
make.top.bottom.equalToSuperview()
|
|
||||||
make.left.equalTo(iconImageView.snp.right).offset(8*width)
|
make.left.equalTo(iconImageView.snp.right).offset(8*width)
|
||||||
make.right.equalToSuperview().offset(-16*width)
|
make.centerY.equalToSuperview()
|
||||||
}
|
}
|
||||||
|
searchView.isUserInteractionEnabled = true
|
||||||
|
searchView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(searchClick(_:))))
|
||||||
return searchView
|
return searchView
|
||||||
}
|
}
|
||||||
|
//前往搜索结果页
|
||||||
}
|
@objc fileprivate func searchClick(_ sender:UITapGestureRecognizer) {
|
||||||
//MARK: - textField
|
let resultVC = MPPositive_SearchResultShowViewController()
|
||||||
extension MPPositive_SearchViewController:UITextFieldDelegate {
|
navigationController?.pushViewController(resultVC, animated: false)
|
||||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
||||||
let text = (textField.text! as NSString).replacingCharacters(in: range, with: string)
|
|
||||||
guard text.count <= 30 else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if text.isEmpty {
|
|
||||||
suggestions = nil
|
|
||||||
}else {
|
|
||||||
//触发网络请求
|
|
||||||
loadSearchSuggestions(text)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
//避免重复请求
|
|
||||||
private func cancelDebounceTimer() {
|
|
||||||
debounceTimer?.invalidate()
|
|
||||||
debounceTimer = nil
|
|
||||||
}
|
|
||||||
//当用户输入文本通过后进行检索
|
|
||||||
private func loadSearchSuggestions(_ text:String) {
|
|
||||||
cancelDebounceTimer()
|
|
||||||
//停止输入0.8秒后调用
|
|
||||||
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false) { [weak self] _ in
|
|
||||||
self?.fetchSearchSuggestions(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//获取建议组
|
|
||||||
private func fetchSearchSuggestions(_ text:String) {
|
|
||||||
MP_NetWorkManager.shared.requestSearchSuggestions(text) { [weak self] (result) in
|
|
||||||
self?.suggestions = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
||||||
//判断textField是否存在文本
|
|
||||||
if let text = textField.text, text.isEmpty != true {
|
|
||||||
//用户输入了文本
|
|
||||||
let showVC = MPPositive_SearchResultShowViewController()
|
|
||||||
|
|
||||||
navigationController?.pushViewController(showVC, animated: true)
|
|
||||||
return true
|
|
||||||
}else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//MARK: - tableView
|
|
||||||
extension MPPositive_SearchViewController:UITableViewDataSource, UITableViewDelegate {
|
|
||||||
func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return suggestions != nil ? suggestions.count:0
|
|
||||||
}
|
|
||||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return suggestions != nil ? suggestions[section].count:0
|
|
||||||
}
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
if suggestions[indexPath.section][indexPath.row].reviewUrls != nil {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionListTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionListTableViewCell
|
|
||||||
cell.item = suggestions[indexPath.section][indexPath.row]
|
|
||||||
return cell
|
|
||||||
}else {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionItemTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionItemTableViewCell
|
|
||||||
cell.item = suggestions[indexPath.section][indexPath.row]
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,114 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchResultPreviewShowView.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
///对搜索预览结果展示
|
||||||
|
class MPPositive_SearchResultPreviewShowView: UIView, JXSegmentedListContainerViewListDelegate {
|
||||||
|
//tableView
|
||||||
|
private lazy var tableView:UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
tableView.sectionHeaderTopPadding = 0
|
||||||
|
}
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.estimatedRowHeight = 200
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell"
|
||||||
|
private var sectionLists:[MPPositive_SearchResultListViewModel]!{
|
||||||
|
didSet{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if sectionLists != nil {
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var scrollBlock:(() -> Void)?
|
||||||
|
var chooseMoreIndexBlock:((Int) -> Void)?
|
||||||
|
init(frame: CGRect, sectionLists:[MPPositive_SearchResultListViewModel]) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .init(hex: "1A1A1A")
|
||||||
|
self.sectionLists = sectionLists
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
//MARK: - 分页代理
|
||||||
|
func listView() -> UIView {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
//配置
|
||||||
|
private func configure() {
|
||||||
|
addSubview(tableView)
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.top.right.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//查看更多
|
||||||
|
@objc private func moreFindClick(_ sender:UIButton) {
|
||||||
|
let selectedTag = sender.tag
|
||||||
|
if chooseMoreIndexBlock != nil {
|
||||||
|
chooseMoreIndexBlock!(selectedTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - tableView
|
||||||
|
extension MPPositive_SearchResultPreviewShowView:UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
if scrollBlock != nil {
|
||||||
|
scrollBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return sectionLists.count
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return sectionLists[section].previewItemViews.count
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchResultShowTableViewCellID, for: indexPath) as! MPPositive_SearchResultShowTableViewCell
|
||||||
|
cell.itemView = sectionLists[indexPath.section].previewItemViews[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||||
|
let sectionView:UIView = .init(frame: .init(x: 0, y: 0, width: screen_Width, height: 50*width))
|
||||||
|
sectionView.backgroundColor = .init(hex: "#1A1A1A")
|
||||||
|
let label = createLabel(sectionLists[section].title, font: .systemFont(ofSize: 16*width, weight: .semibold), textColor: .white, textAlignment: .left)
|
||||||
|
sectionView.addSubview(label)
|
||||||
|
label.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(18*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
if section != 0 {
|
||||||
|
//更多按钮
|
||||||
|
let btn:UIButton = .init()
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Home_More'logo"), for: .normal)
|
||||||
|
btn.tag = section
|
||||||
|
btn.addTarget(self, action: #selector(moreFindClick(_ :)), for: .touchUpInside)
|
||||||
|
sectionView.addSubview(btn)
|
||||||
|
btn.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(24*width)
|
||||||
|
make.right.equalToSuperview().offset(-18*width)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sectionView
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||||
|
return 50*width
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchResultShowTableViewCell.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class MPPositive_SearchResultShowTableViewCell: UITableViewCell {
|
||||||
|
//特殊图片(展示预览图片)
|
||||||
|
private lazy var iconImageView:UIImageView = {
|
||||||
|
let imageView:UIImageView = .init()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 14*width, weight: .regular), textColor: .white, textAlignment: .left)
|
||||||
|
private lazy var subtitleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left)
|
||||||
|
///更多按钮
|
||||||
|
private lazy var moreBtn:UIButton = {
|
||||||
|
let btn:UIButton = .init()
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Song_More'logo"), for: .normal)
|
||||||
|
btn.addTarget(self, action: #selector(moreActionClick(_ :)), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
///下载状态按钮
|
||||||
|
private lazy var loadBtn:UIButton = {
|
||||||
|
let btn:UIButton = .init()
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Song_Unload'logo"), for: .normal)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Song_Loaded'logo"), for: .selected)
|
||||||
|
btn.addTarget(self, action: #selector(loadActionClick(_ :)), for: .touchUpInside)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
var itemView:MPPositive_SearchResultItemViewModel!{
|
||||||
|
didSet{
|
||||||
|
itemView.setImage(iconImageView)
|
||||||
|
titleLabel.text = itemView.title
|
||||||
|
subtitleLabel.text = itemView.subtitle
|
||||||
|
//检索类型
|
||||||
|
if itemView.item.itemType == .single {
|
||||||
|
moreBtn.isHidden = false
|
||||||
|
loadBtn.isHidden = false
|
||||||
|
}else {
|
||||||
|
moreBtn.isHidden = true
|
||||||
|
loadBtn.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = .clear
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
// Initialization code
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
// Configure the view for the selected state
|
||||||
|
}
|
||||||
|
private func configure() {
|
||||||
|
contentView.addSubview(iconImageView)
|
||||||
|
iconImageView.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(60*width)
|
||||||
|
make.top.equalToSuperview().offset(8*width).priority(999)
|
||||||
|
make.bottom.equalToSuperview().offset(-8*width)
|
||||||
|
make.left.equalToSuperview().offset(18*width)
|
||||||
|
}
|
||||||
|
contentView.addSubview(moreBtn)
|
||||||
|
moreBtn.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(24*width)
|
||||||
|
make.centerY.equalTo(iconImageView.snp.centerY)
|
||||||
|
make.right.equalToSuperview().offset(-18*width)
|
||||||
|
}
|
||||||
|
contentView.addSubview(loadBtn)
|
||||||
|
loadBtn.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(24*width)
|
||||||
|
make.centerY.equalTo(iconImageView.snp.centerY)
|
||||||
|
make.right.equalToSuperview().offset(-54*width)
|
||||||
|
}
|
||||||
|
contentView.addSubview(titleLabel)
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(iconImageView.snp.top).offset(10*width)
|
||||||
|
make.left.equalTo(iconImageView.snp.right).offset(12*width)
|
||||||
|
make.right.equalTo(loadBtn.snp.left).offset(-10*width)
|
||||||
|
}
|
||||||
|
contentView.addSubview(subtitleLabel)
|
||||||
|
subtitleLabel.snp.makeConstraints { make in
|
||||||
|
make.bottom.equalTo(iconImageView.snp.bottom).offset(-10*width)
|
||||||
|
make.left.equalTo(titleLabel.snp.left)
|
||||||
|
make.right.equalTo(titleLabel.snp.right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//点击更多
|
||||||
|
@objc private func moreActionClick(_ sender:UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
//点击下载
|
||||||
|
@objc private func loadActionClick(_ sender:UIButton) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchResultTypeShowView.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
///对搜索结果进行分类详情展示
|
||||||
|
class MPPositive_SearchResultTypeShowView: UIView, JXSegmentedListContainerViewListDelegate {
|
||||||
|
//tableView
|
||||||
|
private lazy var tableView:UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
tableView.sectionHeaderTopPadding = 0
|
||||||
|
}
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.estimatedRowHeight = 200
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.register(MPPositive_SearchResultShowTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchResultShowTableViewCellID)
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
private let MPPositive_SearchResultShowTableViewCellID = "MPPositive_SearchResultShowTableViewCell"
|
||||||
|
//对应的分类详情展示
|
||||||
|
private var sectionList:MPPositive_SearchResultListViewModel!{
|
||||||
|
didSet{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if sectionList != nil {
|
||||||
|
if sectionList.allItemViews.count == 0 {
|
||||||
|
//开始请求内容
|
||||||
|
sectionList.requestSearchType()
|
||||||
|
}
|
||||||
|
sectionList.completionBlock = {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var scrollBlock:(() -> Void)?
|
||||||
|
|
||||||
|
init(frame: CGRect, sectionList:MPPositive_SearchResultListViewModel) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .init(hex: "1A1A1A")
|
||||||
|
self.sectionList = sectionList
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
//MARK: - 分页代理
|
||||||
|
func listView() -> UIView {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
//配置
|
||||||
|
private func configure() {
|
||||||
|
addSubview(tableView)
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.top.right.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - tableView
|
||||||
|
extension MPPositive_SearchResultTypeShowView:UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
if scrollBlock != nil {
|
||||||
|
scrollBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return sectionList?.allItemViews.count ?? 0
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchResultShowTableViewCellID, for: indexPath) as! MPPositive_SearchResultShowTableViewCell
|
||||||
|
cell.itemView = sectionList.allItemViews[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchResultsShowView.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class MPPositive_SearchResultsShowView: UIView {
|
||||||
|
///搜索结果管理模型
|
||||||
|
var loadModel:MPPositive_LoadSearchResultsViewModel!{
|
||||||
|
didSet{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else {return}
|
||||||
|
if loadModel == nil {
|
||||||
|
isHidden = true
|
||||||
|
}else {
|
||||||
|
loadModel.resultReloadBlock = {
|
||||||
|
[weak self] in
|
||||||
|
//刷新分页控制器
|
||||||
|
guard let self = self else {return}
|
||||||
|
isHidden = false
|
||||||
|
dataSource.titles = loadModel?.sectionLists.compactMap({$0.title}) ?? []
|
||||||
|
dataSource.reloadData(selectedIndex: 0)
|
||||||
|
segmentView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - 分页栏设置
|
||||||
|
//分页栏View
|
||||||
|
private lazy var segmentView:JXSegmentedView = {
|
||||||
|
var jxSegmentView = JXSegmentedView()
|
||||||
|
jxSegmentView.backgroundColor = .init(hex: "1A1A1A")
|
||||||
|
return jxSegmentView
|
||||||
|
}()
|
||||||
|
//数据源
|
||||||
|
private lazy var dataSource:JXSegmentedTitleDataSource = {
|
||||||
|
var dataSource = JXSegmentedTitleDataSource()
|
||||||
|
//标题未选中状态
|
||||||
|
dataSource.titleNormalColor = .init(hex: "#666666")
|
||||||
|
dataSource.titleNormalFont = .systemFont(ofSize: 16*width, weight: .regular)
|
||||||
|
//标题选中状态
|
||||||
|
dataSource.titleSelectedColor = .init(hex: "#80F988")
|
||||||
|
dataSource.titleSelectedFont = .systemFont(ofSize: 16*width, weight: .bold)
|
||||||
|
//是否颜色过度
|
||||||
|
dataSource.isTitleColorGradientEnabled = true
|
||||||
|
dataSource.isSelectedAnimable = true
|
||||||
|
dataSource.isItemTransitionEnabled = true
|
||||||
|
return dataSource
|
||||||
|
}()
|
||||||
|
//指示器
|
||||||
|
private lazy var indicator:JXSegmentedIndicatorLineView = {
|
||||||
|
let indicator = JXSegmentedIndicatorLineView()
|
||||||
|
indicator.indicatorWidth = 16*width
|
||||||
|
indicator.indicatorHeight = 3*width
|
||||||
|
indicator.indicatorCornerRadius = 1.5*width
|
||||||
|
indicator.indicatorColor = .init(hex: "#80F988")
|
||||||
|
indicator.indicatorPosition = .bottom
|
||||||
|
return indicator
|
||||||
|
}()
|
||||||
|
//分页内容承载框
|
||||||
|
fileprivate lazy var listContainerView:JXSegmentedListContainerView = {
|
||||||
|
let listContainerView = JXSegmentedListContainerView(dataSource: self)
|
||||||
|
listContainerView.backgroundColor = .clear
|
||||||
|
return listContainerView
|
||||||
|
}()
|
||||||
|
var scrollBlock:(() -> Void)?
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
//配置
|
||||||
|
private func configure() {
|
||||||
|
segmentView.indicators = [indicator]
|
||||||
|
segmentView.dataSource = dataSource
|
||||||
|
//配置View
|
||||||
|
addSubview(segmentView)
|
||||||
|
segmentView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview()
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.height.equalTo(60*width)
|
||||||
|
}
|
||||||
|
addSubview(listContainerView)
|
||||||
|
listContainerView.snp.makeConstraints { (make) in
|
||||||
|
make.left.right.bottom.equalToSuperview().priority(999)
|
||||||
|
make.top.equalTo(segmentView.snp.bottom).priority(999)
|
||||||
|
}
|
||||||
|
segmentView.contentScrollView = listContainerView.scrollView
|
||||||
|
segmentView.listContainer = listContainerView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - JXSegmentedTitleDataSource
|
||||||
|
extension MPPositive_SearchResultsShowView: JXSegmentedListContainerViewDataSource, JXSegmentedViewDelegate {
|
||||||
|
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
|
||||||
|
return dataSource.titles.count
|
||||||
|
}
|
||||||
|
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate {
|
||||||
|
if index == 0 {
|
||||||
|
//展示预览结果
|
||||||
|
let showView:MPPositive_SearchResultPreviewShowView = .init(frame: listContainerView.frame, sectionLists: loadModel.sectionLists)
|
||||||
|
showView.scrollBlock = {
|
||||||
|
[weak self] in
|
||||||
|
if self?.scrollBlock != nil {
|
||||||
|
self?.scrollBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showView.chooseMoreIndexBlock = {
|
||||||
|
[weak self] (selectedIndex) in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
segmentView.selectItemAt(index: selectedIndex)
|
||||||
|
}
|
||||||
|
return showView
|
||||||
|
}else {
|
||||||
|
//展示分类结果
|
||||||
|
let showView:MPPositive_SearchResultTypeShowView = .init(frame: listContainerView.frame, sectionList: loadModel.sectionLists[index])
|
||||||
|
showView.scrollBlock = {
|
||||||
|
[weak self] in
|
||||||
|
if self?.scrollBlock != nil {
|
||||||
|
self?.scrollBlock!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,12 +8,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class MPPositive_SearchSuggestionItemTableViewCell: UITableViewCell {
|
class MPPositive_SearchSuggestionItemTableViewCell: UITableViewCell {
|
||||||
//搜索Icon
|
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 14*width, weight: .medium), textColor: .white, textAlignment: .left)
|
||||||
private lazy var iconImageView:UIImageView = UIImageView(image: .init(named: "B_Seach"))
|
var item:NSAttributedString!{
|
||||||
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left)
|
|
||||||
var item:MPPositive_SearchSuggestionItemModel!{
|
|
||||||
didSet{
|
didSet{
|
||||||
titleLabel.text = item.title ?? ""
|
titleLabel.attributedText = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
@ -36,17 +34,11 @@ class MPPositive_SearchSuggestionItemTableViewCell: UITableViewCell {
|
|||||||
// Configure the view for the selected state
|
// Configure the view for the selected state
|
||||||
}
|
}
|
||||||
private func configure() {
|
private func configure() {
|
||||||
contentView.addSubview(iconImageView)
|
|
||||||
iconImageView.snp.makeConstraints { make in
|
|
||||||
make.left.equalToSuperview().offset(12*width)
|
|
||||||
make.width.height.equalTo(30*width)
|
|
||||||
make.centerY.equalToSuperview()
|
|
||||||
}
|
|
||||||
contentView.addSubview(titleLabel)
|
contentView.addSubview(titleLabel)
|
||||||
titleLabel.snp.makeConstraints { make in
|
titleLabel.snp.makeConstraints { make in
|
||||||
make.centerY.equalToSuperview()
|
make.centerY.equalToSuperview()
|
||||||
make.left.equalTo(iconImageView.snp.right).offset(10*width)
|
make.left.equalToSuperview().offset(18*width)
|
||||||
make.right.equalToSuperview().offset(-12*width)
|
make.right.equalToSuperview().offset(-18*width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
//
|
|
||||||
// MPPositive_SearchSuggestionListTableViewCell.swift
|
|
||||||
// MusicPlayer
|
|
||||||
//
|
|
||||||
// Created by Mr.Zhou on 2024/5/12.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Kingfisher
|
|
||||||
class MPPositive_SearchSuggestionListTableViewCell: UITableViewCell {
|
|
||||||
//搜索Icon
|
|
||||||
private lazy var reviewImageView:UIImageView = {
|
|
||||||
let imageView = UIImageView()
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
return imageView
|
|
||||||
}()
|
|
||||||
private lazy var titleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .white, textAlignment: .left)
|
|
||||||
private lazy var subtitleLabel:UILabel = createLabel(font: .systemFont(ofSize: 12*width, weight: .regular), textColor: .init(hex: "#FFFFFF", alpha: 0.6), textAlignment: .left)
|
|
||||||
var item:MPPositive_SearchSuggestionItemModel!{
|
|
||||||
didSet{
|
|
||||||
reviewImageView.kf.setImage(with: URL(string: item.reviewUrls?.last ?? ""), placeholder: placeholderImage)
|
|
||||||
titleLabel.text = item.title ?? ""
|
|
||||||
subtitleLabel.text = item.subtitle ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
selectionStyle = .none
|
|
||||||
backgroundColor = .clear
|
|
||||||
configure()
|
|
||||||
}
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
}
|
|
||||||
override func awakeFromNib() {
|
|
||||||
super.awakeFromNib()
|
|
||||||
// Initialization code
|
|
||||||
}
|
|
||||||
|
|
||||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
|
||||||
super.setSelected(selected, animated: animated)
|
|
||||||
|
|
||||||
// Configure the view for the selected state
|
|
||||||
}
|
|
||||||
private func configure() {
|
|
||||||
contentView.addSubview(reviewImageView)
|
|
||||||
reviewImageView.snp.makeConstraints { make in
|
|
||||||
make.left.equalToSuperview().offset(12*width)
|
|
||||||
make.width.height.equalTo(30*width)
|
|
||||||
make.centerY.equalToSuperview()
|
|
||||||
}
|
|
||||||
contentView.addSubview(titleLabel)
|
|
||||||
titleLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(reviewImageView.snp.top)
|
|
||||||
make.left.equalTo(reviewImageView.snp.right).offset(10*width)
|
|
||||||
make.right.equalToSuperview().offset(-12*width)
|
|
||||||
}
|
|
||||||
contentView.addSubview(subtitleLabel)
|
|
||||||
subtitleLabel.snp.makeConstraints { make in
|
|
||||||
make.bottom.equalTo(reviewImageView.snp.bottom)
|
|
||||||
make.left.equalTo(titleLabel.snp.left)
|
|
||||||
make.right.equalTo(titleLabel.snp.right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// MPPositive_SearchSuggestionsView.swift
|
||||||
|
// MusicPlayer
|
||||||
|
//
|
||||||
|
// Created by Mr.Zhou on 2024/5/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
///展示搜索建议框
|
||||||
|
class MPPositive_SearchSuggestionsView: UIView {
|
||||||
|
//搜索建议tableView
|
||||||
|
private lazy var tableView:UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.rowHeight = 75*width
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.register(MPPositive_SearchSuggestionItemTableViewCell.self, forCellReuseIdentifier: MPPositive_SearchSuggestionItemTableViewCellID)
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
private let MPPositive_SearchSuggestionItemTableViewCellID = "MPPositive_SearchSuggestionItemTableViewCell"
|
||||||
|
//对用户展示的搜索建议组
|
||||||
|
var suggestions:[NSAttributedString]!{
|
||||||
|
didSet{
|
||||||
|
//刷新内容
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var selectedTextBlock:((String) -> Void)?
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
addSubview(tableView)
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.top.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - tableView
|
||||||
|
extension MPPositive_SearchSuggestionsView:UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return suggestions != nil ? suggestions.count:0
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: MPPositive_SearchSuggestionItemTableViewCellID, for: indexPath) as! MPPositive_SearchSuggestionItemTableViewCell
|
||||||
|
cell.item = suggestions[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
if selectedTextBlock != nil {
|
||||||
|
let text = suggestions[indexPath.row].string
|
||||||
|
selectedTextBlock!(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Podfile
3
Podfile
@ -18,4 +18,7 @@ pod 'SVProgressHUD'
|
|||||||
pod 'Alamofire'
|
pod 'Alamofire'
|
||||||
#网络图片实现
|
#网络图片实现
|
||||||
pod "Kingfisher"
|
pod "Kingfisher"
|
||||||
|
#分页工具
|
||||||
|
pod 'JXSegmentedView'
|
||||||
|
pod 'JXPagingView/Paging'
|
||||||
end
|
end
|
||||||
|
|||||||
10
Podfile.lock
10
Podfile.lock
@ -1,6 +1,8 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.9.1)
|
- Alamofire (5.9.1)
|
||||||
- IQKeyboardManagerSwift (6.5.13)
|
- IQKeyboardManagerSwift (6.5.13)
|
||||||
|
- JXPagingView/Paging (2.1.3)
|
||||||
|
- JXSegmentedView (1.3.3)
|
||||||
- Kingfisher (7.11.0)
|
- Kingfisher (7.11.0)
|
||||||
- SnapKit (5.6.0)
|
- SnapKit (5.6.0)
|
||||||
- SVProgressHUD (2.2.5)
|
- SVProgressHUD (2.2.5)
|
||||||
@ -9,6 +11,8 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JXPagingView/Paging
|
||||||
|
- JXSegmentedView
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- SnapKit
|
- SnapKit
|
||||||
- SVProgressHUD
|
- SVProgressHUD
|
||||||
@ -18,6 +22,8 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JXPagingView
|
||||||
|
- JXSegmentedView
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- SnapKit
|
- SnapKit
|
||||||
- SVProgressHUD
|
- SVProgressHUD
|
||||||
@ -26,11 +32,13 @@ SPEC REPOS:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
||||||
IQKeyboardManagerSwift: 69b5fb9960edff37263d45c44fd97427e1f8c22b
|
IQKeyboardManagerSwift: 69b5fb9960edff37263d45c44fd97427e1f8c22b
|
||||||
|
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||||
|
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
|
||||||
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
||||||
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
|
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
|
||||||
SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
|
SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
|
||||||
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
||||||
|
|
||||||
PODFILE CHECKSUM: d3eda313abd465652ca5c62aaab483d8f02ed2d2
|
PODFILE CHECKSUM: c0bbca53cdc7c53ea7a9dbc8d75b7ae964a1dbc2
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
21
Pods/JXPagingView/LICENSE
generated
Normal file
21
Pods/JXPagingView/LICENSE
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 暴走的鑫鑫
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
294
Pods/JXPagingView/README.md
generated
Normal file
294
Pods/JXPagingView/README.md
generated
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# JXPagingView
|
||||||
|
|
||||||
|
类似微博主页、简书主页、QQ联系人页面等效果。多页面嵌套,既可以上下滑动,也可以左右滑动切换页面。支持HeaderView悬浮、支持下拉刷新、上拉加载更多。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- 支持OC与Swift;
|
||||||
|
- 支持列表懒加载,等到列表真正显示的时候才加载,而不是一次性加载所有列表;
|
||||||
|
- 支持首页下拉刷新、列表视图下拉刷新、列表视图上拉加载更多;
|
||||||
|
- 支持悬浮SectionHeader的垂直位置调整;
|
||||||
|
- 支持从顶部用力往上滚动,下面的列表会跟着滚动,而不会突然卡主,需要使用`JXPagerSmoothView`类;
|
||||||
|
- 列表封装简洁,只要遵从`JXPagingViewListViewDelegate`协议即可。UIView、UIViewController等都可以;
|
||||||
|
- 使用JXCategoryView/JXSegmentedView分类控制器,几乎支持所有主流效果、高度自定义、可灵活扩展;
|
||||||
|
- 支持横竖屏切换;
|
||||||
|
- 支持点击状态栏滚动当前列表到顶部;
|
||||||
|
- 支持列表显示和消失的生命周期方法;
|
||||||
|
- isListHorizontalScrollEnabled属性控制列表是否可以左右滑动,默认YES;
|
||||||
|
- 支持`FDFullscreenPopGesture`等全屏手势兼容处理;
|
||||||
|
|
||||||
|
## 预览
|
||||||
|
|
||||||
|
| 效果 | 预览图 |
|
||||||
|
|-------|-------|
|
||||||
|
| **头图缩放** <br/>参考[ZoomViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Zoom/ZoomViewController.m)类 |  |
|
||||||
|
| **主页下拉刷新&列表上拉加载更多** <br/>参考[RefreshViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Refresh/RefreshViewController.m)类 |  |
|
||||||
|
| **列表下拉刷新** <br/>参考[ListRefreshViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Refresh/ListRefreshViewController.m)类 |  |
|
||||||
|
| **悬浮sectionHeader位置调整** |  |
|
||||||
|
| **导航栏隐藏** <br/> 参考[NaviBarHiddenViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/NavigationBarHidden/NaviBarHiddenViewController.m)类 |  |
|
||||||
|
| **CollectionView列表示例**<br/>参考[CollectionViewViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/CollectionView/CollectionViewViewController.swift)类 <br/> 只有swift的demo工程有该示例 |  |
|
||||||
|
| **HeaderView更新高度示例**<br/> 参考[HeightChangeAnimationViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/HeightChange/HeightChangeAnimationViewController.swift)类 <br/> 只有swift demo工程才有该示例 |  |
|
||||||
|
| **PagingView嵌套CategoryView** <br/> 参考[NestViewController](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagerViewExample-OC/JXPagerViewExample-OC/Example/Nest/NestViewController.m)类 <br/> 只有 **OC!OC!OC!** 的demo工程才有该示例 <br/> 操作比较特殊,如果需要此效果,<br/> 请认真参考源码,有问题多试试 <br/> 参考NestViewController.h类 |  |
|
||||||
|
| **CategoryView嵌套PagingView** <br/> 参考[NestViewController.swift](https://github.com/pujiaxin33/JXPagingView/tree/master/Examples/JXPagingViewExample/JXPagingViewExample/Example/CategoryNestPaging/NestViewController.swift)类 <br/> 只有 **Swift!Swift!Swift!** 的demo工程才有该示例 <br/> 操作比较特殊,如果需要此效果,<br/> 请认真参考源码,有问题多试试 <br/> 参考NestViewController.swift类 |  |
|
||||||
|
| **点击状态栏** |  |
|
||||||
|
| **横竖屏旋转** |  |
|
||||||
|
| **JXPageListView**<br/> 顶部需要自定义cell的场景,类似于电商APP首页,滑动到列表最底部才是分类控制器 <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) <br/> 该效果是另一个库,点击查看[JXPageListView](https://github.com/pujiaxin33/JXPageListView) |  |
|
||||||
|
| **JXPagerSmoothView**<br/> 类似淘宝、转转首页 <br/> 从顶部用力往上滚动,下面的列表会继续滚动 |  |
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 手动
|
||||||
|
|
||||||
|
**Swift版本:** Clone代码,拖入JXPagingView-Swift文件夹,使用`JXPagingView`类;
|
||||||
|
|
||||||
|
**OC版本:** Clone代码,拖入JXPagerView文件夹,使用`JXPagerView`类;
|
||||||
|
|
||||||
|
### CocoaPods
|
||||||
|
|
||||||
|
- **Swift版本**
|
||||||
|
|
||||||
|
支持swift版本:5.0+
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
target '<Your Target Name>' do
|
||||||
|
pod 'JXPagingView/Paging'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **OC版本**
|
||||||
|
```ruby
|
||||||
|
target '<Your Target Name>' do
|
||||||
|
pod 'JXPagingView/Pager'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Swift与OC的仓库地址不一样,请注意选择!
|
||||||
|
|
||||||
|
先`pod repo update`然后再`pod install`
|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
swift版本使用类似,只是类名及相关API更改为`JXPagingView`,具体细节请查看Swfit工程。
|
||||||
|
|
||||||
|
### 1、初始化`JXCategoryTitleView`和`JXPagerView`
|
||||||
|
|
||||||
|
```Objective-C
|
||||||
|
self.categoryView = [[JXCategoryTitleView alloc] initWithFrame:frame];
|
||||||
|
//配置categoryView,细节参考源码
|
||||||
|
|
||||||
|
self.pagerView = [[JXPagerView alloc] initWithDelegate:self];
|
||||||
|
[self.view addSubview:self.pagerView];
|
||||||
|
|
||||||
|
//⚠️⚠️⚠️将pagerView的listContainerView和categoryView.listContainer进行关联,这样列表就可以和categoryView联动了。⚠️⚠️⚠️
|
||||||
|
self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Swift版本列表关联代码**
|
||||||
|
```Swift
|
||||||
|
//给JXPagingListContainerView添加extension,表示遵从JXSegmentedViewListContainer的协议
|
||||||
|
extension JXPagingListContainerView: JXSegmentedViewListContainer {}
|
||||||
|
//⚠️⚠️⚠️将pagingView的listContainerView和segmentedView.listContainer进行关联,这样列表就可以和categoryView联动了。⚠️⚠️⚠️
|
||||||
|
segmentedView.listContainer = pagingView.listContainerView
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2、实现`JXPagerViewDelegate`协议
|
||||||
|
|
||||||
|
```Objective-C
|
||||||
|
/**
|
||||||
|
返回tableHeaderView的高度,因为内部需要比对判断,只能是整型数
|
||||||
|
*/
|
||||||
|
- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView {
|
||||||
|
return JXTableHeaderViewHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
返回tableHeaderView
|
||||||
|
*/
|
||||||
|
- (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView {
|
||||||
|
return self.userHeaderView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
返回悬浮HeaderView的高度,因为内部需要比对判断,只能是整型数
|
||||||
|
*/
|
||||||
|
- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
|
||||||
|
return JXheightForHeaderInSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
返回悬浮HeaderView
|
||||||
|
*/
|
||||||
|
- (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
|
||||||
|
return self.categoryView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
返回列表的数量
|
||||||
|
*/
|
||||||
|
- (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView {
|
||||||
|
//和categoryView的item数量一致
|
||||||
|
return self.titles.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
根据index初始化一个对应列表实例。注意:一定要是新生成的实例!!!
|
||||||
|
只要遵循JXPagerViewListViewDelegate即可,无论你返回的是UIView还是UIViewController都可以。
|
||||||
|
*/
|
||||||
|
- (id<JXPagerViewListViewDelegate>)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index {
|
||||||
|
TestListBaseView *listView = [[TestListBaseView alloc] init];
|
||||||
|
if (index == 0) {
|
||||||
|
listView.dataSource = @[@"橡胶火箭", @"橡胶火箭炮", @"橡胶机关枪"...].mutableCopy;
|
||||||
|
}else if (index == 1) {
|
||||||
|
listView.dataSource = @[@"吃烤肉", @"吃鸡腿肉", @"吃牛肉", @"各种肉"].mutableCopy;
|
||||||
|
}else {
|
||||||
|
listView.dataSource = @[@"【剑士】罗罗诺亚·索隆", @"【航海士】娜美", @"【狙击手】乌索普"...].mutableCopy;
|
||||||
|
}
|
||||||
|
[listView beginFirstRefresh];
|
||||||
|
return listView;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3、实现`JXPagerViewListViewDelegate`协议
|
||||||
|
|
||||||
|
列表可以是任意类,UIView、UIViewController等等都可以,只要实现了`JXPagerViewListViewDelegate`协议就行。
|
||||||
|
|
||||||
|
⚠️⚠️⚠️一定要保证`scrollCallback`的正确回调,许多朋友都容易疏忽这一点,导致异常,务必重点注意!
|
||||||
|
|
||||||
|
下面的使用代码参考的是`TestListBaseView`类
|
||||||
|
|
||||||
|
```Objective-C
|
||||||
|
/**
|
||||||
|
返回listView。如果是vc包裹的就是vc.view;如果是自定义view包裹的,就是自定义view自己。
|
||||||
|
*/
|
||||||
|
- (UIView *)listView {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
返回listView内部持有的UIScrollView或UITableView或UICollectionView
|
||||||
|
主要用于mainTableView已经显示了header,listView的contentOffset需要重置时,内部需要访问到外部传入进来的listView内的scrollView
|
||||||
|
*/
|
||||||
|
- (UIScrollView *)listScrollView {
|
||||||
|
return self.tableView;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时,需要调用该代理方法传入的callback
|
||||||
|
*/
|
||||||
|
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback {
|
||||||
|
self.scrollCallback = callback;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4、列表回调处理
|
||||||
|
|
||||||
|
`TestListBaseView`在其`tableView`的滚动回调中,通过调用上面持有的scrollCallback,把列表的滚动事件回调给JXPagerView内部。
|
||||||
|
```Objective-C
|
||||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
!self.scrollCallback ?: self.scrollCallback(scrollView);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
[实现原理](https://github.com/pujiaxin33/JXPagingView/blob/master/Document/JXPagingView%E5%8E%9F%E7%90%86.md)
|
||||||
|
|
||||||
|
## `JXPagerSmoothView`
|
||||||
|
|
||||||
|
如果你需要类似于**淘宝**、**转转**首页从顶部header用力往上滚动之后,下面的列表会跟着滚动的效果。因为`JXPagerView`的实现原理限制,当用户从顶部header的位置用力往上滚动,`JXPagerView`会在`JXCategoryView`刚好在顶部的时候突然停住。这个时候就需要使用`JXPagerSmoothView`,swift版本叫`JXPagingSmoothView`。
|
||||||
|
|
||||||
|
因为与`JXPagerView`的原理完全不同,所以各自会有一些特性的区别,但是从使用体验来说,是完全一致的。具体使用细节请参考demo示例。
|
||||||
|
|
||||||
|
实现原理参考[JXPagerSmoothView文章解析](https://juejin.im/post/5ddb2fe4f265da7def5424c7)
|
||||||
|
|
||||||
|
|
||||||
|
## 特殊说明
|
||||||
|
|
||||||
|
### JXCategoryView、JXSegmentedView
|
||||||
|
悬浮的HeaderView,用的是我写的:[OC版本-JXCategoryView](https://github.com/pujiaxin33/JXCategoryView) 、[Swift版本-JXSegmentedView](https://github.com/pujiaxin33/JXSegmentedView)。几乎实现了所有主流效果,而且非常容易自定义扩展,强烈推荐阅读。
|
||||||
|
|
||||||
|
|
||||||
|
### 头图缩放说明
|
||||||
|
头图缩放原理,参考这个库:[JXTableViewZoomHeaderImageView](https://github.com/pujiaxin33/JXTableViewZoomHeaderImageView)
|
||||||
|
|
||||||
|
### 列表下拉刷新说明
|
||||||
|
|
||||||
|
需要使用`JXPagerListRefreshView`类(是`JXPagerView`的子类)
|
||||||
|
|
||||||
|
### JXPagerListContainerType说明
|
||||||
|
|
||||||
|
UIScrollView:优势:没有其他副作用。劣势:实时的视图内存占用相对大一点,因为所有加载之后的列表视图都在视图层级里面。
|
||||||
|
UICollectionView:优势:因为列表被添加到cell上,实时的视图内存占用更少,适合内存要求特别高的场景。劣势:因为cell重用机制的问题,导致列表被移除屏幕外之后,会被放入缓存区,而不存在于视图层级中。如果刚好你的列表使用了下拉刷新视图,在快速切换过程中,就会导致下拉刷新回调不成功的问题。(使用MJRefresh会出现此问题)一句话概括:使用CollectionView的时候,就不要让列表使用下拉刷新加载。
|
||||||
|
|
||||||
|
### 关于下方列表视图的代理方法`- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath`有时候需要点击两次才回调
|
||||||
|
|
||||||
|
出现步骤:当手指放在下方列表视图往下拉,直到TableHeaderView完全显示。
|
||||||
|
|
||||||
|
原因:经过上面的步骤之后,手指已经离开屏幕且列表视图已经完全静止,UIScrollView的isDragging属性却依然是true。就导致了后续的第一次点击,让系统认为当前UIScrollView依然在滚动,该点击就让UIScrollView停止下来,没有继续转发给UITableView,就没有转化成didSelectRow事件。
|
||||||
|
|
||||||
|
解决方案:经过N种尝试之后,还是没有回避掉系统的`isDragging`异常为true的bug。大家可以在自定义cell最下方放置一个与cell同大小的button,把button的touchUpInside事件当做`didSelectRow`的回调。因为UIButton在响应链中的优先级要高于UIGestureRecognizer。
|
||||||
|
|
||||||
|
代码:请参考`TestTableViewCell`类的配置。
|
||||||
|
|
||||||
|
### 指定默认选中index
|
||||||
|
|
||||||
|
默认显示index=2的列表,代码如下:
|
||||||
|
```
|
||||||
|
self.pagerView.defaultSelectedIndex = 2;
|
||||||
|
self.categoryView.defaultSelectedIndex = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 顶部轮播图手势处理
|
||||||
|
|
||||||
|
如果TableHeaderView添加了轮播图,获取其他可以横向滚动的UIScrollView。如果不处理,就会出现左右滚动轮播图的时候又可以触发整个页面的上下滚动。为了规避该问题,请参考示例仓库中`BannerViewController`类的处理方法。即可同一时间只允许左右滚动或者上下滚动。
|
||||||
|
|
||||||
|
### 关于列表用UIViewController封装且要支持横竖屏的tips
|
||||||
|
|
||||||
|
在列表UIViewController类里面一定要加上下面这段代码:(不要问我为什么,我也不知道,谁知道系统内部是怎么操作的,反正加上就没毛病了)
|
||||||
|
```
|
||||||
|
- (void)loadView {
|
||||||
|
self.view = [[UIView alloc] init];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `JXPagerSmoothView` header有UITextField或者`UITextView`
|
||||||
|
详情参考OC版本示例【滚动延续 Header有输入框】
|
||||||
|
|
||||||
|
列表自定义子类化`UITableView`或者`UICollectionView`,然后重载`scrollRectToVisible`方法,示例代码如下。
|
||||||
|
```Object-C
|
||||||
|
@implementation TestTableView
|
||||||
|
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated {
|
||||||
|
[self setContentOffset:CGPointMake(self.contentOffset.x, rect.origin.y) animated:animated];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
```
|
||||||
|
|
||||||
|
### `FDFullscreenPopGesture`等全屏手势兼容处理
|
||||||
|
|
||||||
|
[全屏手势兼容处理文档,点击查看 ❗️❗️❗️](https://github.com/pujiaxin33/JXPagingView/blob/master/Document/%E5%85%A8%E5%B1%8F%E6%89%8B%E5%8A%BF%E5%A4%84%E7%90%86.md)
|
||||||
|
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
- **0.0.9版本**:将下面两个API的返回值修改为了NSUInteger(swift版本为Int),之前版本是CGFloat,升级为0.0.9及以上的时候,记得修改一下使用地方的返回值类型,不然会引起crash。
|
||||||
|
- `- (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView`
|
||||||
|
- `- (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView`
|
||||||
|
- **1.0.0版本**:
|
||||||
|
删除代理方法`- (NSArray <id<JXPagerViewListViewDelegate>> *)listViewsInPagerView:(JXPagerView *)pagerView;`,请参考示例使用下面两个代理方法:
|
||||||
|
- `- (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView;`
|
||||||
|
- `- (id<JXPagerViewListViewDelegate>)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index;`
|
||||||
|
- **2.0.0版本**:`JXPagerListContainerView`进行了重构,列表拥有了完整的生命周期方法。列表是`UIViewController`类,`viewWillAppear`等生命周期方法将会正确触发。
|
||||||
|
-
|
||||||
|
- 删除了collectionView,用`scrollView`属性替换。
|
||||||
|
- 和`CategoryView`的联动绑定代码更新为`self.categoryView.listContainer = (id<JXCategoryViewListContainer>)self.pagerView.listContainerView;`。
|
||||||
|
- `JXPagerView`新增`- (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type`初始化方法,可以指定列表容器为`UIScrollView`或者`UICollectionView`;
|
||||||
|
|
||||||
|
|
||||||
|
## 补充
|
||||||
|
|
||||||
|
有不明白的地方,建议多看下源码。再有疑问的,欢迎提Issue交流🤝
|
||||||
|
|
||||||
|
|
||||||
619
Pods/JXPagingView/Sources/JXPagingView/JXPagingListContainerView.swift
generated
Normal file
619
Pods/JXPagingView/Sources/JXPagingView/JXPagingListContainerView.swift
generated
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
//
|
||||||
|
// JXPagingListContainerView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 列表容器视图的类型
|
||||||
|
///- ScrollView: UIScrollView。优势:没有其他副作用。劣势:实时的视图内存占用相对大一点,因为所有加载之后的列表视图都在视图层级里面。
|
||||||
|
/// - CollectionView: 使用UICollectionView。优势:因为列表被添加到cell上,实时的视图内存占用更少,适合内存要求特别高的场景。劣势:因为cell重用机制的问题,导致列表被移除屏幕外之后,会被放入缓存区,而不存在于视图层级中。如果刚好你的列表使用了下拉刷新视图,在快速切换过程中,就会导致下拉刷新回调不成功的问题。一句话概括:使用CollectionView的时候,就不要让列表使用下拉刷新加载。
|
||||||
|
public enum JXPagingListContainerType {
|
||||||
|
case scrollView
|
||||||
|
case collectionView
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol JXPagingViewListViewDelegate: NSObjectProtocol {
|
||||||
|
/// 如果列表是VC,就返回VC.view
|
||||||
|
/// 如果列表是View,就返回View自己
|
||||||
|
///
|
||||||
|
/// - Returns: 返回列表视图
|
||||||
|
func listView() -> UIView
|
||||||
|
/// 返回listView内部持有的UIScrollView或UITableView或UICollectionView
|
||||||
|
/// 主要用于mainTableView已经显示了header,listView的contentOffset需要重置时,内部需要访问到外部传入进来的listView内的scrollView
|
||||||
|
///
|
||||||
|
/// - Returns: listView内部持有的UIScrollView或UITableView或UICollectionView
|
||||||
|
func listScrollView() -> UIScrollView
|
||||||
|
/// 当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时,需要调用该代理方法传入的callback
|
||||||
|
///
|
||||||
|
/// - Parameter callback: `scrollViewDidScroll`回调时调用的callback
|
||||||
|
func listViewDidScrollCallback(callback: @escaping (UIScrollView)->())
|
||||||
|
|
||||||
|
/// 将要重置listScrollView的contentOffset
|
||||||
|
func listScrollViewWillResetContentOffset()
|
||||||
|
/// 可选实现,列表将要显示的时候调用
|
||||||
|
func listWillAppear()
|
||||||
|
/// 可选实现,列表显示的时候调用
|
||||||
|
func listDidAppear()
|
||||||
|
/// 可选实现,列表将要消失的时候调用
|
||||||
|
func listWillDisappear()
|
||||||
|
/// 可选实现,列表消失的时候调用
|
||||||
|
func listDidDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension JXPagingViewListViewDelegate {
|
||||||
|
|
||||||
|
func listScrollViewWillResetContentOffset() {}
|
||||||
|
func listWillAppear() {}
|
||||||
|
func listDidAppear() {}
|
||||||
|
func listWillDisappear() {}
|
||||||
|
func listDidDisappear() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol JXPagingListContainerViewDataSource: NSObjectProtocol {
|
||||||
|
/// 返回list的数量
|
||||||
|
///
|
||||||
|
/// - Parameter listContainerView: JXPagingListContainerView
|
||||||
|
func numberOfLists(in listContainerView: JXPagingListContainerView) -> Int
|
||||||
|
|
||||||
|
/// 根据index初始化一个对应列表实例,需要是遵从`JXPagingViewListViewDelegate`协议的对象。
|
||||||
|
/// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXPagingViewListViewDelegate`协议,该方法返回自定义UIView即可。
|
||||||
|
/// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXPagingViewListViewDelegate`协议,该方法返回自定义UIViewController即可。
|
||||||
|
/// 注意:一定要是新生成的实例!!!
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - listContainerView: JXPagingListContainerView
|
||||||
|
/// - index: 目标index
|
||||||
|
/// - Returns: 遵从JXPagingViewListViewDelegate协议的实例
|
||||||
|
func listContainerView(_ listContainerView: JXPagingListContainerView, initListAt index: Int) -> JXPagingViewListViewDelegate
|
||||||
|
|
||||||
|
|
||||||
|
/// 控制能否初始化对应index的列表。有些业务需求,需要在某些情况才允许初始化某些列表,通过通过该代理实现控制。
|
||||||
|
func listContainerView(_ listContainerView: JXPagingListContainerView, canInitListAt index: Int) -> Bool
|
||||||
|
|
||||||
|
/// 返回自定义UIScrollView或UICollectionView的Class
|
||||||
|
/// 某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。
|
||||||
|
///
|
||||||
|
/// - Parameter listContainerView: JXPagingListContainerView
|
||||||
|
/// - Returns: 自定义UIScrollView实例
|
||||||
|
func scrollViewClass(in listContainerView: JXPagingListContainerView) -> AnyClass?
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension JXPagingListContainerViewDataSource {
|
||||||
|
func listContainerView(_ listContainerView: JXPagingListContainerView, canInitListAt index: Int) -> Bool { true }
|
||||||
|
func scrollViewClass(in listContainerView: JXPagingListContainerView) -> AnyClass? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol JXPagingListContainerViewDelegate: NSObjectProtocol {
|
||||||
|
func listContainerViewDidScroll(_ listContainerView: JXPagingListContainerView)
|
||||||
|
func listContainerViewWillBeginDragging(_ listContainerView: JXPagingListContainerView)
|
||||||
|
func listContainerViewDidEndScrolling(_ listContainerView: JXPagingListContainerView)
|
||||||
|
func listContainerView(_ listContainerView: JXPagingListContainerView, listDidAppearAt index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXPagingListContainerViewDelegate {
|
||||||
|
|
||||||
|
func listContainerViewDidScroll(_ listContainerView: JXPagingListContainerView) {}
|
||||||
|
func listContainerViewWillBeginDragging(_ listContainerView: JXPagingListContainerView) {}
|
||||||
|
func listContainerViewDidEndScrolling(_ listContainerView: JXPagingListContainerView) {}
|
||||||
|
func listContainerView(_ listContainerView: JXPagingListContainerView, listDidAppearAt index: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXPagingListContainerView: UIView {
|
||||||
|
public private(set) var type: JXPagingListContainerType
|
||||||
|
public private(set) weak var dataSource: JXPagingListContainerViewDataSource?
|
||||||
|
public private(set) var scrollView: UIScrollView!
|
||||||
|
public var isCategoryNestPagingEnabled = false {
|
||||||
|
didSet {
|
||||||
|
if let containerScrollView = scrollView as? JXPagingListContainerScrollView {
|
||||||
|
containerScrollView.isCategoryNestPagingEnabled = isCategoryNestPagingEnabled
|
||||||
|
}else if let containerScrollView = scrollView as? JXPagingListContainerCollectionView {
|
||||||
|
containerScrollView.isCategoryNestPagingEnabled = isCategoryNestPagingEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 已经加载过的列表字典。key是index,value是对应的列表
|
||||||
|
open var validListDict = [Int:JXPagingViewListViewDelegate]()
|
||||||
|
/// 滚动切换的时候,滚动距离超过一页的多少百分比,就触发列表的初始化。默认0.01(即列表显示了一点就触发加载)。范围0~1,开区间不包括0和1
|
||||||
|
open var initListPercent: CGFloat = 0.01 {
|
||||||
|
didSet {
|
||||||
|
if initListPercent <= 0 || initListPercent >= 1 {
|
||||||
|
assertionFailure("initListPercent值范围为开区间(0,1),即不包括0和1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public var listCellBackgroundColor: UIColor = .white
|
||||||
|
/// 需要和segmentedView.defaultSelectedIndex保持一致,用于触发默认index列表的加载
|
||||||
|
public var defaultSelectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
currentIndex = defaultSelectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weak var delegate: JXPagingListContainerViewDelegate?
|
||||||
|
public private(set) var currentIndex: Int = 0
|
||||||
|
private var collectionView: UICollectionView!
|
||||||
|
private var containerVC: JXPagingListContainerViewController!
|
||||||
|
private var willAppearIndex: Int = -1
|
||||||
|
private var willDisappearIndex: Int = -1
|
||||||
|
|
||||||
|
public init(dataSource: JXPagingListContainerViewDataSource, type: JXPagingListContainerType = .collectionView) {
|
||||||
|
self.dataSource = dataSource
|
||||||
|
self.type = type
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
open func commonInit() {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
containerVC = JXPagingListContainerViewController()
|
||||||
|
containerVC.view.backgroundColor = .clear
|
||||||
|
addSubview(containerVC.view)
|
||||||
|
containerVC.viewWillAppearClosure = {[weak self] in
|
||||||
|
self?.listWillAppear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewDidAppearClosure = {[weak self] in
|
||||||
|
self?.listDidAppear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewWillDisappearClosure = {[weak self] in
|
||||||
|
self?.listWillDisappear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewDidDisappearClosure = {[weak self] in
|
||||||
|
self?.listDidDisappear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
if type == .scrollView {
|
||||||
|
if let scrollViewClass = dataSource.scrollViewClass(in: self) as? UIScrollView.Type {
|
||||||
|
scrollView = scrollViewClass.init()
|
||||||
|
}else {
|
||||||
|
scrollView = JXPagingListContainerScrollView.init()
|
||||||
|
}
|
||||||
|
scrollView.backgroundColor = .clear
|
||||||
|
scrollView.delegate = self
|
||||||
|
scrollView.isPagingEnabled = true
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
scrollView.scrollsToTop = false
|
||||||
|
scrollView.bounces = false
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
containerVC.view.addSubview(scrollView)
|
||||||
|
}else if type == .collectionView {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
layout.minimumLineSpacing = 0
|
||||||
|
layout.minimumInteritemSpacing = 0
|
||||||
|
if let collectionViewClass = dataSource.scrollViewClass(in: self) as? UICollectionView.Type {
|
||||||
|
collectionView = collectionViewClass.init(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
}else {
|
||||||
|
collectionView = JXPagingListContainerCollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
}
|
||||||
|
collectionView.backgroundColor = .clear
|
||||||
|
collectionView.isPagingEnabled = true
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.showsVerticalScrollIndicator = false
|
||||||
|
collectionView.scrollsToTop = false
|
||||||
|
collectionView.bounces = false
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
collectionView.isPrefetchingEnabled = false
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
self.collectionView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
containerVC.view.addSubview(collectionView)
|
||||||
|
//让外部统一访问scrollView
|
||||||
|
scrollView = collectionView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func willMove(toSuperview newSuperview: UIView?) {
|
||||||
|
super.willMove(toSuperview: newSuperview)
|
||||||
|
var next: UIResponder? = newSuperview
|
||||||
|
while next != nil {
|
||||||
|
if let vc = next as? UIViewController{
|
||||||
|
vc.addChild(containerVC)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next = next?.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
containerVC.view.frame = bounds
|
||||||
|
if type == .scrollView {
|
||||||
|
if scrollView.frame == CGRect.zero || scrollView.bounds.size != bounds.size {
|
||||||
|
scrollView.frame = bounds
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height)
|
||||||
|
for (index, list) in validListDict {
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
}
|
||||||
|
scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex)*scrollView.bounds.size.width, y: 0)
|
||||||
|
}else {
|
||||||
|
scrollView.frame = bounds
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
if collectionView.frame == CGRect.zero || collectionView.bounds.size != bounds.size {
|
||||||
|
collectionView.frame = bounds
|
||||||
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
collectionView.reloadData()
|
||||||
|
collectionView.setContentOffset(CGPoint(x: CGFloat(currentIndex)*collectionView.bounds.size.width, y: 0), animated: false)
|
||||||
|
}else {
|
||||||
|
collectionView.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewListContainer
|
||||||
|
|
||||||
|
public func contentScrollView() -> UIScrollView {
|
||||||
|
return scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrolling(from leftIndex: Int, to rightIndex: Int, percent: CGFloat, selectedIndex: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func didClickSelectedItem(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
willAppearIndex = -1
|
||||||
|
willDisappearIndex = -1
|
||||||
|
if currentIndex != index {
|
||||||
|
listWillDisappear(at: currentIndex)
|
||||||
|
listWillAppear(at: index)
|
||||||
|
listDidDisappear(at: currentIndex)
|
||||||
|
listDidAppear(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reloadData() {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
if currentIndex < 0 || currentIndex >= dataSource.numberOfLists(in: self) {
|
||||||
|
defaultSelectedIndex = 0
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
validListDict.values.forEach { (list) in
|
||||||
|
if let listVC = list as? UIViewController {
|
||||||
|
listVC.removeFromParent()
|
||||||
|
}
|
||||||
|
list.listView().removeFromSuperview()
|
||||||
|
}
|
||||||
|
validListDict.removeAll()
|
||||||
|
if type == .scrollView {
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height)
|
||||||
|
}else {
|
||||||
|
collectionView.reloadData()
|
||||||
|
}
|
||||||
|
listWillAppear(at: currentIndex)
|
||||||
|
listDidAppear(at: currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
func initListIfNeeded(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
if dataSource.listContainerView(self, canInitListAt: index) == false {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var existedList = validListDict[index]
|
||||||
|
if existedList != nil {
|
||||||
|
//列表已经创建好了
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existedList = dataSource.listContainerView(self, initListAt: index)
|
||||||
|
guard let list = existedList else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
containerVC.addChild(vc)
|
||||||
|
}
|
||||||
|
validListDict[index] = list
|
||||||
|
switch type {
|
||||||
|
case .scrollView:
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
scrollView.addSubview(list.listView())
|
||||||
|
case .collectionView:
|
||||||
|
if let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) {
|
||||||
|
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
list.listView().frame = cell.contentView.bounds
|
||||||
|
cell.contentView.addSubview(list.listView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listWillAppear(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var existedList = validListDict[index]
|
||||||
|
if existedList != nil {
|
||||||
|
existedList?.listWillAppear()
|
||||||
|
if let vc = existedList as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//当前列表未被创建(页面初始化或通过点击触发的listWillAppear)
|
||||||
|
guard dataSource.listContainerView(self, canInitListAt: index) != false else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existedList = dataSource.listContainerView(self, initListAt: index)
|
||||||
|
guard let list = existedList else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
containerVC.addChild(vc)
|
||||||
|
}
|
||||||
|
validListDict[index] = list
|
||||||
|
if type == .scrollView {
|
||||||
|
if list.listView().superview == nil {
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
scrollView.addSubview(list.listView())
|
||||||
|
}
|
||||||
|
list.listWillAppear()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0))
|
||||||
|
cell?.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
list.listView().frame = cell?.contentView.bounds ?? CGRect.zero
|
||||||
|
cell?.contentView.addSubview(list.listView())
|
||||||
|
list.listWillAppear()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidAppear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentIndex = index
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listDidAppear()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.endAppearanceTransition()
|
||||||
|
}
|
||||||
|
delegate?.listContainerView(self, listDidAppearAt: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listWillDisappear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listWillDisappear()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(false, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidDisappear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listDidDisappear()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.endAppearanceTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkIndexValid(_ index: Int) -> Bool {
|
||||||
|
guard let dataSource = dataSource else { return false }
|
||||||
|
let count = dataSource.numberOfLists(in: self)
|
||||||
|
if count <= 0 || index >= count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidAppearOrDisappear(scrollView: UIScrollView) {
|
||||||
|
let currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width
|
||||||
|
if willAppearIndex != -1 || willDisappearIndex != -1 {
|
||||||
|
let disappearIndex = willDisappearIndex
|
||||||
|
let appearIndex = willAppearIndex
|
||||||
|
if willAppearIndex > willDisappearIndex {
|
||||||
|
//将要出现的列表在右边
|
||||||
|
if currentIndexPercent >= CGFloat(willAppearIndex) {
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
listDidDisappear(at: disappearIndex)
|
||||||
|
listDidAppear(at: appearIndex)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//将要出现的列表在左边
|
||||||
|
if currentIndexPercent <= CGFloat(willAppearIndex) {
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
listDidDisappear(at: disappearIndex)
|
||||||
|
listDidAppear(at: appearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXPagingListContainerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
guard let dataSource = dataSource else { return 0 }
|
||||||
|
return dataSource.numberOfLists(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
|
||||||
|
cell.contentView.backgroundColor = listCellBackgroundColor
|
||||||
|
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
let list = validListDict[indexPath.item]
|
||||||
|
if list != nil {
|
||||||
|
if list is UIViewController {
|
||||||
|
list?.listView().frame = cell.contentView.bounds
|
||||||
|
}else {
|
||||||
|
list?.listView().frame = cell.bounds
|
||||||
|
}
|
||||||
|
cell.contentView.addSubview(list!.listView())
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
delegate?.listContainerViewDidScroll(self)
|
||||||
|
guard scrollView.isTracking || scrollView.isDragging else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let percent = scrollView.contentOffset.x/scrollView.bounds.size.width
|
||||||
|
let maxCount = Int(round(scrollView.contentSize.width/scrollView.bounds.size.width))
|
||||||
|
var leftIndex = Int(floor(Double(percent)))
|
||||||
|
leftIndex = max(0, min(maxCount - 1, leftIndex))
|
||||||
|
let rightIndex = leftIndex + 1;
|
||||||
|
if percent < 0 || rightIndex >= maxCount {
|
||||||
|
listDidAppearOrDisappear(scrollView: scrollView)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let remainderRatio = percent - CGFloat(leftIndex)
|
||||||
|
if rightIndex == currentIndex {
|
||||||
|
//当前选中的在右边,用户正在从右边往左边滑动
|
||||||
|
if validListDict[leftIndex] == nil && remainderRatio < (1 - initListPercent) {
|
||||||
|
initListIfNeeded(at: leftIndex)
|
||||||
|
}else if validListDict[leftIndex] != nil {
|
||||||
|
if willAppearIndex == -1 {
|
||||||
|
willAppearIndex = leftIndex;
|
||||||
|
listWillAppear(at: willAppearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if willDisappearIndex == -1 {
|
||||||
|
willDisappearIndex = rightIndex
|
||||||
|
listWillDisappear(at: willDisappearIndex)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//当前选中的在左边,用户正在从左边往右边滑动
|
||||||
|
if validListDict[rightIndex] == nil && remainderRatio > initListPercent {
|
||||||
|
initListIfNeeded(at: rightIndex)
|
||||||
|
}else if validListDict[rightIndex] != nil {
|
||||||
|
if willAppearIndex == -1 {
|
||||||
|
willAppearIndex = rightIndex
|
||||||
|
listWillAppear(at: willAppearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if willDisappearIndex == -1 {
|
||||||
|
willDisappearIndex = leftIndex
|
||||||
|
listWillDisappear(at: willDisappearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listDidAppearOrDisappear(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
//滑动到一半又取消滑动处理
|
||||||
|
if willAppearIndex != -1 || willDisappearIndex != -1 {
|
||||||
|
listWillDisappear(at: willAppearIndex)
|
||||||
|
listWillAppear(at: willDisappearIndex)
|
||||||
|
listDidDisappear(at: willAppearIndex)
|
||||||
|
listDidAppear(at: willDisappearIndex)
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
}
|
||||||
|
delegate?.listContainerViewDidEndScrolling(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
delegate?.listContainerViewWillBeginDragging(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if !decelerate {
|
||||||
|
delegate?.listContainerViewDidEndScrolling(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
delegate?.listContainerViewDidEndScrolling(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JXPagingListContainerViewController: UIViewController {
|
||||||
|
var viewWillAppearClosure: (()->())?
|
||||||
|
var viewDidAppearClosure: (()->())?
|
||||||
|
var viewWillDisappearClosure: (()->())?
|
||||||
|
var viewDidDisappearClosure: (()->())?
|
||||||
|
override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false }
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
viewWillAppearClosure?()
|
||||||
|
}
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
viewDidAppearClosure?()
|
||||||
|
}
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
viewWillDisappearClosure?()
|
||||||
|
}
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
viewDidDisappearClosure?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JXPagingListContainerScrollView: UIScrollView, UIGestureRecognizerDelegate {
|
||||||
|
var isCategoryNestPagingEnabled = false
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if isCategoryNestPagingEnabled, let panGestureClass = NSClassFromString("UIScrollViewPanGestureRecognizer"), gestureRecognizer.isMember(of: panGestureClass) {
|
||||||
|
let panGesture = gestureRecognizer as! UIPanGestureRecognizer
|
||||||
|
let velocityX = panGesture.velocity(in: panGesture.view!).x
|
||||||
|
if velocityX > 0 {
|
||||||
|
//当前在第一个页面,且往左滑动,就放弃该手势响应,让外层接收,达到多个PagingView左右切换效果
|
||||||
|
if contentOffset.x == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}else if velocityX < 0 {
|
||||||
|
//当前在最后一个页面,且往右滑动,就放弃该手势响应,让外层接收,达到多个PagingView左右切换效果
|
||||||
|
if contentOffset.x + bounds.size.width == contentSize.width {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class JXPagingListContainerCollectionView: UICollectionView, UIGestureRecognizerDelegate {
|
||||||
|
var isCategoryNestPagingEnabled = false
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if isCategoryNestPagingEnabled, let panGestureClass = NSClassFromString("UIScrollViewPanGestureRecognizer"), gestureRecognizer.isMember(of: panGestureClass) {
|
||||||
|
let panGesture = gestureRecognizer as! UIPanGestureRecognizer
|
||||||
|
let velocityX = panGesture.velocity(in: panGesture.view!).x
|
||||||
|
if velocityX > 0 {
|
||||||
|
//当前在第一个页面,且往左滑动,就放弃该手势响应,让外层接收,达到多个PagingView左右切换效果
|
||||||
|
if contentOffset.x == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}else if velocityX < 0 {
|
||||||
|
//当前在最后一个页面,且往右滑动,就放弃该手势响应,让外层接收,达到多个PagingView左右切换效果
|
||||||
|
if contentOffset.x + bounds.size.width == contentSize.width {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Pods/JXPagingView/Sources/JXPagingView/JXPagingListRefreshView.swift
generated
Normal file
89
Pods/JXPagingView/Sources/JXPagingView/JXPagingListRefreshView.swift
generated
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// JXPagingListRefreshView.swift
|
||||||
|
// JXPagingView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/8/28.
|
||||||
|
// Copyright © 2018年 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXPagingListRefreshView: JXPagingView {
|
||||||
|
private var lastScrollingListViewContentOffsetY: CGFloat = 0
|
||||||
|
|
||||||
|
public override init(delegate: JXPagingViewDelegate, listContainerType: JXPagingListContainerType = .collectionView) {
|
||||||
|
super.init(delegate: delegate, listContainerType: listContainerType)
|
||||||
|
|
||||||
|
mainTableView.bounces = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func preferredProcessMainTableViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if pinSectionHeaderVerticalOffset != 0 {
|
||||||
|
if !(currentScrollingListView != nil && currentScrollingListView!.contentOffset.y > minContentOffsetYInListScrollView(currentScrollingListView!)) {
|
||||||
|
//没有处于滚动某一个listView的状态
|
||||||
|
if scrollView.contentOffset.y <= 0 {
|
||||||
|
mainTableView.bounces = false
|
||||||
|
mainTableView.contentOffset = CGPoint.zero
|
||||||
|
return
|
||||||
|
}else {
|
||||||
|
mainTableView.bounces = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let currentScrollingListView = currentScrollingListView else { return }
|
||||||
|
if (currentScrollingListView.contentOffset.y > minContentOffsetYInListScrollView(currentScrollingListView)) {
|
||||||
|
//mainTableView的header已经滚动不见,开始滚动某一个listView,那么固定mainTableView的contentOffset,让其不动
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainTableView.contentOffset.y < mainTableViewMaxContentOffsetY()) {
|
||||||
|
//mainTableView已经显示了header,listView的contentOffset需要重置
|
||||||
|
for list in validListDict.values {
|
||||||
|
//正在下拉刷新时,不需要重置
|
||||||
|
if list.listScrollView().contentOffset.y > minContentOffsetYInListScrollView(list.listScrollView()) {
|
||||||
|
setListScrollViewToMinContentOffsetY(list.listScrollView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollView.contentOffset.y > mainTableViewMaxContentOffsetY() && currentScrollingListView.contentOffset.y == minContentOffsetYInListScrollView(currentScrollingListView) {
|
||||||
|
//当往上滚动mainTableView的headerView时,滚动到底时,修复listView往上小幅度滚动
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func preferredProcessListViewDidScroll(scrollView: UIScrollView) {
|
||||||
|
guard let currentScrollingListView = currentScrollingListView else { return }
|
||||||
|
var shouldProcess = true
|
||||||
|
if currentScrollingListView.contentOffset.y > lastScrollingListViewContentOffsetY {
|
||||||
|
//往上滚动
|
||||||
|
}else {
|
||||||
|
//往下滚动
|
||||||
|
if mainTableView.contentOffset.y == 0 {
|
||||||
|
shouldProcess = false
|
||||||
|
}else {
|
||||||
|
if (mainTableView.contentOffset.y < mainTableViewMaxContentOffsetY()) {
|
||||||
|
//mainTableView的header还没有消失,让listScrollView一直为0
|
||||||
|
setListScrollViewToMinContentOffsetY(currentScrollingListView)
|
||||||
|
currentScrollingListView.showsVerticalScrollIndicator = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldProcess {
|
||||||
|
if (mainTableView.contentOffset.y < mainTableViewMaxContentOffsetY()) {
|
||||||
|
//处于下拉刷新的状态,scrollView.contentOffset.y为负数,就重置为0
|
||||||
|
if currentScrollingListView.contentOffset.y > minContentOffsetYInListScrollView(currentScrollingListView) {
|
||||||
|
//mainTableView的header还没有消失,让listScrollView一直为0
|
||||||
|
setListScrollViewToMinContentOffsetY(currentScrollingListView)
|
||||||
|
currentScrollingListView.showsVerticalScrollIndicator = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//mainTableView的header刚好消失,固定mainTableView的位置,显示listScrollView的滚动条
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
currentScrollingListView.showsVerticalScrollIndicator = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastScrollingListViewContentOffsetY = currentScrollingListView.contentOffset.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
Pods/JXPagingView/Sources/JXPagingView/JXPagingMainTableView.swift
generated
Normal file
26
Pods/JXPagingView/Sources/JXPagingView/JXPagingMainTableView.swift
generated
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// JXPagingViewMainTableView.swift
|
||||||
|
// JXPagingView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/5/22.
|
||||||
|
// Copyright © 2018年 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc public protocol JXPagingMainTableViewGestureDelegate {
|
||||||
|
//如果headerView(或其他地方)有水平滚动的scrollView,当其正在左右滑动的时候,就不能让列表上下滑动,所以有此代理方法进行对应处理
|
||||||
|
func mainTableViewGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXPagingMainTableView: UITableView, UIGestureRecognizerDelegate {
|
||||||
|
public weak var gestureDelegate: JXPagingMainTableViewGestureDelegate?
|
||||||
|
|
||||||
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureDelegate != nil {
|
||||||
|
return gestureDelegate!.mainTableViewGestureRecognizer(gestureRecognizer, shouldRecognizeSimultaneouslyWith:otherGestureRecognizer)
|
||||||
|
}else {
|
||||||
|
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
Pods/JXPagingView/Sources/JXPagingView/JXPagingSmoothView.swift
generated
Normal file
366
Pods/JXPagingView/Sources/JXPagingView/JXPagingSmoothView.swift
generated
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
//
|
||||||
|
// JXPagingSmoothView.swift
|
||||||
|
// JXPagingView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/11/20.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc public protocol JXPagingSmoothViewListViewDelegate {
|
||||||
|
/// 返回listView。如果是vc包裹的就是vc.view;如果是自定义view包裹的,就是自定义view自己。
|
||||||
|
func listView() -> UIView
|
||||||
|
/// 返回JXPagerSmoothViewListViewDelegate内部持有的UIScrollView或UITableView或UICollectionView
|
||||||
|
func listScrollView() -> UIScrollView
|
||||||
|
@objc optional func listDidAppear()
|
||||||
|
@objc optional func listDidDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public protocol JXPagingSmoothViewDataSource {
|
||||||
|
/// 返回页面header的高度
|
||||||
|
func heightForPagingHeader(in pagingView: JXPagingSmoothView) -> CGFloat
|
||||||
|
/// 返回页面header视图
|
||||||
|
func viewForPagingHeader(in pagingView: JXPagingSmoothView) -> UIView
|
||||||
|
/// 返回悬浮视图的高度
|
||||||
|
func heightForPinHeader(in pagingView: JXPagingSmoothView) -> CGFloat
|
||||||
|
/// 返回悬浮视图
|
||||||
|
func viewForPinHeader(in pagingView: JXPagingSmoothView) -> UIView
|
||||||
|
/// 返回列表的数量
|
||||||
|
func numberOfLists(in pagingView: JXPagingSmoothView) -> Int
|
||||||
|
/// 根据index初始化一个对应列表实例,需要是遵从`JXPagingSmoothViewListViewDelegate`协议的对象。
|
||||||
|
/// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXPagingSmoothViewListViewDelegate`协议,该方法返回自定义UIView即可。
|
||||||
|
/// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXPagingSmoothViewListViewDelegate`协议,该方法返回自定义UIViewController即可。
|
||||||
|
func pagingView(_ pagingView: JXPagingSmoothView, initListAtIndex index: Int) -> JXPagingSmoothViewListViewDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public protocol JXPagingSmoothViewDelegate {
|
||||||
|
@objc optional func pagingSmoothViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open class JXPagingSmoothView: UIView {
|
||||||
|
public private(set) var listDict = [Int : JXPagingSmoothViewListViewDelegate]()
|
||||||
|
public let listCollectionView: JXPagingSmoothCollectionView
|
||||||
|
public var defaultSelectedIndex: Int = 0
|
||||||
|
public weak var delegate: JXPagingSmoothViewDelegate?
|
||||||
|
|
||||||
|
weak var dataSource: JXPagingSmoothViewDataSource?
|
||||||
|
var listHeaderDict = [Int : UIView]()
|
||||||
|
var isSyncListContentOffsetEnabled: Bool = false
|
||||||
|
let pagingHeaderContainerView: UIView
|
||||||
|
var currentPagingHeaderContainerViewY: CGFloat = 0
|
||||||
|
var currentIndex: Int = 0
|
||||||
|
var currentListScrollView: UIScrollView?
|
||||||
|
var heightForPagingHeader: CGFloat = 0
|
||||||
|
var heightForPinHeader: CGFloat = 0
|
||||||
|
var heightForPagingHeaderContainerView: CGFloat = 0
|
||||||
|
let cellIdentifier = "cell"
|
||||||
|
var currentListInitializeContentOffsetY: CGFloat = 0
|
||||||
|
var singleScrollView: UIScrollView?
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
listDict.values.forEach {
|
||||||
|
$0.listScrollView().removeObserver(self, forKeyPath: "contentOffset")
|
||||||
|
$0.listScrollView().removeObserver(self, forKeyPath: "contentSize")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(dataSource: JXPagingSmoothViewDataSource) {
|
||||||
|
self.dataSource = dataSource
|
||||||
|
pagingHeaderContainerView = UIView()
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.minimumLineSpacing = 0
|
||||||
|
layout.minimumInteritemSpacing = 0
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
listCollectionView = JXPagingSmoothCollectionView(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
|
listCollectionView.dataSource = self
|
||||||
|
listCollectionView.delegate = self
|
||||||
|
listCollectionView.isPagingEnabled = true
|
||||||
|
listCollectionView.bounces = false
|
||||||
|
listCollectionView.showsHorizontalScrollIndicator = false
|
||||||
|
listCollectionView.scrollsToTop = false
|
||||||
|
listCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
listCollectionView.isPrefetchingEnabled = false
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
listCollectionView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
listCollectionView.pagingHeaderContainerView = pagingHeaderContainerView
|
||||||
|
addSubview(listCollectionView)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reloadData() {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
currentListScrollView = nil
|
||||||
|
currentIndex = defaultSelectedIndex
|
||||||
|
currentPagingHeaderContainerViewY = 0
|
||||||
|
isSyncListContentOffsetEnabled = false
|
||||||
|
|
||||||
|
listHeaderDict.removeAll()
|
||||||
|
listDict.values.forEach { (list) in
|
||||||
|
list.listScrollView().removeObserver(self, forKeyPath: "contentOffset")
|
||||||
|
list.listScrollView().removeObserver(self, forKeyPath: "contentSize")
|
||||||
|
list.listView().removeFromSuperview()
|
||||||
|
}
|
||||||
|
listDict.removeAll()
|
||||||
|
|
||||||
|
heightForPagingHeader = dataSource.heightForPagingHeader(in: self)
|
||||||
|
heightForPinHeader = dataSource.heightForPinHeader(in: self)
|
||||||
|
heightForPagingHeaderContainerView = heightForPagingHeader + heightForPinHeader
|
||||||
|
|
||||||
|
let pagingHeader = dataSource.viewForPagingHeader(in: self)
|
||||||
|
let pinHeader = dataSource.viewForPinHeader(in: self)
|
||||||
|
pagingHeaderContainerView.addSubview(pagingHeader)
|
||||||
|
pagingHeaderContainerView.addSubview(pinHeader)
|
||||||
|
|
||||||
|
pagingHeaderContainerView.frame = CGRect(x: 0, y: 0, width: bounds.size.width, height: heightForPagingHeaderContainerView)
|
||||||
|
pagingHeader.frame = CGRect(x: 0, y: 0, width: bounds.size.width, height: heightForPagingHeader)
|
||||||
|
pinHeader.frame = CGRect(x: 0, y: heightForPagingHeader, width: bounds.size.width, height: heightForPinHeader)
|
||||||
|
listCollectionView.setContentOffset(CGPoint(x: listCollectionView.bounds.size.width*CGFloat(defaultSelectedIndex), y: 0), animated: false)
|
||||||
|
listCollectionView.reloadData()
|
||||||
|
|
||||||
|
if dataSource.numberOfLists(in: self) == 0 {
|
||||||
|
singleScrollView = UIScrollView()
|
||||||
|
addSubview(singleScrollView!)
|
||||||
|
singleScrollView?.addSubview(pagingHeader)
|
||||||
|
singleScrollView?.contentSize = CGSize(width: bounds.size.width, height: heightForPagingHeader)
|
||||||
|
}else if singleScrollView != nil {
|
||||||
|
singleScrollView?.removeFromSuperview()
|
||||||
|
singleScrollView = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
listCollectionView.frame = bounds
|
||||||
|
if pagingHeaderContainerView.frame == CGRect.zero {
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
if singleScrollView != nil {
|
||||||
|
singleScrollView?.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDidScroll(scrollView: UIScrollView) {
|
||||||
|
if listCollectionView.isDragging || listCollectionView.isDecelerating {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let index = listIndex(for: scrollView)
|
||||||
|
if index != currentIndex {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentListScrollView = scrollView
|
||||||
|
let contentOffsetY = scrollView.contentOffset.y + heightForPagingHeaderContainerView
|
||||||
|
if contentOffsetY < heightForPagingHeader {
|
||||||
|
isSyncListContentOffsetEnabled = true
|
||||||
|
currentPagingHeaderContainerViewY = -contentOffsetY
|
||||||
|
for list in listDict.values {
|
||||||
|
if list.listScrollView() != currentListScrollView {
|
||||||
|
list.listScrollView().setContentOffset(scrollView.contentOffset, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let header = listHeader(for: scrollView)
|
||||||
|
if pagingHeaderContainerView.superview != header {
|
||||||
|
pagingHeaderContainerView.frame.origin.y = 0
|
||||||
|
header?.addSubview(pagingHeaderContainerView)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
if pagingHeaderContainerView.superview != self {
|
||||||
|
pagingHeaderContainerView.frame.origin.y = -heightForPagingHeader
|
||||||
|
addSubview(pagingHeaderContainerView)
|
||||||
|
}
|
||||||
|
if isSyncListContentOffsetEnabled {
|
||||||
|
isSyncListContentOffsetEnabled = false
|
||||||
|
currentPagingHeaderContainerViewY = -heightForPagingHeader
|
||||||
|
for list in listDict.values {
|
||||||
|
if list.listScrollView() != currentListScrollView {
|
||||||
|
list.listScrollView().setContentOffset(CGPoint(x: 0, y: -heightForPinHeader), animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - KVO
|
||||||
|
|
||||||
|
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "contentOffset" {
|
||||||
|
if let scrollView = object as? UIScrollView {
|
||||||
|
listDidScroll(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
}else if keyPath == "contentSize" {
|
||||||
|
if let scrollView = object as? UIScrollView {
|
||||||
|
let minContentSizeHeight = bounds.size.height - heightForPinHeader
|
||||||
|
if minContentSizeHeight > scrollView.contentSize.height {
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: minContentSizeHeight)
|
||||||
|
//新的scrollView第一次加载的时候重置contentOffset
|
||||||
|
if currentListScrollView != nil, scrollView != currentListScrollView! {
|
||||||
|
scrollView.contentOffset = CGPoint(x: 0, y: currentListInitializeContentOffsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
func listHeader(for listScrollView: UIScrollView) -> UIView? {
|
||||||
|
for (index, list) in listDict {
|
||||||
|
if list.listScrollView() == listScrollView {
|
||||||
|
return listHeaderDict[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listIndex(for listScrollView: UIScrollView) -> Int {
|
||||||
|
for (index, list) in listDict {
|
||||||
|
if list.listScrollView() == listScrollView {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDidAppear(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
let count = dataSource.numberOfLists(in: self)
|
||||||
|
if count <= 0 || index >= count {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listDict[index]?.listDidAppear?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func listDidDisappear(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
let count = dataSource.numberOfLists(in: self)
|
||||||
|
if count <= 0 || index >= count {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listDict[index]?.listDidDisappear?()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列表左右切换滚动结束之后,需要把pagerHeaderContainerView添加到当前index的列表上面
|
||||||
|
func horizontalScrollDidEnd(at index: Int) {
|
||||||
|
currentIndex = index
|
||||||
|
guard let listHeader = listHeaderDict[index], let listScrollView = listDict[index]?.listScrollView() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listDict.values.forEach { $0.listScrollView().scrollsToTop = ($0.listScrollView() === listScrollView) }
|
||||||
|
if listScrollView.contentOffset.y <= -heightForPinHeader {
|
||||||
|
pagingHeaderContainerView.frame.origin.y = 0
|
||||||
|
listHeader.addSubview(pagingHeaderContainerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXPagingSmoothView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
guard let dataSource = dataSource else { return 0 }
|
||||||
|
return dataSource.numberOfLists(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
guard let dataSource = dataSource else { return UICollectionViewCell(frame: CGRect.zero) }
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
|
||||||
|
var list = listDict[indexPath.item]
|
||||||
|
if list == nil {
|
||||||
|
list = dataSource.pagingView(self, initListAtIndex: indexPath.item)
|
||||||
|
listDict[indexPath.item] = list!
|
||||||
|
list?.listView().setNeedsLayout()
|
||||||
|
list?.listView().layoutIfNeeded()
|
||||||
|
if list?.listScrollView().isKind(of: UITableView.self) == true {
|
||||||
|
(list?.listScrollView() as? UITableView)?.estimatedRowHeight = 0
|
||||||
|
(list?.listScrollView() as? UITableView)?.estimatedSectionHeaderHeight = 0
|
||||||
|
(list?.listScrollView() as? UITableView)?.estimatedSectionFooterHeight = 0
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
list?.listScrollView().contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
list?.listScrollView().contentInset = UIEdgeInsets(top: heightForPagingHeaderContainerView, left: 0, bottom: 0, right: 0)
|
||||||
|
currentListInitializeContentOffsetY = -heightForPagingHeaderContainerView + min(-currentPagingHeaderContainerViewY, heightForPagingHeader)
|
||||||
|
list?.listScrollView().contentOffset = CGPoint(x: 0, y: currentListInitializeContentOffsetY)
|
||||||
|
let listHeader = UIView(frame: CGRect(x: 0, y: -heightForPagingHeaderContainerView, width: bounds.size.width, height: heightForPagingHeaderContainerView))
|
||||||
|
list?.listScrollView().addSubview(listHeader)
|
||||||
|
if pagingHeaderContainerView.superview == nil {
|
||||||
|
listHeader.addSubview(pagingHeaderContainerView)
|
||||||
|
}
|
||||||
|
listHeaderDict[indexPath.item] = listHeader
|
||||||
|
list?.listScrollView().addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
|
||||||
|
list?.listScrollView().addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
|
||||||
|
}
|
||||||
|
listDict.values.forEach { $0.listScrollView().scrollsToTop = ($0 === list) }
|
||||||
|
if let listView = list?.listView(), listView.superview != cell.contentView {
|
||||||
|
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
listView.frame = cell.contentView.bounds
|
||||||
|
cell.contentView.addSubview(listView)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
|
listDidAppear(at: indexPath.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
|
listDidDisappear(at: indexPath.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
delegate?.pagingSmoothViewDidScroll?(scrollView)
|
||||||
|
let indexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width
|
||||||
|
let index = Int(scrollView.contentOffset.x/scrollView.bounds.size.width)
|
||||||
|
let listScrollView = listDict[index]?.listScrollView()
|
||||||
|
if (indexPercent - CGFloat(index) == 0) && index != currentIndex && !(scrollView.isDragging || scrollView.isDecelerating) && listScrollView?.contentOffset.y ?? 0 <= -heightForPinHeader {
|
||||||
|
horizontalScrollDidEnd(at: index)
|
||||||
|
}else {
|
||||||
|
//左右滚动的时候,就把listHeaderContainerView添加到self,达到悬浮在顶部的效果
|
||||||
|
if pagingHeaderContainerView.superview != self {
|
||||||
|
pagingHeaderContainerView.frame.origin.y = currentPagingHeaderContainerViewY
|
||||||
|
addSubview(pagingHeaderContainerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index != currentIndex {
|
||||||
|
currentIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if !decelerate {
|
||||||
|
let index = Int(scrollView.contentOffset.x/scrollView.bounds.size.width)
|
||||||
|
horizontalScrollDidEnd(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
let index = Int(scrollView.contentOffset.x/scrollView.bounds.size.width)
|
||||||
|
horizontalScrollDidEnd(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JXPagingSmoothCollectionView: UICollectionView, UIGestureRecognizerDelegate {
|
||||||
|
var pagingHeaderContainerView: UIView?
|
||||||
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
let point = touch.location(in: pagingHeaderContainerView)
|
||||||
|
if pagingHeaderContainerView?.bounds.contains(point) == true {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
446
Pods/JXPagingView/Sources/JXPagingView/JXPagingView.swift
generated
Normal file
446
Pods/JXPagingView/Sources/JXPagingView/JXPagingView.swift
generated
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
//
|
||||||
|
// JXPagingView.swift
|
||||||
|
// JXPagingView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/5/22.
|
||||||
|
// Copyright © 2018年 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol JXPagingViewDelegate: NSObjectProtocol {
|
||||||
|
/// tableHeaderView的高度,因为内部需要比对判断,只能是整型数
|
||||||
|
func tableHeaderViewHeight(in pagingView: JXPagingView) -> Int
|
||||||
|
/// 返回tableHeaderView
|
||||||
|
func tableHeaderView(in pagingView: JXPagingView) -> UIView
|
||||||
|
/// 返回悬浮HeaderView的高度,因为内部需要比对判断,只能是整型数
|
||||||
|
func heightForPinSectionHeader(in pagingView: JXPagingView) -> Int
|
||||||
|
/// 返回悬浮HeaderView
|
||||||
|
func viewForPinSectionHeader(in pagingView: JXPagingView) -> UIView
|
||||||
|
/// 返回列表的数量
|
||||||
|
func numberOfLists(in pagingView: JXPagingView) -> Int
|
||||||
|
/// 根据index初始化一个对应列表实例,需要是遵从`JXPagerViewListViewDelegate`协议的对象。
|
||||||
|
/// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXPagerViewListViewDelegate`协议,该方法返回自定义UIView即可。
|
||||||
|
/// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXPagerViewListViewDelegate`协议,该方法返回自定义UIViewController即可。
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pagingView: pagingView description
|
||||||
|
/// - index: 新生成的列表实例
|
||||||
|
func pagingView(_ pagingView: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate
|
||||||
|
|
||||||
|
|
||||||
|
/// 返回对应index的列表唯一标识
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pagingView: pagingView description
|
||||||
|
/// - index: 列表的下标
|
||||||
|
func pagingView(_ pagingView: JXPagingView, listIdentifierAtIndex index: Int) -> String?
|
||||||
|
|
||||||
|
/// 将要被弃用!请使用pagingView(_ pagingView: JXPagingView, mainTableViewDidScroll scrollView: UIScrollView) 方法作为替代。
|
||||||
|
@available(*, message: "Use pagingView(_ pagingView: JXPagingView, mainTableViewDidScroll scrollView: UIScrollView) method")
|
||||||
|
func mainTableViewDidScroll(_ scrollView: UIScrollView)
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidScroll scrollView: UIScrollView)
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewWillBeginDragging scrollView: UIScrollView)
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndDragging scrollView: UIScrollView, willDecelerate decelerate: Bool)
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndDecelerating scrollView: UIScrollView)
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndScrollingAnimation scrollView: UIScrollView)
|
||||||
|
|
||||||
|
|
||||||
|
/// 返回自定义UIScrollView或UICollectionView的Class
|
||||||
|
/// 某些特殊情况需要自己处理列表容器内UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。
|
||||||
|
///
|
||||||
|
/// - Parameter pagingView: JXPagingView
|
||||||
|
/// - Returns: 自定义UIScrollView实例
|
||||||
|
func scrollViewClassInListContainerView(in pagingView: JXPagingView) -> AnyClass?
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension JXPagingViewDelegate {
|
||||||
|
func pagingView(_ pagingView: JXPagingView, listIdentifierAtIndex index: Int) -> String? { nil }
|
||||||
|
|
||||||
|
func mainTableViewDidScroll(_ scrollView: UIScrollView) {}
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidScroll scrollView: UIScrollView) {}
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewWillBeginDragging scrollView: UIScrollView) {}
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndDragging scrollView: UIScrollView, willDecelerate decelerate: Bool) {}
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndDecelerating scrollView: UIScrollView) {}
|
||||||
|
func pagingView(_ pagingView: JXPagingView, mainTableViewDidEndScrollingAnimation scrollView: UIScrollView) {}
|
||||||
|
|
||||||
|
|
||||||
|
/// 返回自定义UIScrollView或UICollectionView的Class
|
||||||
|
/// 某些特殊情况需要自己处理列表容器内UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。
|
||||||
|
///
|
||||||
|
/// - Parameter pagingView: JXPagingView
|
||||||
|
/// - Returns: 自定义UIScrollView实例
|
||||||
|
func scrollViewClassInListContainerView(in pagingView: JXPagingView) -> AnyClass? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXPagingView: UIView {
|
||||||
|
/// 需要和categoryView.defaultSelectedIndex保持一致
|
||||||
|
public var defaultSelectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
listContainerView.defaultSelectedIndex = defaultSelectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public private(set) lazy var mainTableView: JXPagingMainTableView = JXPagingMainTableView(frame: CGRect.zero, style: .plain)
|
||||||
|
public private(set) lazy var listContainerView: JXPagingListContainerView = JXPagingListContainerView(dataSource: self, type: listContainerType)
|
||||||
|
/// 当前已经加载过可用的列表字典,key就是index值,value是对应的列表。
|
||||||
|
public private(set) var validListDict = [Int:JXPagingViewListViewDelegate]()
|
||||||
|
/// 顶部固定sectionHeader的垂直偏移量。数值越大越往下沉。
|
||||||
|
public var pinSectionHeaderVerticalOffset: Int = 0
|
||||||
|
public var isListHorizontalScrollEnabled = true {
|
||||||
|
didSet {
|
||||||
|
listContainerView.scrollView.isScrollEnabled = isListHorizontalScrollEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 是否允许当前列表自动显示或隐藏列表是垂直滚动指示器。true:悬浮的headerView滚动到顶部开始滚动列表时,就会显示,反之隐藏。false:内部不会处理列表的垂直滚动指示器。默认为:true。
|
||||||
|
public var automaticallyDisplayListVerticalScrollIndicator = true
|
||||||
|
/// 当allowsCacheList为true时,请务必实现代理方法`func pagingView(_ pagingView: JXPagingView, listIdentifierAtIndex index: Int) -> String`
|
||||||
|
public var allowsCacheList: Bool = false
|
||||||
|
public private(set) var currentScrollingListView: UIScrollView?
|
||||||
|
internal var currentList: JXPagingViewListViewDelegate?
|
||||||
|
private var currentIndex: Int = 0
|
||||||
|
private weak var delegate: JXPagingViewDelegate?
|
||||||
|
private var tableHeaderContainerView: UIView!
|
||||||
|
private let cellIdentifier = "cell"
|
||||||
|
private let listContainerType: JXPagingListContainerType
|
||||||
|
private var listCache = [String:JXPagingViewListViewDelegate]()
|
||||||
|
|
||||||
|
public init(delegate: JXPagingViewDelegate, listContainerType: JXPagingListContainerType = .collectionView) {
|
||||||
|
self.delegate = delegate
|
||||||
|
self.listContainerType = listContainerType
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
|
listContainerView.delegate = self
|
||||||
|
|
||||||
|
mainTableView.showsVerticalScrollIndicator = false
|
||||||
|
mainTableView.showsHorizontalScrollIndicator = false
|
||||||
|
mainTableView.separatorStyle = .none
|
||||||
|
mainTableView.dataSource = self
|
||||||
|
mainTableView.delegate = self
|
||||||
|
mainTableView.scrollsToTop = false
|
||||||
|
refreshTableHeaderView()
|
||||||
|
mainTableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
mainTableView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
mainTableView.sectionHeaderTopPadding = 0
|
||||||
|
}
|
||||||
|
addSubview(mainTableView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if mainTableView.frame != bounds {
|
||||||
|
mainTableView.frame = bounds
|
||||||
|
mainTableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadData() {
|
||||||
|
currentList = nil
|
||||||
|
currentScrollingListView = nil
|
||||||
|
validListDict.removeAll()
|
||||||
|
if allowsCacheList, let listCount = delegate?.numberOfLists(in: self) {
|
||||||
|
//根据新数据删除不需要的list
|
||||||
|
var newListIdentifierArray = [String]()
|
||||||
|
for index in 0..<listCount {
|
||||||
|
if let listIdentifier = delegate?.pagingView(self, listIdentifierAtIndex: index) {
|
||||||
|
newListIdentifierArray.append(listIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let existedKeys = Array(listCache.keys)
|
||||||
|
for listIdentifier in existedKeys {
|
||||||
|
if !newListIdentifierArray.contains(listIdentifier) {
|
||||||
|
listCache.removeValue(forKey: listIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshTableHeaderView()
|
||||||
|
if pinSectionHeaderVerticalOffset != 0 && mainTableView.contentOffset.y > CGFloat(pinSectionHeaderVerticalOffset) {
|
||||||
|
mainTableView.contentOffset = .zero
|
||||||
|
}
|
||||||
|
mainTableView.reloadData()
|
||||||
|
listContainerView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func resizeTableHeaderViewHeight(animatable: Bool = false, duration: TimeInterval = 0.25, curve: UIView.AnimationCurve = .linear) {
|
||||||
|
guard let delegate = delegate else { return }
|
||||||
|
if animatable {
|
||||||
|
var options: UIView.AnimationOptions = .curveLinear
|
||||||
|
switch curve {
|
||||||
|
case .easeIn: options = .curveEaseIn
|
||||||
|
case .easeOut: options = .curveEaseOut
|
||||||
|
case .easeInOut: options = .curveEaseInOut
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
var bounds = tableHeaderContainerView.bounds
|
||||||
|
bounds.size.height = CGFloat(delegate.tableHeaderViewHeight(in: self))
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
|
||||||
|
self.tableHeaderContainerView.frame = bounds
|
||||||
|
self.mainTableView.tableHeaderView = self.tableHeaderContainerView
|
||||||
|
self.mainTableView.setNeedsLayout()
|
||||||
|
self.mainTableView.layoutIfNeeded()
|
||||||
|
}, completion: nil)
|
||||||
|
}else {
|
||||||
|
var bounds = tableHeaderContainerView.bounds
|
||||||
|
bounds.size.height = CGFloat(delegate.tableHeaderViewHeight(in: self))
|
||||||
|
tableHeaderContainerView.frame = bounds
|
||||||
|
mainTableView.tableHeaderView = tableHeaderContainerView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredProcessListViewDidScroll(scrollView: UIScrollView) {
|
||||||
|
if (mainTableView.contentOffset.y < mainTableViewMaxContentOffsetY()) {
|
||||||
|
//mainTableView的header还没有消失,让listScrollView一直为0
|
||||||
|
currentList?.listScrollViewWillResetContentOffset()
|
||||||
|
setListScrollViewToMinContentOffsetY(scrollView)
|
||||||
|
if automaticallyDisplayListVerticalScrollIndicator {
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//mainTableView的header刚好消失,固定mainTableView的位置,显示listScrollView的滚动条
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
if automaticallyDisplayListVerticalScrollIndicator {
|
||||||
|
scrollView.showsVerticalScrollIndicator = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredProcessMainTableViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard let currentScrollingListView = currentScrollingListView else { return }
|
||||||
|
if (currentScrollingListView.contentOffset.y > minContentOffsetYInListScrollView(currentScrollingListView)) {
|
||||||
|
//mainTableView的header已经滚动不见,开始滚动某一个listView,那么固定mainTableView的contentOffset,让其不动
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainTableView.contentOffset.y < mainTableViewMaxContentOffsetY()) {
|
||||||
|
//mainTableView已经显示了header,listView的contentOffset需要重置
|
||||||
|
for list in validListDict.values {
|
||||||
|
list.listScrollViewWillResetContentOffset()
|
||||||
|
setListScrollViewToMinContentOffsetY(list.listScrollView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollView.contentOffset.y > mainTableViewMaxContentOffsetY() && currentScrollingListView.contentOffset.y == minContentOffsetYInListScrollView(currentScrollingListView) {
|
||||||
|
//当往上滚动mainTableView的headerView时,滚动到底时,修复listView往上小幅度滚动
|
||||||
|
setMainTableViewToMaxContentOffsetY()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
|
||||||
|
func refreshTableHeaderView() {
|
||||||
|
guard let delegate = delegate else { return }
|
||||||
|
let tableHeaderView = delegate.tableHeaderView(in: self)
|
||||||
|
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat(delegate.tableHeaderViewHeight(in: self))))
|
||||||
|
containerView.addSubview(tableHeaderView)
|
||||||
|
tableHeaderView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
let top = NSLayoutConstraint(item: tableHeaderView, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1, constant: 0)
|
||||||
|
let leading = NSLayoutConstraint(item: tableHeaderView, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .leading, multiplier: 1, constant: 0)
|
||||||
|
let bottom = NSLayoutConstraint(item: tableHeaderView, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: 0)
|
||||||
|
let trailing = NSLayoutConstraint(item: tableHeaderView, attribute: .trailing, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: 1, constant: 0)
|
||||||
|
containerView.addConstraints([top, leading, bottom, trailing])
|
||||||
|
tableHeaderContainerView = containerView
|
||||||
|
mainTableView.tableHeaderView = containerView
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjustMainScrollViewToTargetContentInsetIfNeeded(inset: UIEdgeInsets) {
|
||||||
|
if mainTableView.contentInset != inset {
|
||||||
|
//防止循环调用
|
||||||
|
mainTableView.delegate = nil
|
||||||
|
mainTableView.contentInset = inset
|
||||||
|
mainTableView.delegate = self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//仅用于处理设置了pinSectionHeaderVerticalOffset,又添加了MJRefresh的下拉刷新。这种情况会导致JXPagingView和MJRefresh来回设置contentInset值。针对这种及其特殊的情况,就内部特殊处理了。通过下面的判断条件,来判定当前是否处于下拉刷新中。请勿让pinSectionHeaderVerticalOffset和下拉刷新设置的contentInset.top值相同。
|
||||||
|
//具体原因参考:https://github.com/pujiaxin33/JXPagingView/issues/203
|
||||||
|
func isSetMainScrollViewContentInsetToZeroEnabled(scrollView: UIScrollView) -> Bool {
|
||||||
|
return !(scrollView.contentInset.top != 0 && scrollView.contentInset.top != CGFloat(pinSectionHeaderVerticalOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainTableViewMaxContentOffsetY() -> CGFloat {
|
||||||
|
guard let delegate = delegate else { return 0 }
|
||||||
|
return CGFloat(delegate.tableHeaderViewHeight(in: self)) - CGFloat(pinSectionHeaderVerticalOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMainTableViewToMaxContentOffsetY() {
|
||||||
|
mainTableView.contentOffset = CGPoint(x: 0, y: mainTableViewMaxContentOffsetY())
|
||||||
|
}
|
||||||
|
|
||||||
|
func minContentOffsetYInListScrollView(_ scrollView: UIScrollView) -> CGFloat {
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
return -scrollView.adjustedContentInset.top
|
||||||
|
}
|
||||||
|
return -scrollView.contentInset.top
|
||||||
|
}
|
||||||
|
|
||||||
|
func setListScrollViewToMinContentOffsetY(_ scrollView: UIScrollView) {
|
||||||
|
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: minContentOffsetYInListScrollView(scrollView))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pinSectionHeaderHeight() -> CGFloat {
|
||||||
|
guard let delegate = delegate else { return 0 }
|
||||||
|
return CGFloat(delegate.heightForPinSectionHeader(in: self))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 外部传入的listView,当其内部的scrollView滚动时,需要调用该方法
|
||||||
|
func listViewDidScroll(scrollView: UIScrollView) {
|
||||||
|
currentScrollingListView = scrollView
|
||||||
|
preferredProcessListViewDidScroll(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - UITableViewDataSource, UITableViewDelegate
|
||||||
|
extension JXPagingView: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return max(bounds.height - pinSectionHeaderHeight() - CGFloat(pinSectionHeaderVerticalOffset), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
cell.backgroundColor = UIColor.clear
|
||||||
|
if listContainerView.superview != cell.contentView {
|
||||||
|
cell.contentView.addSubview(listContainerView)
|
||||||
|
}
|
||||||
|
if listContainerView.frame != cell.bounds {
|
||||||
|
listContainerView.frame = cell.bounds
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||||
|
return pinSectionHeaderHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||||
|
guard let delegate = delegate else { return nil }
|
||||||
|
return delegate.viewForPinSectionHeader(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
//加上footer之后,下滑滚动就变得丝般顺滑了
|
||||||
|
open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||||
|
let footerView = UIView(frame: CGRect.zero)
|
||||||
|
footerView.backgroundColor = UIColor.clear
|
||||||
|
return footerView
|
||||||
|
}
|
||||||
|
|
||||||
|
open func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if pinSectionHeaderVerticalOffset != 0 {
|
||||||
|
if !(currentScrollingListView != nil && currentScrollingListView!.contentOffset.y > minContentOffsetYInListScrollView(currentScrollingListView!)) {
|
||||||
|
//没有处于滚动某一个listView的状态
|
||||||
|
if scrollView.contentOffset.y >= CGFloat(pinSectionHeaderVerticalOffset) {
|
||||||
|
//固定的位置就是contentInset.top
|
||||||
|
adjustMainScrollViewToTargetContentInsetIfNeeded(inset: UIEdgeInsets(top: CGFloat(pinSectionHeaderVerticalOffset), left: 0, bottom: 0, right: 0))
|
||||||
|
}else {
|
||||||
|
if isSetMainScrollViewContentInsetToZeroEnabled(scrollView: scrollView) {
|
||||||
|
adjustMainScrollViewToTargetContentInsetIfNeeded(inset: UIEdgeInsets.zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferredProcessMainTableViewDidScroll(scrollView)
|
||||||
|
delegate?.mainTableViewDidScroll(scrollView)
|
||||||
|
delegate?.pagingView(self, mainTableViewDidScroll: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
//用户正在上下滚动的时候,就不允许左右滚动
|
||||||
|
listContainerView.scrollView.isScrollEnabled = false
|
||||||
|
delegate?.pagingView(self, mainTableViewWillBeginDragging: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if isListHorizontalScrollEnabled && !decelerate {
|
||||||
|
listContainerView.scrollView.isScrollEnabled = true
|
||||||
|
}
|
||||||
|
delegate?.pagingView(self, mainTableViewDidEndDragging: scrollView, willDecelerate: decelerate)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
if isListHorizontalScrollEnabled {
|
||||||
|
listContainerView.scrollView.isScrollEnabled = true
|
||||||
|
}
|
||||||
|
if isSetMainScrollViewContentInsetToZeroEnabled(scrollView: scrollView) {
|
||||||
|
if mainTableView.contentInset.top != 0 && pinSectionHeaderVerticalOffset != 0 {
|
||||||
|
adjustMainScrollViewToTargetContentInsetIfNeeded(inset: UIEdgeInsets.zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate?.pagingView(self, mainTableViewDidEndDecelerating: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
if isListHorizontalScrollEnabled {
|
||||||
|
listContainerView.scrollView.isScrollEnabled = true
|
||||||
|
}
|
||||||
|
delegate?.pagingView(self, mainTableViewDidEndScrollingAnimation: scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXPagingView: JXPagingListContainerViewDataSource {
|
||||||
|
public func numberOfLists(in listContainerView: JXPagingListContainerView) -> Int {
|
||||||
|
guard let delegate = delegate else { return 0 }
|
||||||
|
return delegate.numberOfLists(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listContainerView(_ listContainerView: JXPagingListContainerView, initListAt index: Int) -> JXPagingViewListViewDelegate {
|
||||||
|
guard let delegate = delegate else { fatalError("JXPaingView.delegate must not be nil") }
|
||||||
|
var list = validListDict[index]
|
||||||
|
if allowsCacheList, list == nil, let listIdentifier = delegate.pagingView(self, listIdentifierAtIndex: index) {
|
||||||
|
list = listCache[listIdentifier]
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = delegate.pagingView(self, initListAtIndex: index)
|
||||||
|
list?.listViewDidScrollCallback {[weak self, weak list] (scrollView) in
|
||||||
|
self?.currentList = list
|
||||||
|
self?.listViewDidScroll(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
validListDict[index] = list!
|
||||||
|
if allowsCacheList, let listIdentifier = delegate.pagingView(self, listIdentifierAtIndex: index) {
|
||||||
|
listCache[listIdentifier] = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list!
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewClass(in listContainerView: JXPagingListContainerView) -> AnyClass? {
|
||||||
|
return delegate?.scrollViewClassInListContainerView(in: self) ?? UIView.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXPagingView: JXPagingListContainerViewDelegate {
|
||||||
|
public func listContainerViewWillBeginDragging(_ listContainerView: JXPagingListContainerView) {
|
||||||
|
mainTableView.isScrollEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listContainerViewDidEndScrolling(_ listContainerView: JXPagingListContainerView) {
|
||||||
|
mainTableView.isScrollEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func listContainerView(_ listContainerView: JXPagingListContainerView, listDidAppearAt index: Int) {
|
||||||
|
currentScrollingListView = validListDict[index]?.listScrollView()
|
||||||
|
for listItem in validListDict.values {
|
||||||
|
if listItem === validListDict[index] {
|
||||||
|
listItem.listScrollView().scrollsToTop = true
|
||||||
|
}else {
|
||||||
|
listItem.listScrollView().scrollsToTop = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
14
Pods/JXPagingView/Sources/PrivacyInfo.xcprivacy
generated
Normal file
14
Pods/JXPagingView/Sources/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
21
Pods/JXSegmentedView/LICENSE
generated
Normal file
21
Pods/JXSegmentedView/LICENSE
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 暴走的鑫鑫
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
269
Pods/JXSegmentedView/README-English.md
generated
Normal file
269
Pods/JXSegmentedView/README-English.md
generated
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<div align=center><img src="Example/JXSegmentedViewExample/Image/JXSegmentedViewSmall.png" width="467" height="84" /></div>
|
||||||
|
|
||||||
|
[](#)
|
||||||
|
[](#)
|
||||||
|
[](https://cocoapods.org/pods/JXSegmentedView)
|
||||||
|
[](#)
|
||||||
|
|
||||||
|
A powerful and easy to use segmented view (segmentedcontrol, pagingview, pagerview, pagecontrol, categoryview)
|
||||||
|
|
||||||
|
Advantages compared to other similar tripartite libraries:
|
||||||
|
- Indicator logic use Protocol Oriented Programming, which can be easily to extension;
|
||||||
|
- Provide more comprehensive and rich effects, and support almost all popular APP effects;
|
||||||
|
- Use subclassing to manage cell styles, with clearer logic and simpler extensions;
|
||||||
|
|
||||||
|
## Objective-C Version
|
||||||
|
|
||||||
|
If you are looking for the Objective-C version, please click to view
|
||||||
|
[JXCategoryView](https://github.com/pujiaxin33/JXCategoryView)
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
### Indicator Preview
|
||||||
|
|
||||||
|
Description | Gif |
|
||||||
|
----|------|
|
||||||
|
Line fixed width | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineFixedWidth.gif" width="350" height="80"> |
|
||||||
|
Line flexible width | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineFlexibleWidth.gif" width="350" height="80"> |
|
||||||
|
Line lengthen | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineLengthen.gif" width="350" height="80"> |
|
||||||
|
Line lengthen and offset | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineLengthenOffset.gif" width="350" height="80"> |
|
||||||
|
RainbowLine | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineRainbow.gif" width="350" height="80"> |
|
||||||
|
DotLine | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineDot.gif" width="334" height="80"> |
|
||||||
|
DoubleLine | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/LineDouble.gif" width="350" height="80"> |
|
||||||
|
Triangle bottom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/Triangle.gif" width="350" height="80"> |
|
||||||
|
Triangle top | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/TriangleTop.gif" width="350" height="80"> |
|
||||||
|
Background | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorBackground.gif" width="350" height="80"> |
|
||||||
|
Background with shadow | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorBackgroundShadow.gif" width="350" height="80"> |
|
||||||
|
Background mask | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorBackgroundMask.gif" width="350" height="80"> |
|
||||||
|
Background mask without bottom view | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorBackgroundMaskPure.gif" width="350" height="80"> |
|
||||||
|
Background gradient<br>(fixed) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorBackgroundGradient.gif" width="350" height="80"> |
|
||||||
|
Gradient<br>(change with position) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorGradient.gif" width="350" height="80"> |
|
||||||
|
Image bottom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorImageBottom.gif" width="350" height="80"> |
|
||||||
|
Image background | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorImageBG.gif" width="350" height="80"> |
|
||||||
|
mixed indicators | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Indicator/IndicatorMixed.gif" width="350" height="80"> |
|
||||||
|
|
||||||
|
The following indicators support up and down position switching:
|
||||||
|
`JXSegmentedIndicatorLineView`、`JXSegmentedIndicatorRainbowLineView`、`JXSegmentedIndicatorDotLineView`、`JXSegmentedIndicatorDoubleLineView`、`JXSegmentedIndicatorTriangleView`、`JXSegmentedIndicatorImageView`
|
||||||
|
|
||||||
|
### Cell Preview
|
||||||
|
|
||||||
|
Description | Gif |
|
||||||
|
----|------|
|
||||||
|
title color gradient | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/ColorGradient.gif" width="350" height="80"> |
|
||||||
|
text color gradient | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TextGradient.gif" width="350" height="80"> |
|
||||||
|
transform zoom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/ZoomOnly.gif" width="350" height="80"> |
|
||||||
|
transform zoom + stroke width | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/ZoomStrokeWidth.gif" width="350" height="80"> |
|
||||||
|
transform zoom + selected animation | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/ZoomAnimation.gif" width="350" height="80"> |
|
||||||
|
transform zoom + cell width zoom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/ZoomCellWidth.gif" width="350" height="80"> |
|
||||||
|
TitleImage_Top | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleImageTop.gif" width="350" height="80"> |
|
||||||
|
TitleImage_Left | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleImageLeft.gif" width="350" height="80"> |
|
||||||
|
TitleImage_Bottom | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleImageBottom.gif" width="350" height="80"> |
|
||||||
|
TitleImage_Right | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleImageRight.gif" width="350" height="80"> |
|
||||||
|
TitleImage_OnlyImage | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleImageOnlyImage.gif" width="350" height="80"> |
|
||||||
|
TitleOrImage | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleOrImage.gif" width="350" height="80"> |
|
||||||
|
number | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/Number.gif" width="350" height="80"> |
|
||||||
|
dot | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/CellDot.gif" width="350" height="80"> |
|
||||||
|
attributed text | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/TitleAttributed.gif" width="350" height="80"> |
|
||||||
|
mixed cells | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Cell/MixedCell.gif" width="350" height="80"> |
|
||||||
|
|
||||||
|
### Special Preview
|
||||||
|
|
||||||
|
Description | Gif |
|
||||||
|
----|------|
|
||||||
|
less data<br/> isItemSpacingAverageEnabled is true | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/ItemAveTrue.gif" width="350" height="80"> |
|
||||||
|
less data<br/> isItemSpacingAverageEnabled is false | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/ItemAveFalse.gif" width="350" height="80"> |
|
||||||
|
SegmentedControl<br/>reference[`SegmentedControlViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/SegmentedControl/SegmentedControlViewController.swift) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/SegmentedControl.gif" width="350" height="80"> |
|
||||||
|
SegmentedControl<br/>reference[`SegmentedControlViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/SegmentedControl/SegmentedControlViewController.swift) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/SegmentedControl.gif" width="350" height="80"> |
|
||||||
|
use in navigation bar <br/>reference[`NaviSegmentedControlViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/SegmentedControl/NaviSegmentedControlViewController.swift) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/NavigationBar.gif" width="350" height="80"> |
|
||||||
|
nestable<br/>reference[`NestViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/Nest/NestViewController.swift) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/Nest.gif" width="350" height="200"> |
|
||||||
|
user profile page<br/>reference[`PagingViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/Personal/PagingViewController.swift)<br/> more styles just click[JXPagingView](https://github.com/pujiaxin33/JXPagingView) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/Personal.gif" width="350" height="567"> |
|
||||||
|
data load & refresh<br/>reference[`LoadDataViewController`](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/LoadData/WithListContainerView/LoadDataViewController.swift) | <img src="https://github.com/pujiaxin33/JXExampleImages/blob/master/JXSegmentedView/Special/LoadData.gif" width="350" height="200"> |
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- iOS 9.0+
|
||||||
|
- Xcode 9+
|
||||||
|
- Swift 4.2、5.0
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
Clone the code and drag the Sources folder into the project to use it.
|
||||||
|
|
||||||
|
### CocoaPods
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
target '<Your Target Name>' do
|
||||||
|
pod 'JXSegmentedView'
|
||||||
|
end
|
||||||
|
```
|
||||||
|
Execute `pod repo update` first, then execute `pod install`
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### `JXSegmentedView` example
|
||||||
|
|
||||||
|
1.JXSegmentedView initialize
|
||||||
|
```Swift
|
||||||
|
self.segmentedView = JXSegmentedView()
|
||||||
|
self.segmentedView.delegate = self
|
||||||
|
self.view.addSubview(self.segmentedView)
|
||||||
|
```
|
||||||
|
|
||||||
|
2.dataSource initialize
|
||||||
|
|
||||||
|
The `dataSouce` type is the `JXSegmentedViewDataSource` protocol. Use a separate class to implement the `JXSegmentedViewDataSource` protocol for code isolation. By selecting different class assignments to `dataSource`, you can control the `JXSegmentedView` display effect and implement plugin. For example, selecting the JXSegmentedTitleImageDataSource class as the dataSource selects the display effect of the text image; selecting the JXSegmentedNumberDataSource class as the dataSource selects the display effect of the text & number;
|
||||||
|
```Swift
|
||||||
|
//segmentedDataSource must be strongly held by the property, or it will be released
|
||||||
|
self.segmentedDataSource = JXSegmentedTitleDataSource()
|
||||||
|
//Configuring data source related properties
|
||||||
|
self.segmentedDataSource.titles = ["猴哥", "青蛙王子", "旺财"]
|
||||||
|
self.segmentedDataSource.isTitleColorGradientEnabled = true
|
||||||
|
//The reloadData(selectedIndex:) method must be called, and the method will internally refresh the data source array.
|
||||||
|
self.segmentedDataSource.reloadData(selectedIndex: 0)
|
||||||
|
//Associated dataSource
|
||||||
|
self.segmentedView.dataSource = self.segmentedDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
3.Indicator initialize
|
||||||
|
```Swift
|
||||||
|
let indicator = JXSegmentedIndicatorLineView()
|
||||||
|
indicator.indicatorWidth = 20
|
||||||
|
self.segmentedView.indicators = [indicator]
|
||||||
|
```
|
||||||
|
|
||||||
|
4.Implement `JXSegmentedViewDelegate`
|
||||||
|
```Swift
|
||||||
|
//This method is called when you click to select or scroll to select. Applicable to only care about selected events, regardless of whether it is click or scroll.
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) {}
|
||||||
|
|
||||||
|
// This method will be called when the selected condition is clicked.
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int) {}
|
||||||
|
|
||||||
|
// This method is called when the scroll is selected.
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int) {}
|
||||||
|
|
||||||
|
// Then callback when scrolling
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `contentScrollView` list container usage example
|
||||||
|
|
||||||
|
#### Use the `UIScrollView` custom usage example directly
|
||||||
|
|
||||||
|
Because the code is scattered and the amount of code is large, it is not recommended. There are many places to pay attention to when using it properly, especially for students who are new to iOS.
|
||||||
|
|
||||||
|
Do not directly paste the code, click [LoadDataCustomViewController](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/LoadData/ListCustom/LoadDataCustomViewController.swift) to view the source code.
|
||||||
|
|
||||||
|
As an alternative, the official use & is highly recommended to use the following in this way 👇👇👇.
|
||||||
|
|
||||||
|
#### Use the example of the `JXSegmentedListContainerView` wrapper class
|
||||||
|
|
||||||
|
`JXSegmentedListContainerView` is a highly encapsulated class for list views with the following advantages:
|
||||||
|
- Compared to the direct use of `UIScrollView` customization, the package is high, the code is centralized, and the use is simple;
|
||||||
|
- List lazy loading: List initialization is only performed when a list is displayed. Instead of loading all the lists at once, the performance is better;
|
||||||
|
|
||||||
|
1.`JXSegmentedListContainerView` initialize
|
||||||
|
```Swift
|
||||||
|
self.listContainerView = JXSegmentedListContainerView(dataSource: self)
|
||||||
|
self.view.addSubview(self.listContainerView)
|
||||||
|
//Associate the cotentScrollView
|
||||||
|
self.segmentedView.contentScrollView = self.listContainerView.scrollView
|
||||||
|
```
|
||||||
|
|
||||||
|
2.Implement `JXSegmentedListContainerViewDataSource`
|
||||||
|
```Swift
|
||||||
|
//return numbers of lists
|
||||||
|
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
|
||||||
|
return self.segmentedDataSource.titles.count
|
||||||
|
}
|
||||||
|
//return the instance which comform `JXSegmentedListContainerViewListDelegate`
|
||||||
|
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate {
|
||||||
|
return ListBaseViewController()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3.Implement `JXSegmentedListContainerViewListDelegate` for list
|
||||||
|
|
||||||
|
Regardless of whether the type of the list is UIView or UIViewController
|
||||||
|
```Swift
|
||||||
|
/// If the list is VC, return VC.view
|
||||||
|
/// If the list is View, return to View itself
|
||||||
|
/// - Returns: list
|
||||||
|
func listView() -> UIView {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
//Optional use, when the list is displayed
|
||||||
|
func listDidAppear() {}
|
||||||
|
|
||||||
|
//Optional use, called when the list disappears
|
||||||
|
func listDidDisappear() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
4.Tell the key event `JXSegmentedListContainerView`
|
||||||
|
|
||||||
|
In the following two `JXSegmentedViewDelegate` proxy methods, call the corresponding code, don't forget this one❗️❗️❗️
|
||||||
|
```Swift
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int) {
|
||||||
|
//Pass the didClickSelectedItemAt event to the listContainerView, which must be called! ! !
|
||||||
|
listContainerView.didClickSelectedItem(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat) {
|
||||||
|
//Pass the scrolling event to the listContainerView, which must be called! ! !
|
||||||
|
listContainerView.segmentedViewScrolling(from: leftIndex, to: rightIndex, percent: percent, selectedIndex: segmentedView.selectedIndex)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Click [LoadDataViewController](https://github.com/pujiaxin33/JXSegmentedView/blob/master/JXSegmentedView/Special/LoadData/WithListContainerView/LoadDataViewController.swift) to see the source code.
|
||||||
|
|
||||||
|
### Usage Summary
|
||||||
|
|
||||||
|
Because `JXSegmentedView` supports many features: indicators, cell styles, list containers, etc. How to manage the code orderly becomes a problem. The use of protocols, inheritance, and encapsulation classes greatly simplifies the use, and increases flexibility, making extensions quite easy.
|
||||||
|
|
||||||
|
- Core main class:`JXSegmentedView`
|
||||||
|
- Data Source & Cell Style Custom Class: Classes that follow the `JXSegmentedViewDataSource` protocol
|
||||||
|
- Indicator class: `UIView` class that complies with the `JXSegmentedIndicatorProtocol` protocol
|
||||||
|
- List container: officially recommended `JXSegmentedListContainerView` class, special case can be customized using `UIScrollView`
|
||||||
|
|
||||||
|
### Indicator style customization
|
||||||
|
|
||||||
|
- To inherit the `JXSegmentedIndicatorProtocol` protocol, click on [JXSegmentedIndicatorProtocol](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Sources/Indicator/JXSegmentedIndicatorProtocol.swift)
|
||||||
|
- The base class `JXSegmentedIndicatorBaseView` inheriting the `JXSegmentedIndicatorProtocol` protocol is provided, which provides many basic properties. Click to see [JXSegmentedIndicatorBaseView](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Sources/Indicator/JXSegmentedIndicatorBaseView.swift)
|
||||||
|
- Custom indicator, please refer to the implemented indicator view, try more, think more, please ask Issue or join feedback QQ group if you have any questions.
|
||||||
|
|
||||||
|
|
||||||
|
### dataSource and Cell customization
|
||||||
|
|
||||||
|
- Need to inherit the `JXSegmentedViewDataSource` protocol, click on [JXSegmentedViewDataSource](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Sources/Core/JXSegmentedView.swift)
|
||||||
|
- Provides the base class `JXSegmentedBaseDataSource` that inherits the `JXSegmentedViewDataSource` protocol, which provides many basic properties. Click to see [JXSegmentedBaseDataSource](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Sources/Core/JXSegmentedBaseDataSource.swift)
|
||||||
|
- Any custom requirements, dataSource, cell, itemModel must be subclassed. Even if a subclass cell does nothing. Used to maintain the inheritance chain, so as not to know who to inherit after subclassing;
|
||||||
|
- dataSource and Cell customization, please refer to the implemented dataSource, try more, think more, please ask Issue or join feedback QQ group if you have any questions.
|
||||||
|
|
||||||
|
## Common Attribute Description
|
||||||
|
|
||||||
|
[Common attribute description document address](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Document/English/property.md)
|
||||||
|
|
||||||
|
## Other Usage Tips
|
||||||
|
|
||||||
|
[Other usage tips document address](https://github.com/pujiaxin33/JXSegmentedView/blob/master/Document/English/tips.md)
|
||||||
|
|
||||||
|
## Supplement
|
||||||
|
|
||||||
|
If you are just starting to use `JXSegmentedView`, be sure to search for the documentation or source code when you need to support certain features during development. Confirm that you have implemented the features you want to support. Please don't ask the documentation and source code to see it, just ask questions directly. This is a waste of time for everyone. If you don't support the features you want, feel free to ask for an discussion, or implement a PullRequest yourself.
|
||||||
|
|
||||||
|
If you have any suggestions or questions, you can contact me by:</br>
|
||||||
|
E-mail:317437084@qq.com </br>
|
||||||
|
QQ Group: 112440276
|
||||||
|
|
||||||
|
<img src="https://note.youdao.com/yws/public/resource/c6fa96a65e424afcf7f6304ddf5c283a/xmlnote/8dc821d271c35845acff3f853f434bce/3913" width="300" height="411">
|
||||||
|
|
||||||
|
If you like, just star❤️ it.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
JXSegmentedView is released under the MIT license.
|
||||||
41
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeCell.swift
generated
Normal file
41
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeCell.swift
generated
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleAttributeCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/3.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleAttributeCell: JXSegmentedBaseCell {
|
||||||
|
open var titleLabel = UILabel()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(titleLabel)
|
||||||
|
|
||||||
|
let centerX = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1, constant: 0)
|
||||||
|
contentView.addConstraint(centerX)
|
||||||
|
let centerY = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)
|
||||||
|
contentView.addConstraint(centerY)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleAttributeItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.numberOfLines = myItemModel.titleNumberOfLines
|
||||||
|
if myItemModel.isSelected && myItemModel.selectedAttributedTitle != nil {
|
||||||
|
titleLabel.attributedText = myItemModel.selectedAttributedTitle
|
||||||
|
}else {
|
||||||
|
titleLabel.attributedText = myItemModel.attributedTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeDataSource.swift
generated
Normal file
76
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeDataSource.swift
generated
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleAttributeDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/2.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleAttributeDataSource: JXSegmentedBaseDataSource {
|
||||||
|
/// 富文本title数组
|
||||||
|
open var attributedTitles = [NSAttributedString]()
|
||||||
|
/// 选中时的富文本,可选。如果要使用确保count与attributedTitles一致。
|
||||||
|
open var selectedAttributedTitles: [NSAttributedString]?
|
||||||
|
/// 如果将JXSegmentedView嵌套进UITableView的cell,每次重用的时候,JXSegmentedView进行reloadData时,会重新计算所有的title宽度。所以该应用场景,需要UITableView的cellModel缓存titles的文字宽度,再通过该闭包方法返回给JXSegmentedView。
|
||||||
|
open var widthForTitleClosure: ((NSAttributedString)->(CGFloat))?
|
||||||
|
/// title的numberOfLines
|
||||||
|
open var titleNumberOfLines: Int = 2
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedTitleAttributeItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredItemCount() -> Int {
|
||||||
|
return attributedTitles.count
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleAttributeItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myItemModel.attributedTitle = attributedTitles[index]
|
||||||
|
myItemModel.selectedAttributedTitle = selectedAttributedTitles?[index]
|
||||||
|
myItemModel.textWidth = widthForTitle(myItemModel.attributedTitle, selectedTitle: myItemModel.selectedAttributedTitle)
|
||||||
|
myItemModel.titleNumberOfLines = titleNumberOfLines
|
||||||
|
}
|
||||||
|
|
||||||
|
open func widthForTitle(_ title: NSAttributedString?, selectedTitle: NSAttributedString?) -> CGFloat {
|
||||||
|
let attriText = selectedTitle != nil ? selectedTitle : title
|
||||||
|
guard let text = attriText else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if widthForTitleClosure != nil {
|
||||||
|
return widthForTitleClosure!(text)
|
||||||
|
}else {
|
||||||
|
let textWidth = text.boundingRect(with: CGSize(width: CGFloat.infinity, height: CGFloat.infinity), options: NSStringDrawingOptions.init(rawValue: NSStringDrawingOptions.usesLineFragmentOrigin.rawValue | NSStringDrawingOptions.usesFontLeading.rawValue), context: nil).size.width
|
||||||
|
return CGFloat(ceilf(Float(textWidth)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 因为该方法会被频繁调用,所以应该在`preferredRefreshItemModel( _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)`方法里面,根据数据源计算好文字宽度,然后缓存起来。该方法直接使用已经计算好的文字宽度即可。
|
||||||
|
open override func preferredSegmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat {
|
||||||
|
var width: CGFloat = 0
|
||||||
|
if itemWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
let myItemModel = dataSource[index] as! JXSegmentedTitleAttributeItemModel
|
||||||
|
width = myItemModel.textWidth + itemWidthIncrement
|
||||||
|
}else {
|
||||||
|
width = itemWidth + itemWidthIncrement
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedTitleAttributeCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeItemModel.swift
generated
Normal file
16
Pods/JXSegmentedView/Sources/AttributeTitle/JXSegmentedTitleAttributeItemModel.swift
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleAttributeItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/3.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleAttributeItemModel: JXSegmentedBaseItemModel {
|
||||||
|
open var attributedTitle: NSAttributedString?
|
||||||
|
open var selectedAttributedTitle: NSAttributedString?
|
||||||
|
open var titleNumberOfLines: Int = 0
|
||||||
|
open var textWidth: CGFloat = 0
|
||||||
|
}
|
||||||
46
Pods/JXSegmentedView/Sources/Common/JXSegmentedAnimator.swift
generated
Normal file
46
Pods/JXSegmentedView/Sources/Common/JXSegmentedAnimator.swift
generated
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedAnimator.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/21.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedAnimator {
|
||||||
|
open var duration: TimeInterval = 0.25
|
||||||
|
open var progressClosure: ((CGFloat)->())?
|
||||||
|
open var completedClosure: (()->())?
|
||||||
|
private var displayLink: CADisplayLink!
|
||||||
|
private var firstTimestamp: CFTimeInterval?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
displayLink = CADisplayLink(target: self, selector: #selector(processDisplayLink(sender:)))
|
||||||
|
}
|
||||||
|
|
||||||
|
open func start() {
|
||||||
|
displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func stop() {
|
||||||
|
progressClosure?(1)
|
||||||
|
displayLink.invalidate()
|
||||||
|
completedClosure?()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func processDisplayLink(sender: CADisplayLink) {
|
||||||
|
if firstTimestamp == nil {
|
||||||
|
firstTimestamp = sender.timestamp
|
||||||
|
}
|
||||||
|
let percent = (sender.timestamp - firstTimestamp!)/duration
|
||||||
|
if percent >= 1 {
|
||||||
|
progressClosure?(1)
|
||||||
|
displayLink.invalidate()
|
||||||
|
completedClosure?()
|
||||||
|
}else {
|
||||||
|
progressClosure?(CGFloat(percent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
512
Pods/JXSegmentedView/Sources/Common/JXSegmentedListContainerView.swift
generated
Normal file
512
Pods/JXSegmentedView/Sources/Common/JXSegmentedListContainerView.swift
generated
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedListContainerView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 列表容器视图的类型
|
||||||
|
///- ScrollView: UIScrollView。优势:没有其他副作用。劣势:视图内存占用相对大一点。因为所有的列表视图都在UIScrollView的视图层级里面。
|
||||||
|
/// - CollectionView: 使用UICollectionView。优势:因为列表被添加到cell上,视图的内存占用更少,适合内存要求特别高的场景。劣势:因为cell重用机制的问题,导致列表下拉刷新视图(比如MJRefresh),会因为被removeFromSuperview而被隐藏。所以,列表有下拉刷新需求的,请使用scrollView type。
|
||||||
|
public enum JXSegmentedListContainerType {
|
||||||
|
case scrollView
|
||||||
|
case collectionView
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public protocol JXSegmentedListContainerViewListDelegate {
|
||||||
|
/// 如果列表是VC,就返回VC.view
|
||||||
|
/// 如果列表是View,就返回View自己
|
||||||
|
///
|
||||||
|
/// - Returns: 返回列表视图
|
||||||
|
func listView() -> UIView
|
||||||
|
@objc optional func listWillAppear()
|
||||||
|
@objc optional func listDidAppear()
|
||||||
|
@objc optional func listWillDisappear()
|
||||||
|
@objc optional func listDidDisappear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public protocol JXSegmentedListContainerViewDataSource {
|
||||||
|
/// 返回list的数量
|
||||||
|
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int
|
||||||
|
|
||||||
|
/// 根据index初始化一个对应列表实例,需要是遵从`JXSegmentedListContainerViewListDelegate`协议的对象。
|
||||||
|
/// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIView即可。
|
||||||
|
/// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXSegmentedListContainerViewListDelegate`协议,该方法返回自定义UIViewController即可。
|
||||||
|
/// 注意:一定要是新生成的实例!!!
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - listContainerView: JXSegmentedListContainerView
|
||||||
|
/// - index: 目标index
|
||||||
|
/// - Returns: 遵从JXSegmentedListContainerViewListDelegate协议的实例
|
||||||
|
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate
|
||||||
|
|
||||||
|
|
||||||
|
/// 控制能否初始化对应index的列表。有些业务需求,需要在某些情况才允许初始化某些列表,通过通过该代理实现控制。
|
||||||
|
@objc optional func listContainerView(_ listContainerView: JXSegmentedListContainerView, canInitListAt index: Int) -> Bool
|
||||||
|
|
||||||
|
/// 返回自定义UIScrollView或UICollectionView的Class
|
||||||
|
/// 某些特殊情况需要自己处理UIScrollView内部逻辑。比如项目用了FDFullscreenPopGesture,需要处理手势相关代理。
|
||||||
|
///
|
||||||
|
/// - Parameter listContainerView: JXSegmentedListContainerView
|
||||||
|
/// - Returns: 自定义UIScrollView实例
|
||||||
|
@objc optional func scrollViewClass(in listContainerView: JXSegmentedListContainerView) -> AnyClass
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXSegmentedListContainerView: UIView, JXSegmentedViewListContainer, JXSegmentedViewRTLCompatible {
|
||||||
|
open private(set) var type: JXSegmentedListContainerType
|
||||||
|
open private(set) weak var dataSource: JXSegmentedListContainerViewDataSource?
|
||||||
|
open private(set) var scrollView: UIScrollView!
|
||||||
|
/// 已经加载过的列表字典。key是index,value是对应的列表
|
||||||
|
open private(set) var validListDict = [Int:JXSegmentedListContainerViewListDelegate]()
|
||||||
|
/// 滚动切换的时候,滚动距离超过一页的多少百分比,就触发列表的初始化。默认0.01(即列表显示了一点就触发加载)。范围0~1,开区间不包括0和1
|
||||||
|
open var initListPercent: CGFloat = 0.01 {
|
||||||
|
didSet {
|
||||||
|
if initListPercent <= 0 || initListPercent >= 1 {
|
||||||
|
assertionFailure("initListPercent值范围为开区间(0,1),即不包括0和1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var listCellBackgroundColor: UIColor = .white
|
||||||
|
/// 需要和segmentedView.defaultSelectedIndex保持一致,用于触发默认index列表的加载
|
||||||
|
open var defaultSelectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
currentIndex = defaultSelectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var currentIndex: Int = 0
|
||||||
|
private lazy var collectionView: UICollectionView = {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
layout.minimumLineSpacing = 0
|
||||||
|
layout.minimumInteritemSpacing = 0
|
||||||
|
if let collectionViewClass = dataSource?.scrollViewClass?(in: self) as? UICollectionView.Type {
|
||||||
|
return collectionViewClass.init(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
}else {
|
||||||
|
return UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
private lazy var containerVC = JXSegmentedListContainerViewController()
|
||||||
|
private var willAppearIndex: Int = -1
|
||||||
|
private var willDisappearIndex: Int = -1
|
||||||
|
|
||||||
|
public init(dataSource: JXSegmentedListContainerViewDataSource, type: JXSegmentedListContainerType = .scrollView) {
|
||||||
|
self.dataSource = dataSource
|
||||||
|
self.type = type
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
open func commonInit() {
|
||||||
|
containerVC.view.backgroundColor = .clear
|
||||||
|
addSubview(containerVC.view)
|
||||||
|
containerVC.viewWillAppearClosure = {[weak self] in
|
||||||
|
self?.listWillAppear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewDidAppearClosure = {[weak self] in
|
||||||
|
self?.listDidAppear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewWillDisappearClosure = {[weak self] in
|
||||||
|
self?.listWillDisappear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
containerVC.viewDidDisappearClosure = {[weak self] in
|
||||||
|
self?.listDidDisappear(at: self?.currentIndex ?? 0)
|
||||||
|
}
|
||||||
|
if type == .scrollView {
|
||||||
|
if let scrollViewClass = dataSource?.scrollViewClass?(in: self) as? UIScrollView.Type {
|
||||||
|
scrollView = scrollViewClass.init()
|
||||||
|
}else {
|
||||||
|
scrollView = UIScrollView.init()
|
||||||
|
}
|
||||||
|
scrollView.delegate = self
|
||||||
|
scrollView.isPagingEnabled = true
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
scrollView.scrollsToTop = false
|
||||||
|
scrollView.bounces = false
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
segmentedView(horizontalFlipForView: scrollView)
|
||||||
|
}
|
||||||
|
containerVC.view.addSubview(scrollView)
|
||||||
|
}else if type == .collectionView {
|
||||||
|
collectionView.isPagingEnabled = true
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.showsVerticalScrollIndicator = false
|
||||||
|
collectionView.scrollsToTop = false
|
||||||
|
collectionView.bounces = false
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.register(JXSegmentedRTLCollectionCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
collectionView.isPrefetchingEnabled = false
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
self.collectionView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
collectionView.semanticContentAttribute = .forceLeftToRight
|
||||||
|
segmentedView(horizontalFlipForView: collectionView)
|
||||||
|
}
|
||||||
|
containerVC.view.addSubview(collectionView)
|
||||||
|
//让外部统一访问scrollView
|
||||||
|
scrollView = collectionView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func willMove(toSuperview newSuperview: UIView?) {
|
||||||
|
super.willMove(toSuperview: newSuperview)
|
||||||
|
var next: UIResponder? = newSuperview
|
||||||
|
while next != nil {
|
||||||
|
if let vc = next as? UIViewController{
|
||||||
|
vc.addChild(containerVC)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next = next?.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
containerVC.view.frame = bounds
|
||||||
|
guard let count = dataSource?.numberOfLists(in: self) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if type == .scrollView {
|
||||||
|
if scrollView.frame == CGRect.zero || scrollView.bounds.size != bounds.size {
|
||||||
|
scrollView.frame = bounds
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height)
|
||||||
|
for (index, list) in validListDict {
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
}
|
||||||
|
scrollView.contentOffset = CGPoint(x: CGFloat(currentIndex)*scrollView.bounds.size.width, y: 0)
|
||||||
|
}else {
|
||||||
|
scrollView.frame = bounds
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(count), height: scrollView.bounds.size.height)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
if collectionView.frame == CGRect.zero || collectionView.bounds.size != bounds.size {
|
||||||
|
collectionView.frame = bounds
|
||||||
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
collectionView.setContentOffset(CGPoint(x: CGFloat(currentIndex)*collectionView.bounds.size.width, y: 0), animated: false)
|
||||||
|
}else {
|
||||||
|
collectionView.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewListContainer
|
||||||
|
|
||||||
|
public func contentScrollView() -> UIScrollView {
|
||||||
|
return scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrolling(from leftIndex: Int, to rightIndex: Int, percent: CGFloat, selectedIndex: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
open func didClickSelectedItem(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
willAppearIndex = -1
|
||||||
|
willDisappearIndex = -1
|
||||||
|
if currentIndex != index {
|
||||||
|
listWillDisappear(at: currentIndex)
|
||||||
|
listWillAppear(at: index)
|
||||||
|
listDidDisappear(at: currentIndex)
|
||||||
|
listDidAppear(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadData() {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
if currentIndex < 0 || currentIndex >= dataSource.numberOfLists(in: self) {
|
||||||
|
defaultSelectedIndex = 0
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
validListDict.values.forEach { (list) in
|
||||||
|
if let listVC = list as? UIViewController {
|
||||||
|
listVC.removeFromParent()
|
||||||
|
}
|
||||||
|
list.listView().removeFromSuperview()
|
||||||
|
}
|
||||||
|
validListDict.removeAll()
|
||||||
|
if type == .scrollView {
|
||||||
|
scrollView.contentSize = CGSize(width: scrollView.bounds.size.width*CGFloat(dataSource.numberOfLists(in: self)), height: scrollView.bounds.size.height)
|
||||||
|
}else {
|
||||||
|
collectionView.reloadData()
|
||||||
|
}
|
||||||
|
listWillAppear(at: currentIndex)
|
||||||
|
listDidAppear(at: currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
func initListIfNeeded(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
if dataSource.listContainerView?(self, canInitListAt: index) == false {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var existedList = validListDict[index]
|
||||||
|
if existedList != nil {
|
||||||
|
//列表已经创建好了
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existedList = dataSource.listContainerView(self, initListAt: index)
|
||||||
|
guard let list = existedList else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
containerVC.addChild(vc)
|
||||||
|
}
|
||||||
|
validListDict[index] = list
|
||||||
|
if type == .scrollView {
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
scrollView.addSubview(list.listView())
|
||||||
|
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
segmentedView(horizontalFlipForView: list.listView())
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0))
|
||||||
|
cell?.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
list.listView().frame = cell?.contentView.bounds ?? CGRect.zero
|
||||||
|
cell?.contentView.addSubview(list.listView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listWillAppear(at index: Int) {
|
||||||
|
guard let dataSource = dataSource else { return }
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var existedList = validListDict[index]
|
||||||
|
if existedList != nil {
|
||||||
|
existedList?.listWillAppear?()
|
||||||
|
if let vc = existedList as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//当前列表未被创建(页面初始化或通过点击触发的listWillAppear)
|
||||||
|
guard dataSource.listContainerView?(self, canInitListAt: index) != false else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existedList = dataSource.listContainerView(self, initListAt: index)
|
||||||
|
guard let list = existedList else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
containerVC.addChild(vc)
|
||||||
|
}
|
||||||
|
validListDict[index] = list
|
||||||
|
if type == .scrollView {
|
||||||
|
if list.listView().superview == nil {
|
||||||
|
list.listView().frame = CGRect(x: CGFloat(index)*scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
|
||||||
|
scrollView.addSubview(list.listView())
|
||||||
|
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
segmentedView(horizontalFlipForView: list.listView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.listWillAppear?()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0))
|
||||||
|
cell?.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
list.listView().frame = cell?.contentView.bounds ?? CGRect.zero
|
||||||
|
cell?.contentView.addSubview(list.listView())
|
||||||
|
list.listWillAppear?()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(true, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidAppear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentIndex = index
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listDidAppear?()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.endAppearanceTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listWillDisappear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listWillDisappear?()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.beginAppearanceTransition(false, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidDisappear(at index: Int) {
|
||||||
|
guard checkIndexValid(index) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let list = validListDict[index]
|
||||||
|
list?.listDidDisappear?()
|
||||||
|
if let vc = list as? UIViewController {
|
||||||
|
vc.endAppearanceTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkIndexValid(_ index: Int) -> Bool {
|
||||||
|
guard let dataSource = dataSource else { return false }
|
||||||
|
let count = dataSource.numberOfLists(in: self)
|
||||||
|
if count <= 0 || index >= count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listDidAppearOrDisappear(scrollView: UIScrollView) {
|
||||||
|
let currentIndexPercent = scrollView.contentOffset.x/scrollView.bounds.size.width
|
||||||
|
if willAppearIndex != -1 || willDisappearIndex != -1 {
|
||||||
|
let disappearIndex = willDisappearIndex
|
||||||
|
let appearIndex = willAppearIndex
|
||||||
|
if willAppearIndex > willDisappearIndex {
|
||||||
|
//将要出现的列表在右边
|
||||||
|
if currentIndexPercent >= CGFloat(willAppearIndex) {
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
listDidDisappear(at: disappearIndex)
|
||||||
|
listDidAppear(at: appearIndex)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//将要出现的列表在左边
|
||||||
|
if currentIndexPercent <= CGFloat(willAppearIndex) {
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
listDidDisappear(at: disappearIndex)
|
||||||
|
listDidAppear(at: appearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXSegmentedListContainerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
guard let dataSource = dataSource else { return 0 }
|
||||||
|
return dataSource.numberOfLists(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
|
||||||
|
cell.contentView.backgroundColor = listCellBackgroundColor
|
||||||
|
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
let list = validListDict[indexPath.item]
|
||||||
|
if list != nil {
|
||||||
|
list?.listView().frame = cell.contentView.bounds
|
||||||
|
cell.contentView.addSubview(list!.listView())
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let percent = scrollView.contentOffset.x/scrollView.bounds.size.width
|
||||||
|
let maxCount = Int(round(scrollView.contentSize.width/scrollView.bounds.size.width))
|
||||||
|
var leftIndex = Int(floor(Double(percent)))
|
||||||
|
leftIndex = max(0, min(maxCount - 1, leftIndex))
|
||||||
|
let rightIndex = leftIndex + 1;
|
||||||
|
if percent < 0 || rightIndex >= maxCount {
|
||||||
|
listDidAppearOrDisappear(scrollView: scrollView)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let remainderRatio = percent - CGFloat(leftIndex)
|
||||||
|
if rightIndex == currentIndex {
|
||||||
|
//当前选中的在右边,用户正在从右边往左边滑动
|
||||||
|
if validListDict[leftIndex] == nil && remainderRatio < (1 - initListPercent) {
|
||||||
|
initListIfNeeded(at: leftIndex)
|
||||||
|
}else if validListDict[leftIndex] != nil {
|
||||||
|
if willAppearIndex == -1 {
|
||||||
|
willAppearIndex = leftIndex;
|
||||||
|
listWillAppear(at: willAppearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if willDisappearIndex == -1 {
|
||||||
|
willDisappearIndex = rightIndex
|
||||||
|
listWillDisappear(at: willDisappearIndex)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//当前选中的在左边,用户正在从左边往右边滑动
|
||||||
|
if validListDict[rightIndex] == nil && remainderRatio > initListPercent {
|
||||||
|
initListIfNeeded(at: rightIndex)
|
||||||
|
}else if validListDict[rightIndex] != nil {
|
||||||
|
if willAppearIndex == -1 {
|
||||||
|
willAppearIndex = rightIndex
|
||||||
|
listWillAppear(at: willAppearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if willDisappearIndex == -1 {
|
||||||
|
willDisappearIndex = leftIndex
|
||||||
|
listWillDisappear(at: willDisappearIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listDidAppearOrDisappear(scrollView: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
//滑动到一半又取消滑动处理
|
||||||
|
if willAppearIndex != -1 || willDisappearIndex != -1 {
|
||||||
|
listWillDisappear(at: willAppearIndex)
|
||||||
|
listWillAppear(at: willDisappearIndex)
|
||||||
|
listDidDisappear(at: willAppearIndex)
|
||||||
|
listDidAppear(at: willDisappearIndex)
|
||||||
|
willDisappearIndex = -1
|
||||||
|
willAppearIndex = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JXSegmentedListContainerViewController: UIViewController {
|
||||||
|
var viewWillAppearClosure: (()->())?
|
||||||
|
var viewDidAppearClosure: (()->())?
|
||||||
|
var viewWillDisappearClosure: (()->())?
|
||||||
|
var viewDidDisappearClosure: (()->())?
|
||||||
|
override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false }
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
viewWillAppearClosure?()
|
||||||
|
}
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
viewDidAppearClosure?()
|
||||||
|
}
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
viewWillDisappearClosure?()
|
||||||
|
}
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
viewDidDisappearClosure?()
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Pods/JXSegmentedView/Sources/Common/JXSegmentedRTLLayout.swift
generated
Normal file
50
Pods/JXSegmentedView/Sources/Common/JXSegmentedRTLLayout.swift
generated
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedRTLLayout.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by blue on 2020/6/18.
|
||||||
|
// Copyright © 2020 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol JXSegmentedViewRTLCompatible: class {
|
||||||
|
func segmentedViewShouldRTLLayout() -> Bool
|
||||||
|
func segmentedView(horizontalFlipForView view: UIView?)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension JXSegmentedViewRTLCompatible {
|
||||||
|
|
||||||
|
/// 根据当前系统布局方式返回是否需要RTL布局
|
||||||
|
func segmentedViewShouldRTLLayout() -> Bool {
|
||||||
|
return UIView.userInterfaceLayoutDirection(for: UIView.appearance().semanticContentAttribute) == .rightToLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在RTL布局下水平翻转当前视图
|
||||||
|
/// - Parameter view: 需要翻转的视图
|
||||||
|
func segmentedView(horizontalFlipForView view: UIView?) {
|
||||||
|
view?.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class JXSegmentedRTLCollectionCell: UICollectionViewCell, JXSegmentedViewRTLCompatible {
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonInit() {
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
segmentedView(horizontalFlipForView: self)
|
||||||
|
segmentedView(horizontalFlipForView: contentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
Pods/JXSegmentedView/Sources/Common/JXSegmentedViewTool.swift
generated
Normal file
76
Pods/JXSegmentedView/Sources/Common/JXSegmentedViewTool.swift
generated
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedViewTool.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public extension UIColor {
|
||||||
|
var jx_red: CGFloat {
|
||||||
|
var r: CGFloat = 0
|
||||||
|
getRed(&r, green: nil, blue: nil, alpha: nil)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
var jx_green: CGFloat {
|
||||||
|
var g: CGFloat = 0
|
||||||
|
getRed(nil, green: &g, blue: nil, alpha: nil)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
var jx_blue: CGFloat {
|
||||||
|
var b: CGFloat = 0
|
||||||
|
getRed(nil, green: nil, blue: &b, alpha: nil)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
var jx_alpha: CGFloat {
|
||||||
|
return cgColor.alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JXSegmentedViewTool {
|
||||||
|
public static func interpolate<T: SignedNumeric & Comparable>(from: T, to: T, percent: T) -> T {
|
||||||
|
let percent = max(0, min(1, percent))
|
||||||
|
return from + (to - from) * percent
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func interpolateColor(from: UIColor, to: UIColor, percent: CGFloat) -> UIColor {
|
||||||
|
let r = interpolate(from: from.jx_red, to: to.jx_red, percent: percent)
|
||||||
|
let g = interpolate(from: from.jx_green, to: to.jx_green, percent: CGFloat(percent))
|
||||||
|
let b = interpolate(from: from.jx_blue, to: to.jx_blue, percent: CGFloat(percent))
|
||||||
|
let a = interpolate(from: from.jx_alpha, to: to.jx_alpha, percent: CGFloat(percent))
|
||||||
|
return UIColor(red: r, green: g, blue: b, alpha: a)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func interpolateColors(from: [CGColor], to: [CGColor], percent: CGFloat) -> [CGColor] {
|
||||||
|
var resultColors = [CGColor]()
|
||||||
|
for index in 0..<from.count {
|
||||||
|
let fromColor = UIColor(cgColor: from[index])
|
||||||
|
let toColor = UIColor(cgColor: to[index])
|
||||||
|
let r = interpolate(from: fromColor.jx_red, to: toColor.jx_red, percent: percent)
|
||||||
|
let g = interpolate(from: fromColor.jx_green, to: toColor.jx_green, percent: CGFloat(percent))
|
||||||
|
let b = interpolate(from: fromColor.jx_blue, to: toColor.jx_blue, percent: CGFloat(percent))
|
||||||
|
let a = interpolate(from: fromColor.jx_alpha, to: toColor.jx_alpha, percent: CGFloat(percent))
|
||||||
|
resultColors.append(UIColor(red: r, green: g, blue: b, alpha: a).cgColor)
|
||||||
|
}
|
||||||
|
return resultColors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXSegmentedViewTool {
|
||||||
|
public static func interpolateThemeColor(from: UIColor, to: UIColor, percent: CGFloat) -> UIColor {
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
return UIColor { (traitCollection) -> UIColor in
|
||||||
|
let resolvedFrom = from.resolvedColor(with: traitCollection)
|
||||||
|
let resolvedTo = to.resolvedColor(with: traitCollection)
|
||||||
|
return interpolateColor(from: resolvedFrom, to: resolvedTo, percent: percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return interpolateColor(from: from, to: to, percent: percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseCell.swift
generated
Normal file
118
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseCell.swift
generated
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedBaseCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public typealias JXSegmentedCellSelectedAnimationClosure = (CGFloat)->()
|
||||||
|
|
||||||
|
open class JXSegmentedBaseCell: UICollectionViewCell, JXSegmentedViewRTLCompatible {
|
||||||
|
open var itemModel: JXSegmentedBaseItemModel?
|
||||||
|
open var animator: JXSegmentedAnimator?
|
||||||
|
private var selectedAnimationClosureArray = [JXSegmentedCellSelectedAnimationClosure]()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
animator?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
animator?.stop()
|
||||||
|
animator = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func commonInit() {
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
segmentedView(horizontalFlipForView: self)
|
||||||
|
segmentedView(horizontalFlipForView: contentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func canStartSelectedAnimation(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) -> Bool {
|
||||||
|
var isSelectedAnimatable = false
|
||||||
|
if itemModel.isSelectedAnimable {
|
||||||
|
if selectedType == .scroll {
|
||||||
|
//滚动选中且没有开启左右过渡,允许动画
|
||||||
|
if !itemModel.isItemTransitionEnabled {
|
||||||
|
isSelectedAnimatable = true
|
||||||
|
}
|
||||||
|
}else if selectedType == .click || selectedType == .code {
|
||||||
|
//点击和代码选中,允许动画
|
||||||
|
isSelectedAnimatable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isSelectedAnimatable
|
||||||
|
}
|
||||||
|
|
||||||
|
open func appendSelectedAnimationClosure(closure: @escaping JXSegmentedCellSelectedAnimationClosure) {
|
||||||
|
selectedAnimationClosureArray.append(closure)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func startSelectedAnimationIfNeeded(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
if itemModel.isSelectedAnimable && canStartSelectedAnimation(itemModel: itemModel, selectedType: selectedType) {
|
||||||
|
//需要更新isTransitionAnimating,用于处理在过滤时,禁止响应点击,避免界面异常。
|
||||||
|
itemModel.isTransitionAnimating = true
|
||||||
|
animator?.progressClosure = {[weak self] (percent) in
|
||||||
|
guard self != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for closure in self!.selectedAnimationClosureArray {
|
||||||
|
closure(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animator?.completedClosure = {[weak self] in
|
||||||
|
itemModel.isTransitionAnimating = false
|
||||||
|
self?.selectedAnimationClosureArray.removeAll()
|
||||||
|
}
|
||||||
|
animator?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
self.itemModel = itemModel
|
||||||
|
|
||||||
|
if itemModel.isSelectedAnimable {
|
||||||
|
selectedAnimationClosureArray.removeAll()
|
||||||
|
if canStartSelectedAnimation(itemModel: itemModel, selectedType: selectedType) {
|
||||||
|
animator = JXSegmentedAnimator()
|
||||||
|
animator?.duration = itemModel.selectedAnimationDuration
|
||||||
|
}else {
|
||||||
|
animator?.stop()
|
||||||
|
animator = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override var isSelected: Bool {
|
||||||
|
didSet {
|
||||||
|
setSelectedStyle(isSelected: isSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override var isHighlighted: Bool {
|
||||||
|
didSet {
|
||||||
|
setSelectedStyle(isSelected: isHighlighted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSelectedStyle(isSelected: Bool) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseDataSource.swift
generated
Normal file
174
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseDataSource.swift
generated
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedBaseDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedBaseDataSource: JXSegmentedViewDataSource {
|
||||||
|
/// 最终传递给JXSegmentedView的数据源数组
|
||||||
|
open var dataSource = [JXSegmentedBaseItemModel]()
|
||||||
|
/// cell的宽度。为JXSegmentedViewAutomaticDimension时就以内容计算的宽度为准,否则以itemWidth的具体值为准。
|
||||||
|
open var itemWidth: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 真实的item宽度 = itemWidth + itemWidthIncrement。
|
||||||
|
open var itemWidthIncrement: CGFloat = 0
|
||||||
|
/// item之前的间距
|
||||||
|
open var itemSpacing: CGFloat = 20
|
||||||
|
/// 当collectionView.contentSize.width小于JXSegmentedView的宽度时,是否将itemSpacing均分。
|
||||||
|
open var isItemSpacingAverageEnabled: Bool = true
|
||||||
|
/// item左右滚动过渡时,是否允许渐变。比如JXSegmentedTitleDataSource的titleZoom、titleNormalColor、titleStrokeWidth等渐变。
|
||||||
|
open var isItemTransitionEnabled: Bool = true
|
||||||
|
/// 选中的时候,是否需要动画过渡。自定义的cell需要自己处理动画过渡逻辑,动画处理逻辑参考`JXSegmentedTitleCell`
|
||||||
|
open var isSelectedAnimable: Bool = false
|
||||||
|
/// 选中动画的时长
|
||||||
|
open var selectedAnimationDuration: TimeInterval = 0.25
|
||||||
|
/// 是否允许item宽度缩放
|
||||||
|
open var isItemWidthZoomEnabled: Bool = false
|
||||||
|
/// 是否允许item宽度缩放动画
|
||||||
|
open var isItemWidthZoomAnimable: Bool = true
|
||||||
|
/// item宽度选中时的scale
|
||||||
|
open var itemWidthSelectedZoomScale: CGFloat = 1.5
|
||||||
|
|
||||||
|
@available(*, deprecated, renamed: "itemWidth")
|
||||||
|
open var itemContentWidth: CGFloat = JXSegmentedViewAutomaticDimension {
|
||||||
|
didSet {
|
||||||
|
itemWidth = itemContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var animator: JXSegmentedAnimator?
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
animator?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 配置完各种属性之后,需要手动调用该方法,更新数据源
|
||||||
|
///
|
||||||
|
/// - Parameter selectedIndex: 当前选中的index
|
||||||
|
open func reloadData(selectedIndex: Int) {
|
||||||
|
dataSource.removeAll()
|
||||||
|
for index in 0..<preferredItemCount() {
|
||||||
|
let itemModel = preferredItemModelInstance()
|
||||||
|
preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
dataSource.append(itemModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredItemCount() -> Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 子类需要重载该方法,用于返回自己定义的JXSegmentedBaseItemModel子类实例
|
||||||
|
open func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedBaseItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 子类需要重载该方法,用于返回索引为index的item宽度
|
||||||
|
open func preferredSegmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat {
|
||||||
|
return itemWidthIncrement
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 子类需要重载该方法,用于更新索引为index的itemModel
|
||||||
|
open func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
itemModel.index = index
|
||||||
|
itemModel.isItemTransitionEnabled = isItemTransitionEnabled
|
||||||
|
itemModel.isSelectedAnimable = isSelectedAnimable
|
||||||
|
itemModel.selectedAnimationDuration = selectedAnimationDuration
|
||||||
|
itemModel.isItemWidthZoomEnabled = isItemWidthZoomEnabled
|
||||||
|
itemModel.itemWidthNormalZoomScale = 1
|
||||||
|
itemModel.itemWidthSelectedZoomScale = itemWidthSelectedZoomScale
|
||||||
|
if index == selectedIndex {
|
||||||
|
itemModel.isSelected = true
|
||||||
|
itemModel.itemWidthCurrentZoomScale = itemModel.itemWidthSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
itemModel.isSelected = false
|
||||||
|
itemModel.itemWidthCurrentZoomScale = itemModel.itemWidthNormalZoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open func itemDataSource(in segmentedView: JXSegmentedView) -> [JXSegmentedBaseItemModel] {
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 自定义子类请继承方法`func preferredWidthForItem(at index: Int) -> CGFloat`
|
||||||
|
public final func segmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat {
|
||||||
|
return preferredSegmentedView(segmentedView, widthForItemAt: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat {
|
||||||
|
return self.segmentedView(segmentedView, widthForItemAt: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
return JXSegmentedBaseCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
currentSelectedItemModel.isSelected = false
|
||||||
|
willSelectedItemModel.isSelected = true
|
||||||
|
|
||||||
|
if isItemWidthZoomEnabled {
|
||||||
|
if (selectedType == .scroll && !isItemTransitionEnabled) ||
|
||||||
|
selectedType == .click ||
|
||||||
|
selectedType == .code {
|
||||||
|
animator = JXSegmentedAnimator()
|
||||||
|
animator?.duration = selectedAnimationDuration
|
||||||
|
animator?.progressClosure = {[weak self] (percent) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
currentSelectedItemModel.itemWidthCurrentZoomScale = JXSegmentedViewTool.interpolate(from: currentSelectedItemModel.itemWidthSelectedZoomScale, to: currentSelectedItemModel.itemWidthNormalZoomScale, percent: percent)
|
||||||
|
currentSelectedItemModel.itemWidth = self.itemWidthWithZoom(at: currentSelectedItemModel.index, model: currentSelectedItemModel)
|
||||||
|
willSelectedItemModel.itemWidthCurrentZoomScale = JXSegmentedViewTool.interpolate(from: willSelectedItemModel.itemWidthNormalZoomScale, to: willSelectedItemModel.itemWidthSelectedZoomScale, percent: percent)
|
||||||
|
willSelectedItemModel.itemWidth = self.itemWidthWithZoom(at: willSelectedItemModel.index, model: willSelectedItemModel)
|
||||||
|
segmentedView.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
}
|
||||||
|
if isItemWidthZoomAnimable {
|
||||||
|
animator?.start()
|
||||||
|
}else {
|
||||||
|
animator?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
currentSelectedItemModel.itemWidthCurrentZoomScale = currentSelectedItemModel.itemWidthNormalZoomScale
|
||||||
|
willSelectedItemModel.itemWidthCurrentZoomScale = willSelectedItemModel.itemWidthSelectedZoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat) {
|
||||||
|
//如果正在进行itemWidth缩放动画,用户又立马滚动了contentScrollView,需要停止动画。
|
||||||
|
animator?.stop()
|
||||||
|
animator = nil
|
||||||
|
if isItemWidthZoomEnabled && isItemTransitionEnabled {
|
||||||
|
//允许itemWidth缩放动画且允许item渐变过渡
|
||||||
|
leftItemModel.itemWidthCurrentZoomScale = JXSegmentedViewTool.interpolate(from: leftItemModel.itemWidthSelectedZoomScale, to: leftItemModel.itemWidthNormalZoomScale, percent: percent)
|
||||||
|
leftItemModel.itemWidth = itemWidthWithZoom(at: leftItemModel.index, model: leftItemModel)
|
||||||
|
rightItemModel.itemWidthCurrentZoomScale = JXSegmentedViewTool.interpolate(from: rightItemModel.itemWidthNormalZoomScale, to: rightItemModel.itemWidthSelectedZoomScale, percent: percent)
|
||||||
|
rightItemModel.itemWidth = itemWidthWithZoom(at: rightItemModel.index, model: rightItemModel)
|
||||||
|
segmentedView.collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 自定义子类请继承方法`func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)`
|
||||||
|
public final func refreshItemModel(_ segmentedView: JXSegmentedView, _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func itemWidthWithZoom(at index: Int, model: JXSegmentedBaseItemModel) -> CGFloat {
|
||||||
|
var width = self.segmentedView(JXSegmentedView(), widthForItemAt: index)
|
||||||
|
if isItemWidthZoomEnabled {
|
||||||
|
width *= model.itemWidthCurrentZoomScale
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseItemModel.swift
generated
Normal file
30
Pods/JXSegmentedView/Sources/Core/JXSegmentedBaseItemModel.swift
generated
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedBaseItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedBaseItemModel {
|
||||||
|
open var index: Int = 0
|
||||||
|
open var isSelected: Bool = false
|
||||||
|
open var itemWidth: CGFloat = 0
|
||||||
|
/// 指示器视图Frame转换到cell
|
||||||
|
open var indicatorConvertToItemFrame: CGRect = CGRect.zero
|
||||||
|
open var isItemTransitionEnabled: Bool = true
|
||||||
|
open var isSelectedAnimable: Bool = false
|
||||||
|
open var selectedAnimationDuration: TimeInterval = 0
|
||||||
|
/// 是否正在进行过渡动画
|
||||||
|
open var isTransitionAnimating: Bool = false
|
||||||
|
open var isItemWidthZoomEnabled: Bool = false
|
||||||
|
open var itemWidthNormalZoomScale: CGFloat = 0
|
||||||
|
open var itemWidthCurrentZoomScale: CGFloat = 0
|
||||||
|
open var itemWidthSelectedZoomScale: CGFloat = 0
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Pods/JXSegmentedView/Sources/Core/JXSegmentedCollectionView.swift
generated
Normal file
37
Pods/JXSegmentedView/Sources/Core/JXSegmentedCollectionView.swift
generated
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedCollectionView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedCollectionView: UICollectionView {
|
||||||
|
|
||||||
|
open var indicators = [JXSegmentedIndicatorProtocol]() {
|
||||||
|
willSet {
|
||||||
|
for indicator in indicators {
|
||||||
|
indicator.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
didSet {
|
||||||
|
for indicator in indicators {
|
||||||
|
addSubview(indicator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
for indicator in indicators {
|
||||||
|
sendSubviewToBack(indicator)
|
||||||
|
if let backgroundView = backgroundView {
|
||||||
|
sendSubviewToBack(backgroundView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
730
Pods/JXSegmentedView/Sources/Core/JXSegmentedView.swift
generated
Normal file
730
Pods/JXSegmentedView/Sources/Core/JXSegmentedView.swift
generated
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public let JXSegmentedViewAutomaticDimension: CGFloat = -1
|
||||||
|
|
||||||
|
/// 选中item时的类型
|
||||||
|
///
|
||||||
|
/// - unknown: 不是选中
|
||||||
|
/// - code: 通过代码调用方法`func selectItemAt(index: Int)`选中
|
||||||
|
/// - click: 通过点击item选中
|
||||||
|
/// - scroll: 通过滚动到item选中
|
||||||
|
public enum JXSegmentedViewItemSelectedType {
|
||||||
|
case unknown
|
||||||
|
case code
|
||||||
|
case click
|
||||||
|
case scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol JXSegmentedViewListContainer {
|
||||||
|
var defaultSelectedIndex: Int { set get }
|
||||||
|
func contentScrollView() -> UIScrollView
|
||||||
|
func reloadData()
|
||||||
|
func didClickSelectedItem(at index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol JXSegmentedViewDataSource: AnyObject {
|
||||||
|
var isItemWidthZoomEnabled: Bool { get }
|
||||||
|
var selectedAnimationDuration: TimeInterval { get }
|
||||||
|
var itemSpacing: CGFloat { get }
|
||||||
|
var isItemSpacingAverageEnabled: Bool { get }
|
||||||
|
|
||||||
|
func reloadData(selectedIndex: Int)
|
||||||
|
|
||||||
|
/// 返回数据源数组,数组元素必须是JXSegmentedBaseItemModel及其子类
|
||||||
|
///
|
||||||
|
/// - Parameter segmentedView: JXSegmentedView
|
||||||
|
/// - Returns: 数据源数组
|
||||||
|
func itemDataSource(in segmentedView: JXSegmentedView) -> [JXSegmentedBaseItemModel]
|
||||||
|
|
||||||
|
/// 返回index对应item的宽度,等同于cell的宽度。
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 目标index
|
||||||
|
/// - Returns: item的宽度
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat
|
||||||
|
|
||||||
|
/// 返回index对应item的content宽度,等同于cell上面内容的宽度。与上面的代理方法不同,需要注意辨别。部分使用场景下,cell的宽度比较大,但是内容的宽度比较小。这个时候指示器又需要和item的content等宽。所以,添加了此代理方法。
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 目标index
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat
|
||||||
|
|
||||||
|
/// 注册cell class
|
||||||
|
///
|
||||||
|
/// - Parameter segmentedView: JXSegmentedView
|
||||||
|
func registerCellClass(in segmentedView: JXSegmentedView)
|
||||||
|
|
||||||
|
/// 返回index对应的cell
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 目标index
|
||||||
|
/// - Returns: JXSegmentedBaseCell及其子类
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell
|
||||||
|
|
||||||
|
/// 根据当前选中的selectedIndex,刷新目标index的itemModel
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - itemModel: JXSegmentedBaseItemModel
|
||||||
|
/// - index: 目标index
|
||||||
|
/// - selectedIndex: 当前选中的index
|
||||||
|
func refreshItemModel(_ segmentedView: JXSegmentedView, _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)
|
||||||
|
|
||||||
|
/// item选中的时候调用。当前选中的currentSelectedItemModel状态需要更新为未选中;将要选中的willSelectedItemModel状态需要更新为选中。
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - currentSelectedItemModel: 当前选中的itemModel
|
||||||
|
/// - willSelectedItemModel: 将要选中的itemModel
|
||||||
|
/// - selectedType: 选中的类型
|
||||||
|
func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType)
|
||||||
|
|
||||||
|
/// 左右滚动过渡时调用。根据当前的从左到右的百分比,刷新leftItemModel和rightItemModel
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - leftItemModel: 相对位置在左边的itemModel
|
||||||
|
/// - rightItemModel: 相对位置在右边的itemModel
|
||||||
|
/// - percent: 从左到右的百分比
|
||||||
|
func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 为什么会把选中代理分为三个,因为有时候只关心点击选中的,有时候只关心滚动选中的,有时候只关心选中。所以具体情况,使用对应方法。
|
||||||
|
public protocol JXSegmentedViewDelegate: AnyObject {
|
||||||
|
/// 点击选中或者滚动选中都会调用该方法。适用于只关心选中事件,而不关心具体是点击还是滚动选中的情况。
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 选中的index
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int)
|
||||||
|
|
||||||
|
/// 点击选中的情况才会调用该方法
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 选中的index
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int)
|
||||||
|
|
||||||
|
/// 滚动选中的情况才会调用该方法
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 选中的index
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int)
|
||||||
|
|
||||||
|
/// 正在滚动中的回调
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - leftIndex: 正在滚动中,相对位置处于左边的index
|
||||||
|
/// - rightIndex: 正在滚动中,相对位置处于右边的index
|
||||||
|
/// - percent: 从左往右计算的百分比
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat)
|
||||||
|
|
||||||
|
|
||||||
|
/// 是否允许点击选中目标index的item
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - segmentedView: JXSegmentedView
|
||||||
|
/// - index: 目标index
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提供JXSegmentedViewDelegate的默认实现,这样对于遵从JXSegmentedViewDelegate的类来说,所有代理方法都是可选实现的。
|
||||||
|
public extension JXSegmentedViewDelegate {
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { }
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int) { }
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int) { }
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat) { }
|
||||||
|
func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool { return true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部会自己找到父UIViewController,然后将其automaticallyAdjustsScrollViewInsets设置为false,这一点请知晓。
|
||||||
|
open class JXSegmentedView: UIView, JXSegmentedViewRTLCompatible {
|
||||||
|
open weak var dataSource: JXSegmentedViewDataSource? {
|
||||||
|
didSet {
|
||||||
|
dataSource?.reloadData(selectedIndex: selectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open weak var delegate: JXSegmentedViewDelegate?
|
||||||
|
open private(set) var collectionView: JXSegmentedCollectionView!
|
||||||
|
open var contentScrollView: UIScrollView? {
|
||||||
|
willSet {
|
||||||
|
contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
|
||||||
|
}
|
||||||
|
didSet {
|
||||||
|
contentScrollView?.scrollsToTop = false
|
||||||
|
contentScrollView?.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public var listContainer: JXSegmentedViewListContainer? = nil {
|
||||||
|
didSet {
|
||||||
|
listContainer?.defaultSelectedIndex = defaultSelectedIndex
|
||||||
|
contentScrollView = listContainer?.contentScrollView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// indicators的元素必须是遵从JXSegmentedIndicatorProtocol协议的UIView及其子类
|
||||||
|
open var indicators = [JXSegmentedIndicatorProtocol]() {
|
||||||
|
didSet {
|
||||||
|
collectionView.indicators = indicators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 初始化或者reloadData之前设置,用于指定默认的index
|
||||||
|
open var defaultSelectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
selectedIndex = defaultSelectedIndex
|
||||||
|
if listContainer != nil {
|
||||||
|
listContainer?.defaultSelectedIndex = defaultSelectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open private(set) var selectedIndex: Int = 0
|
||||||
|
/// 整体内容的左边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
|
||||||
|
open var contentEdgeInsetLeft: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 整体内容的右边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
|
||||||
|
open var contentEdgeInsetRight: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 点击切换的时候,contentScrollView的切换是否需要动画
|
||||||
|
open var isContentScrollViewClickTransitionAnimationEnabled: Bool = true
|
||||||
|
|
||||||
|
private var itemDataSource = [JXSegmentedBaseItemModel]()
|
||||||
|
private var innerItemSpacing: CGFloat = 0
|
||||||
|
private var lastContentOffset: CGPoint = CGPoint.zero
|
||||||
|
/// 正在滚动中的目标index。用于处理正在滚动列表的时候,立即点击item,会导致界面显示异常。
|
||||||
|
private var scrollingTargetIndex: Int = -1
|
||||||
|
private var isFirstLayoutSubviews = true
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
|
||||||
|
}
|
||||||
|
|
||||||
|
public override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commonInit() {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
collectionView = JXSegmentedCollectionView(frame: CGRect.zero, collectionViewLayout: layout)
|
||||||
|
collectionView.backgroundColor = .clear
|
||||||
|
collectionView.showsVerticalScrollIndicator = false
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.scrollsToTop = false
|
||||||
|
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "JXSegmentedViewInnerEmptyCell")
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.delegate = self
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
collectionView.isPrefetchingEnabled = false
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
collectionView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
if segmentedViewShouldRTLLayout() {
|
||||||
|
collectionView.semanticContentAttribute = .forceLeftToRight
|
||||||
|
segmentedView(horizontalFlipForView: collectionView)
|
||||||
|
}
|
||||||
|
addSubview(collectionView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func willMove(toSuperview newSuperview: UIView?) {
|
||||||
|
super.willMove(toSuperview: newSuperview)
|
||||||
|
|
||||||
|
var nextResponder: UIResponder? = newSuperview
|
||||||
|
while nextResponder != nil {
|
||||||
|
if let parentVC = nextResponder as? UIViewController {
|
||||||
|
parentVC.automaticallyAdjustsScrollViewInsets = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextResponder = nextResponder?.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
//部分使用者为了适配不同的手机屏幕尺寸,JXSegmentedView的宽高比要求保持一样。所以它的高度就会因为不同宽度的屏幕而不一样。计算出来的高度,有时候会是位数很长的浮点数,如果把这个高度设置给UICollectionView就会触发内部的一个错误。所以,为了规避这个问题,在这里对高度统一向下取整。
|
||||||
|
//如果向下取整导致了你的页面异常,请自己重新设置JXSegmentedView的高度,保证为整数即可。
|
||||||
|
let targetFrame = CGRect(x: 0, y: 0, width: bounds.size.width, height: floor(bounds.size.height))
|
||||||
|
if isFirstLayoutSubviews {
|
||||||
|
isFirstLayoutSubviews = false
|
||||||
|
collectionView.frame = targetFrame
|
||||||
|
reloadDataWithoutListContainer()
|
||||||
|
}else {
|
||||||
|
if collectionView.frame != targetFrame {
|
||||||
|
collectionView.frame = targetFrame
|
||||||
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Public
|
||||||
|
public final func dequeueReusableCell(withReuseIdentifier identifier: String, at index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let indexPath = IndexPath(item: index, section: 0)
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
|
||||||
|
guard cell.isKind(of: JXSegmentedBaseCell.self) else {
|
||||||
|
fatalError("Cell class must be subclass of JXSegmentedBaseCell")
|
||||||
|
}
|
||||||
|
return cell as! JXSegmentedBaseCell
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadData() {
|
||||||
|
reloadDataWithoutListContainer()
|
||||||
|
listContainer?.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadDataWithoutListContainer() {
|
||||||
|
dataSource?.reloadData(selectedIndex: selectedIndex)
|
||||||
|
dataSource?.registerCellClass(in: self)
|
||||||
|
if let itemSource = dataSource?.itemDataSource(in: self) {
|
||||||
|
itemDataSource = itemSource
|
||||||
|
}
|
||||||
|
if selectedIndex < 0 || selectedIndex >= itemDataSource.count {
|
||||||
|
defaultSelectedIndex = 0
|
||||||
|
selectedIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
innerItemSpacing = dataSource?.itemSpacing ?? 0
|
||||||
|
var totalItemWidth: CGFloat = 0
|
||||||
|
var totalContentWidth: CGFloat = getContentEdgeInsetLeft()
|
||||||
|
for (index, itemModel) in itemDataSource.enumerated() {
|
||||||
|
itemModel.index = index
|
||||||
|
itemModel.itemWidth = (dataSource?.segmentedView(self, widthForItemAt: index) ?? 0)
|
||||||
|
if dataSource?.isItemWidthZoomEnabled == true {
|
||||||
|
itemModel.itemWidth *= itemModel.itemWidthCurrentZoomScale
|
||||||
|
}
|
||||||
|
itemModel.isSelected = (index == selectedIndex)
|
||||||
|
totalItemWidth += itemModel.itemWidth
|
||||||
|
if index == itemDataSource.count - 1 {
|
||||||
|
totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
|
||||||
|
}else {
|
||||||
|
totalContentWidth += itemModel.itemWidth + innerItemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataSource?.isItemSpacingAverageEnabled == true && totalContentWidth < bounds.size.width {
|
||||||
|
var itemSpacingCount = itemDataSource.count - 1
|
||||||
|
var totalItemSpacingWidth = bounds.size.width - totalItemWidth
|
||||||
|
if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
|
||||||
|
itemSpacingCount += 1
|
||||||
|
}else {
|
||||||
|
totalItemSpacingWidth -= contentEdgeInsetLeft
|
||||||
|
}
|
||||||
|
if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
|
||||||
|
itemSpacingCount += 1
|
||||||
|
}else {
|
||||||
|
totalItemSpacingWidth -= contentEdgeInsetRight
|
||||||
|
}
|
||||||
|
if itemSpacingCount > 0 {
|
||||||
|
innerItemSpacing = totalItemSpacingWidth / CGFloat(itemSpacingCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedItemFrameX = innerItemSpacing
|
||||||
|
var selectedItemWidth: CGFloat = 0
|
||||||
|
totalContentWidth = getContentEdgeInsetLeft()
|
||||||
|
for (index, itemModel) in itemDataSource.enumerated() {
|
||||||
|
if index < selectedIndex {
|
||||||
|
selectedItemFrameX += itemModel.itemWidth + innerItemSpacing
|
||||||
|
}else if index == selectedIndex {
|
||||||
|
selectedItemWidth = itemModel.itemWidth
|
||||||
|
}
|
||||||
|
if index == itemDataSource.count - 1 {
|
||||||
|
totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
|
||||||
|
}else {
|
||||||
|
totalContentWidth += itemModel.itemWidth + innerItemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let minX: CGFloat = 0
|
||||||
|
let maxX = totalContentWidth - bounds.size.width
|
||||||
|
let targetX = selectedItemFrameX - bounds.size.width/2 + selectedItemWidth/2
|
||||||
|
collectionView.setContentOffset(CGPoint(x: max(min(maxX, targetX), minX), y: 0), animated: false)
|
||||||
|
|
||||||
|
if contentScrollView != nil {
|
||||||
|
if contentScrollView!.frame.equalTo(CGRect.zero) &&
|
||||||
|
contentScrollView!.superview != nil {
|
||||||
|
//某些情况系统会出现JXSegmentedView先布局,contentScrollView后布局。就会导致下面指定defaultSelectedIndex失效,所以发现contentScrollView的frame为zero时,强行触发其父视图链里面已经有frame的一个父视图的layoutSubviews方法。
|
||||||
|
//比如JXSegmentedListContainerView会将contentScrollView包裹起来使用,该情况需要JXSegmentedListContainerView.superView触发布局更新
|
||||||
|
var parentView = contentScrollView?.superview
|
||||||
|
while parentView != nil && parentView?.frame.equalTo(CGRect.zero) == true {
|
||||||
|
parentView = parentView?.superview
|
||||||
|
}
|
||||||
|
parentView?.setNeedsLayout()
|
||||||
|
parentView?.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentScrollView!.setContentOffset(CGPoint(x: CGFloat(selectedIndex) * contentScrollView!.bounds.size.width
|
||||||
|
, y: 0), animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for indicator in indicators {
|
||||||
|
if itemDataSource.isEmpty {
|
||||||
|
indicator.isHidden = true
|
||||||
|
}else {
|
||||||
|
indicator.isHidden = false
|
||||||
|
let selectedItemFrame = getItemFrameAt(index: selectedIndex)
|
||||||
|
let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
|
||||||
|
currentSelectedItemFrame: selectedItemFrame,
|
||||||
|
selectedType: .unknown,
|
||||||
|
currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
|
||||||
|
collectionViewContentSize: CGSize(width: totalContentWidth, height: bounds.size.height))
|
||||||
|
indicator.refreshIndicatorState(model: indicatorParams)
|
||||||
|
|
||||||
|
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||||||
|
var indicatorConvertToItemFrame = indicator.frame
|
||||||
|
indicatorConvertToItemFrame.origin.x -= selectedItemFrame.origin.x
|
||||||
|
itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectionView.reloadData()
|
||||||
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadItem(at index: Int) {
|
||||||
|
guard index >= 0 && index < itemDataSource.count else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource?.refreshItemModel(self, itemDataSource[index], at: index, selectedIndex: selectedIndex)
|
||||||
|
let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
cell?.reloadData(itemModel: itemDataSource[index], selectedType: .unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 代码选中指定index
|
||||||
|
/// 如果要同时触发列表容器对应index的列表加载,请再调用`listContainerView.didClickSelectedItem(at: index)`方法
|
||||||
|
///
|
||||||
|
/// - Parameter index: 目标index
|
||||||
|
open func selectItemAt(index: Int) {
|
||||||
|
selectItemAt(index: index, selectedType: .code)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - KVO
|
||||||
|
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "contentOffset" {
|
||||||
|
let contentOffset = change?[NSKeyValueChangeKey.newKey] as! CGPoint
|
||||||
|
if contentScrollView?.isTracking == true || contentScrollView?.isDecelerating == true {
|
||||||
|
//用户滚动引起的contentOffset变化,才处理。
|
||||||
|
if contentScrollView?.bounds.size.width == 0 {
|
||||||
|
// 如果contentScrollView Frame为零,直接忽略
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var progress = contentOffset.x/contentScrollView!.bounds.size.width
|
||||||
|
if Int(progress) > itemDataSource.count - 1 || progress < 0 {
|
||||||
|
//超过了边界,不需要处理
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if contentOffset.x == 0 && selectedIndex == 0 && lastContentOffset.x == 0 {
|
||||||
|
//滚动到了最左边,且已经选中了第一个,且之前的contentOffset.x为0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let maxContentOffsetX = contentScrollView!.contentSize.width - contentScrollView!.bounds.size.width
|
||||||
|
if contentOffset.x == maxContentOffsetX && selectedIndex == itemDataSource.count - 1 && lastContentOffset.x == maxContentOffsetX {
|
||||||
|
//滚动到了最右边,且已经选中了最后一个,且之前的contentOffset.x为maxContentOffsetX
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progress = max(0, min(CGFloat(itemDataSource.count - 1), progress))
|
||||||
|
let baseIndex = Int(floor(progress))
|
||||||
|
let remainderProgress = progress - CGFloat(baseIndex)
|
||||||
|
|
||||||
|
let leftItemFrame = getItemFrameAt(index: baseIndex)
|
||||||
|
let rightItemFrame = getItemFrameAt(index: baseIndex + 1)
|
||||||
|
var rightItemContentWidth: CGFloat = 0
|
||||||
|
if baseIndex + 1 < itemDataSource.count {
|
||||||
|
rightItemContentWidth = dataSource?.segmentedView(self, widthForItemContentAt: baseIndex + 1) ?? 0
|
||||||
|
}
|
||||||
|
let indicatorParams = JXSegmentedIndicatorTransitionParams(currentSelectedIndex: selectedIndex,
|
||||||
|
leftIndex: baseIndex,
|
||||||
|
leftItemFrame: leftItemFrame,
|
||||||
|
leftItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: baseIndex) ?? 0,
|
||||||
|
rightIndex: baseIndex + 1,
|
||||||
|
rightItemFrame: rightItemFrame,
|
||||||
|
rightItemContentWidth: rightItemContentWidth,
|
||||||
|
percent: remainderProgress)
|
||||||
|
|
||||||
|
if remainderProgress == 0 {
|
||||||
|
//滑动翻页,需要更新选中状态
|
||||||
|
//滑动一小段距离,然后放开回到原位,contentOffset同样的值会回调多次。例如在index为1的情况,滑动放开回到原位,contentOffset会多次回调CGPoint(width, 0)
|
||||||
|
if !(lastContentOffset.x == contentOffset.x && selectedIndex == baseIndex) {
|
||||||
|
scrollSelectItemAt(index: baseIndex)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
//快速滑动翻页,当remainderRatio没有变成0,但是已经翻页了,需要通过下面的判断,触发选中
|
||||||
|
if abs(progress - CGFloat(selectedIndex)) > 1 {
|
||||||
|
var targetIndex = baseIndex
|
||||||
|
if progress < CGFloat(selectedIndex) {
|
||||||
|
targetIndex = baseIndex + 1
|
||||||
|
}
|
||||||
|
scrollSelectItemAt(index: targetIndex)
|
||||||
|
}
|
||||||
|
if selectedIndex == baseIndex {
|
||||||
|
scrollingTargetIndex = baseIndex + 1
|
||||||
|
}else {
|
||||||
|
scrollingTargetIndex = baseIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource?.refreshItemModel(self, leftItemModel: itemDataSource[baseIndex], rightItemModel: itemDataSource[baseIndex + 1], percent: remainderProgress)
|
||||||
|
|
||||||
|
for indicator in indicators {
|
||||||
|
indicator.contentScrollViewDidScroll(model: indicatorParams)
|
||||||
|
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||||||
|
var leftIndicatorConvertToItemFrame = indicator.frame
|
||||||
|
leftIndicatorConvertToItemFrame.origin.x -= leftItemFrame.origin.x
|
||||||
|
itemDataSource[baseIndex].indicatorConvertToItemFrame = leftIndicatorConvertToItemFrame
|
||||||
|
|
||||||
|
var rightIndicatorConvertToItemFrame = indicator.frame
|
||||||
|
rightIndicatorConvertToItemFrame.origin.x -= rightItemFrame.origin.x
|
||||||
|
itemDataSource[baseIndex + 1].indicatorConvertToItemFrame = rightIndicatorConvertToItemFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftCell = collectionView.cellForItem(at: IndexPath(item: baseIndex, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
leftCell?.reloadData(itemModel: itemDataSource[baseIndex], selectedType: .unknown)
|
||||||
|
|
||||||
|
let rightCell = collectionView.cellForItem(at: IndexPath(item: baseIndex + 1, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
rightCell?.reloadData(itemModel: itemDataSource[baseIndex + 1], selectedType: .unknown)
|
||||||
|
|
||||||
|
delegate?.segmentedView(self, scrollingFrom: baseIndex, to: baseIndex + 1, percent: remainderProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastContentOffset = contentOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
private func clickSelectItemAt(index: Int) {
|
||||||
|
guard delegate?.segmentedView(self, canClickItemAt: index) != false else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectItemAt(index: index, selectedType: .click)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollSelectItemAt(index: Int) {
|
||||||
|
selectItemAt(index: index, selectedType: .scroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectItemAt(index: Int, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
guard index >= 0 && index < itemDataSource.count else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == selectedIndex {
|
||||||
|
if selectedType == .code {
|
||||||
|
listContainer?.didClickSelectedItem(at: index)
|
||||||
|
}else if selectedType == .click {
|
||||||
|
delegate?.segmentedView(self, didClickSelectedItemAt: index)
|
||||||
|
listContainer?.didClickSelectedItem(at: index)
|
||||||
|
}else if selectedType == .scroll {
|
||||||
|
delegate?.segmentedView(self, didScrollSelectedItemAt: index)
|
||||||
|
}
|
||||||
|
delegate?.segmentedView(self, didSelectedItemAt: index)
|
||||||
|
scrollingTargetIndex = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSelectedItemModel = itemDataSource[selectedIndex]
|
||||||
|
let willSelectedItemModel = itemDataSource[index]
|
||||||
|
dataSource?.refreshItemModel(self, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
let currentSelectedCell = collectionView.cellForItem(at: IndexPath(item: selectedIndex, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
currentSelectedCell?.reloadData(itemModel: currentSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
let willSelectedCell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
if scrollingTargetIndex != -1 && scrollingTargetIndex != index {
|
||||||
|
let scrollingTargetItemModel = itemDataSource[scrollingTargetIndex]
|
||||||
|
scrollingTargetItemModel.isSelected = false
|
||||||
|
dataSource?.refreshItemModel(self, currentSelectedItemModel: scrollingTargetItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
let scrollingTargetCell = collectionView.cellForItem(at: IndexPath(item: scrollingTargetIndex, section: 0)) as? JXSegmentedBaseCell
|
||||||
|
scrollingTargetCell?.reloadData(itemModel: scrollingTargetItemModel, selectedType: selectedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataSource?.isItemWidthZoomEnabled == true {
|
||||||
|
if selectedType == .click || selectedType == .code {
|
||||||
|
//延时为了解决cellwidth变化,点击最后几个cell,scrollToItem会出现位置偏移bu。需要等cellWidth动画渐变结束后再滚动到index的cell位置。
|
||||||
|
let selectedAnimationDurationInMilliseconds = Int((dataSource?.selectedAnimationDuration ?? 0)*1000)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(selectedAnimationDurationInMilliseconds)) {
|
||||||
|
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||||||
|
}
|
||||||
|
}else if selectedType == .scroll {
|
||||||
|
//滚动选中的直接处理
|
||||||
|
collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentScrollView != nil && (selectedType == .click || selectedType == .code) {
|
||||||
|
contentScrollView!.setContentOffset(CGPoint(x: contentScrollView!.bounds.size.width*CGFloat(index), y: 0), animated: isContentScrollViewClickTransitionAnimationEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex = index
|
||||||
|
|
||||||
|
let currentSelectedItemFrame = getSelectedItemFrameAt(index: selectedIndex)
|
||||||
|
for indicator in indicators {
|
||||||
|
let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
|
||||||
|
currentSelectedItemFrame: currentSelectedItemFrame,
|
||||||
|
selectedType: selectedType,
|
||||||
|
currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
|
||||||
|
collectionViewContentSize: nil)
|
||||||
|
indicator.selectItem(model: indicatorParams)
|
||||||
|
|
||||||
|
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||||||
|
var indicatorConvertToItemFrame = indicator.frame
|
||||||
|
indicatorConvertToItemFrame.origin.x -= currentSelectedItemFrame.origin.x
|
||||||
|
itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
|
||||||
|
willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollingTargetIndex = -1
|
||||||
|
if selectedType == .code {
|
||||||
|
listContainer?.didClickSelectedItem(at: index)
|
||||||
|
}else if selectedType == .click {
|
||||||
|
delegate?.segmentedView(self, didClickSelectedItemAt: index)
|
||||||
|
listContainer?.didClickSelectedItem(at: index)
|
||||||
|
}else if selectedType == .scroll {
|
||||||
|
delegate?.segmentedView(self, didScrollSelectedItemAt: index)
|
||||||
|
}
|
||||||
|
delegate?.segmentedView(self, didSelectedItemAt: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getItemFrameAt(index: Int) -> CGRect {
|
||||||
|
guard index < itemDataSource.count else {
|
||||||
|
return CGRect.zero
|
||||||
|
}
|
||||||
|
var x = getContentEdgeInsetLeft()
|
||||||
|
for i in 0..<index {
|
||||||
|
let itemModel = itemDataSource[i]
|
||||||
|
var itemWidth: CGFloat = 0
|
||||||
|
if itemModel.isTransitionAnimating && itemModel.isItemWidthZoomEnabled {
|
||||||
|
//正在进行动画的时候,itemWidthCurrentZoomScale是随着动画渐变的,而没有立即更新到目标值
|
||||||
|
if itemModel.isSelected {
|
||||||
|
itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthNormalZoomScale
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
itemWidth = itemModel.itemWidth
|
||||||
|
}
|
||||||
|
x += itemWidth + innerItemSpacing
|
||||||
|
}
|
||||||
|
var width: CGFloat = 0
|
||||||
|
let selectedItemModel = itemDataSource[index]
|
||||||
|
if selectedItemModel.isTransitionAnimating && selectedItemModel.isItemWidthZoomEnabled {
|
||||||
|
width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
width = selectedItemModel.itemWidth
|
||||||
|
}
|
||||||
|
return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSelectedItemFrameAt(index: Int) -> CGRect {
|
||||||
|
guard index < itemDataSource.count else {
|
||||||
|
return CGRect.zero
|
||||||
|
}
|
||||||
|
var x = getContentEdgeInsetLeft()
|
||||||
|
for i in 0..<index {
|
||||||
|
let itemWidth = (dataSource?.segmentedView(self, widthForItemAt: i) ?? 0)
|
||||||
|
x += itemWidth + innerItemSpacing
|
||||||
|
}
|
||||||
|
var width: CGFloat = 0
|
||||||
|
let selectedItemModel = itemDataSource[index]
|
||||||
|
if selectedItemModel.isItemWidthZoomEnabled {
|
||||||
|
width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
width = selectedItemModel.itemWidth
|
||||||
|
}
|
||||||
|
return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getContentEdgeInsetLeft() -> CGFloat {
|
||||||
|
if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
|
||||||
|
return innerItemSpacing
|
||||||
|
}else {
|
||||||
|
return contentEdgeInsetLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getContentEdgeInsetRight() -> CGFloat {
|
||||||
|
if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
|
||||||
|
return innerItemSpacing
|
||||||
|
}else {
|
||||||
|
return contentEdgeInsetRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXSegmentedView: UICollectionViewDataSource {
|
||||||
|
public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return itemDataSource.count
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
if let cell = dataSource?.segmentedView(self, cellForItemAt: indexPath.item) {
|
||||||
|
cell.reloadData(itemModel: itemDataSource[indexPath.item], selectedType: .unknown)
|
||||||
|
return cell
|
||||||
|
}else {
|
||||||
|
return collectionView.dequeueReusableCell(withReuseIdentifier: "JXSegmentedViewInnerEmptyCell", for: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXSegmentedView: UICollectionViewDelegate {
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
var isTransitionAnimating = false
|
||||||
|
for itemModel in itemDataSource {
|
||||||
|
if itemModel.isTransitionAnimating {
|
||||||
|
isTransitionAnimating = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isTransitionAnimating {
|
||||||
|
//当前没有正在过渡的item,才允许点击选中
|
||||||
|
clickSelectItemAt(index: indexPath.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXSegmentedView: UICollectionViewDelegateFlowLayout {
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||||
|
return UIEdgeInsets(top: 0, left: getContentEdgeInsetLeft(), bottom: 0, right: getContentEdgeInsetRight())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
|
if indexPath.item >= 0, indexPath.item < itemDataSource.count {
|
||||||
|
return CGSize(width: itemDataSource[indexPath.item].itemWidth, height: collectionView.bounds.size.height)
|
||||||
|
} else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||||
|
return innerItemSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||||
|
return innerItemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotCell.swift
generated
Normal file
42
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotCell.swift
generated
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedDotCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedDotCell: JXSegmentedTitleCell {
|
||||||
|
open var dotView = UIView()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
contentView.addSubview(dotView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedDotItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dotView.center = CGPoint(x: titleLabel.frame.maxX + myItemModel.dotOffset.x, y: titleLabel.frame.minY + myItemModel.dotOffset.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedDotItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dotView.backgroundColor = myItemModel.dotColor
|
||||||
|
dotView.bounds = CGRect(x: 0, y: 0, width: myItemModel.dotSize.width, height: myItemModel.dotSize.height)
|
||||||
|
dotView.isHidden = !myItemModel.dotState
|
||||||
|
dotView.layer.cornerRadius = myItemModel.dotCornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotDataSource.swift
generated
Normal file
54
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotDataSource.swift
generated
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedDotDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedDotDataSource: JXSegmentedTitleDataSource {
|
||||||
|
/// 数量需要和titles一致,控制红点是否显示
|
||||||
|
open var dotStates = [Bool]()
|
||||||
|
/// 红点的size
|
||||||
|
open var dotSize = CGSize(width: 10, height: 10)
|
||||||
|
/// 红点的圆角值,JXSegmentedViewAutomaticDimension等于dotSize.height/2
|
||||||
|
open var dotCornerRadius: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 红点的颜色
|
||||||
|
open var dotColor = UIColor.red
|
||||||
|
/// dotView的默认位置是center在titleLabel的右上角,可以通过dotOffset控制X、Y轴的偏移
|
||||||
|
open var dotOffset: CGPoint = CGPoint.zero
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedDotItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let itemModel = itemModel as? JXSegmentedDotItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModel.dotOffset = dotOffset
|
||||||
|
itemModel.dotState = dotStates[index]
|
||||||
|
itemModel.dotColor = dotColor
|
||||||
|
itemModel.dotSize = dotSize
|
||||||
|
if dotCornerRadius == JXSegmentedViewAutomaticDimension {
|
||||||
|
itemModel.dotCornerRadius = dotSize.height/2
|
||||||
|
}else {
|
||||||
|
itemModel.dotCornerRadius = dotCornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedDotCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotItemModel.swift
generated
Normal file
17
Pods/JXSegmentedView/Sources/Dot/JXSegmentedDotItemModel.swift
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedDotItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedDotItemModel: JXSegmentedTitleItemModel {
|
||||||
|
open var dotState = false
|
||||||
|
open var dotSize = CGSize.zero
|
||||||
|
open var dotCornerRadius: CGFloat = 0
|
||||||
|
open var dotColor = UIColor.red
|
||||||
|
open var dotOffset: CGPoint = CGPoint.zero
|
||||||
|
}
|
||||||
19
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedComponetGradientView.swift
generated
Normal file
19
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedComponetGradientView.swift
generated
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorGradientView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/2.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedComponetGradientView: UIView {
|
||||||
|
open class override var layerClass: AnyClass {
|
||||||
|
return CAGradientLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
open var gradientLayer: CAGradientLayer {
|
||||||
|
return layer as! CAGradientLayer
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorBackgroundView.swift
generated
Normal file
92
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorBackgroundView.swift
generated
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorBackgroundView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 不支持indicatorPosition、verticalOffset。默认垂直居中。
|
||||||
|
open class JXSegmentedIndicatorBackgroundView: JXSegmentedIndicatorBaseView {
|
||||||
|
@available(*, deprecated, renamed: "indicatorWidthIncrement")
|
||||||
|
open var backgroundWidthIncrement: CGFloat = 20 {
|
||||||
|
didSet {
|
||||||
|
indicatorWidthIncrement = backgroundWidthIncrement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorWidthIncrement = 20
|
||||||
|
indicatorHeight = 26
|
||||||
|
indicatorColor = .lightGray
|
||||||
|
indicatorPosition = .center
|
||||||
|
verticalOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = indicatorColor
|
||||||
|
layer.cornerRadius = getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
var targetWidth = getIndicatorWidth(itemFrame: leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
|
||||||
|
let leftWidth = targetWidth
|
||||||
|
let rightWidth = getIndicatorWidth(itemFrame: rightItemFrame, itemContentWidth: model.rightItemContentWidth)
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - leftWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - rightWidth)/2
|
||||||
|
let targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat(percent))
|
||||||
|
if indicatorWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: leftWidth, to: rightWidth, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frame.origin.x = targetX
|
||||||
|
self.frame.size.width = targetWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = self.frame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
toFrame.size.width = width
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
UIView.animate(withDuration: scrollAnimationDuration, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
self.frame = toFrame
|
||||||
|
}) { (_) in
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
frame = toFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorBaseView.swift
generated
Normal file
108
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorBaseView.swift
generated
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorBaseView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum JXSegmentedIndicatorPosition {
|
||||||
|
case top
|
||||||
|
case bottom
|
||||||
|
case center
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorBaseView: UIView, JXSegmentedIndicatorProtocol {
|
||||||
|
/// 默认JXSegmentedViewAutomaticDimension(与cell的宽度相等)。内部通过getIndicatorWidth方法获取实际的值
|
||||||
|
open var indicatorWidth: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
open var indicatorWidthIncrement: CGFloat = 0 //指示器的宽度增量。比如需求是指示器宽度比cell宽度多10 point。就可以将该属性赋值为10。最终指示器的宽度=indicatorWidth+indicatorWidthIncrement
|
||||||
|
/// 默认JXSegmentedViewAutomaticDimension(与cell的高度相等)。内部通过getIndicatorHeight方法获取实际的值
|
||||||
|
open var indicatorHeight: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 默认JXSegmentedViewAutomaticDimension (等于indicatorHeight/2)。内部通过getIndicatorCornerRadius方法获取实际的值
|
||||||
|
open var indicatorCornerRadius: CGFloat = JXSegmentedViewAutomaticDimension
|
||||||
|
/// 指示器的颜色
|
||||||
|
open var indicatorColor: UIColor = .red
|
||||||
|
/// 指示器的位置,top、bottom、center
|
||||||
|
open var indicatorPosition: JXSegmentedIndicatorPosition = .bottom
|
||||||
|
/// 垂直方向偏移,指示器默认贴着底部或者顶部,verticalOffset越大越靠近中心。
|
||||||
|
open var verticalOffset: CGFloat = 0
|
||||||
|
/// 手势滚动、点击切换的时候,是否允许滚动。
|
||||||
|
open var isScrollEnabled: Bool = true
|
||||||
|
/// 是否需要将当前的indicator的frame转换到cell。辅助JXSegmentedTitleDataSourced的isTitleMaskEnabled属性使用。
|
||||||
|
/// 如果添加了多个indicator,仅能有一个indicator的isIndicatorConvertToItemFrameEnabled为true。
|
||||||
|
/// 如果有多个indicator的isIndicatorConvertToItemFrameEnabled为true,则以最后一个isIndicatorConvertToItemFrameEnabled为true的indicator为准。
|
||||||
|
open var isIndicatorConvertToItemFrameEnabled: Bool = true
|
||||||
|
/// 点击选中时的滚动动画时长
|
||||||
|
open var scrollAnimationDuration: TimeInterval = 0.25
|
||||||
|
/// 指示器的宽度是否跟随item的内容变化(而不是跟着cell的宽度变化)。indicatorWidth=JXSegmentedViewAutomaticDimension才能生效
|
||||||
|
open var isIndicatorWidthSameAsItemContent = false
|
||||||
|
|
||||||
|
public override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func commonInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getIndicatorCornerRadius(itemFrame: CGRect) -> CGFloat {
|
||||||
|
if indicatorCornerRadius == JXSegmentedViewAutomaticDimension {
|
||||||
|
return getIndicatorHeight(itemFrame: itemFrame)/2
|
||||||
|
}
|
||||||
|
return indicatorCornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getIndicatorWidth(itemFrame: CGRect, itemContentWidth: CGFloat) -> CGFloat {
|
||||||
|
if indicatorWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
if isIndicatorWidthSameAsItemContent {
|
||||||
|
return itemContentWidth + indicatorWidthIncrement
|
||||||
|
}else {
|
||||||
|
return itemFrame.size.width + indicatorWidthIncrement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indicatorWidth + indicatorWidthIncrement
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getIndicatorHeight(itemFrame: CGRect) -> CGFloat {
|
||||||
|
if indicatorHeight == JXSegmentedViewAutomaticDimension {
|
||||||
|
return itemFrame.size.height
|
||||||
|
}
|
||||||
|
return indicatorHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
public func canHandleTransition(model: JXSegmentedIndicatorTransitionParams) -> Bool {
|
||||||
|
if model.percent == 0 || !isScrollEnabled {
|
||||||
|
//model.percent等于0时不需要处理,会调用selectItem(model: JXSegmentedIndicatorParamsModel)方法处理
|
||||||
|
//isScrollEnabled为false不需要处理
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func canSelectedWithAnimation(model: JXSegmentedIndicatorSelectedParams) -> Bool {
|
||||||
|
if isScrollEnabled && (model.selectedType == .click || model.selectedType == .code) {
|
||||||
|
//允许滚动且选中类型是点击或代码选中,才进行动画过渡
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedIndicatorProtocol
|
||||||
|
open func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
}
|
||||||
|
|
||||||
|
open func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
}
|
||||||
|
|
||||||
|
open func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorDotLineView.swift
generated
Normal file
94
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorDotLineView.swift
generated
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorDotLineView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/16.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorDotLineView: JXSegmentedIndicatorBaseView {
|
||||||
|
/// 线的最大宽度
|
||||||
|
open var lineMaxWidth: CGFloat = 50
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
//配置点的size
|
||||||
|
indicatorWidth = 10
|
||||||
|
indicatorHeight = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = indicatorColor
|
||||||
|
layer.cornerRadius = getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
var targetX: CGFloat = leftItemFrame.origin.x
|
||||||
|
let dotWidth = getIndicatorWidth(itemFrame: leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
var targetWidth = dotWidth
|
||||||
|
|
||||||
|
let leftWidth = targetWidth
|
||||||
|
let rightWidth = getIndicatorWidth(itemFrame: rightItemFrame, itemContentWidth: model.rightItemContentWidth)
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - leftWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - rightWidth)/2
|
||||||
|
let centerX = leftX + (rightX - leftX - lineMaxWidth)/2
|
||||||
|
|
||||||
|
//前50%,移动x,增加宽度;后50%,移动x并减小width
|
||||||
|
if percent <= 0.5 {
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from: leftX, to: centerX, percent: CGFloat(percent*2))
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: dotWidth, to: lineMaxWidth, percent: CGFloat(percent*2))
|
||||||
|
}else {
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from: centerX, to: rightX, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: lineMaxWidth, to: dotWidth, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frame.origin.x = targetX
|
||||||
|
self.frame.size.width = targetWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = self.frame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - targetWidth)/2
|
||||||
|
toFrame.size.width = targetWidth
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
UIView.animate(withDuration: scrollAnimationDuration, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
self.frame = toFrame
|
||||||
|
}) { (_) in
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
frame = toFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
109
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorDoubleLineView.swift
generated
Normal file
109
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorDoubleLineView.swift
generated
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorDoubleLineView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/16.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorDoubleLineView: JXSegmentedIndicatorBaseView {
|
||||||
|
/// 线收缩到最小的百分比
|
||||||
|
open var minLineWidthPercent: CGFloat = 0.2
|
||||||
|
public let selectedLineView: UIView = UIView()
|
||||||
|
public let otherLineView: UIView = UIView()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorHeight = 3
|
||||||
|
|
||||||
|
addSubview(selectedLineView)
|
||||||
|
|
||||||
|
otherLineView.alpha = 0
|
||||||
|
addSubview(otherLineView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
selectedLineView.backgroundColor = indicatorColor
|
||||||
|
otherLineView.backgroundColor = indicatorColor
|
||||||
|
selectedLineView.layer.cornerRadius = getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
otherLineView.layer.cornerRadius = getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
selectedLineView.frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
otherLineView.frame = selectedLineView.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
|
||||||
|
let leftCenter = getCenter(in: leftItemFrame)
|
||||||
|
let rightCenter = getCenter(in: rightItemFrame)
|
||||||
|
let leftMaxWidth = getIndicatorWidth(itemFrame: leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
let rightMaxWidth = getIndicatorWidth(itemFrame: rightItemFrame, itemContentWidth: model.rightItemContentWidth)
|
||||||
|
let leftMinWidth = leftMaxWidth*minLineWidthPercent
|
||||||
|
let rightMinWidth = rightMaxWidth*minLineWidthPercent
|
||||||
|
|
||||||
|
let leftWidth: CGFloat = JXSegmentedViewTool.interpolate(from: leftMaxWidth, to: leftMinWidth, percent: CGFloat(percent))
|
||||||
|
let rightWidth: CGFloat = JXSegmentedViewTool.interpolate(from: rightMinWidth, to: rightMaxWidth, percent: CGFloat(percent))
|
||||||
|
let leftAlpha: CGFloat = JXSegmentedViewTool.interpolate(from: 1, to: 0, percent: CGFloat(percent))
|
||||||
|
let rightAlpha: CGFloat = JXSegmentedViewTool.interpolate(from: 0, to: 1, percent: CGFloat(percent))
|
||||||
|
|
||||||
|
if model.currentSelectedIndex == model.leftIndex {
|
||||||
|
selectedLineView.bounds.size.width = leftWidth
|
||||||
|
selectedLineView.center = leftCenter
|
||||||
|
selectedLineView.alpha = leftAlpha
|
||||||
|
|
||||||
|
otherLineView.bounds.size.width = rightWidth
|
||||||
|
otherLineView.center = rightCenter
|
||||||
|
otherLineView.alpha = rightAlpha
|
||||||
|
}else {
|
||||||
|
otherLineView.bounds.size.width = leftWidth
|
||||||
|
otherLineView.center = leftCenter
|
||||||
|
otherLineView.alpha = leftAlpha
|
||||||
|
|
||||||
|
selectedLineView.bounds.size.width = rightWidth
|
||||||
|
selectedLineView.center = rightCenter
|
||||||
|
selectedLineView.alpha = rightAlpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let targetCenter = getCenter(in: model.currentSelectedItemFrame)
|
||||||
|
selectedLineView.bounds.size.width = targetWidth
|
||||||
|
selectedLineView.center = targetCenter
|
||||||
|
selectedLineView.alpha = 1
|
||||||
|
|
||||||
|
otherLineView.alpha = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getCenter(in frame: CGRect) -> CGPoint {
|
||||||
|
return CGPoint(x: frame.midX, y: selectedLineView.center.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorGradientLineView.swift
generated
Normal file
63
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorGradientLineView.swift
generated
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorGradientLineView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2020/7/6.
|
||||||
|
// Copyright © 2020 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 会无视indicatorColor属性,以gradientColors为准
|
||||||
|
open class JXSegmentedIndicatorGradientLineView: JXSegmentedIndicatorLineView {
|
||||||
|
open var colors = [UIColor]()
|
||||||
|
open var startPoint = CGPoint.zero
|
||||||
|
open var endPoint = CGPoint(x: 1, y: 0)
|
||||||
|
open var locations: [NSNumber]?
|
||||||
|
public let gradientLayer = CAGradientLayer()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
layer.masksToBounds = true
|
||||||
|
layer.addSublayer(gradientLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = .clear
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientLayer.frame = bounds
|
||||||
|
gradientLayer.colors = colors.map { $0.cgColor }
|
||||||
|
gradientLayer.startPoint = startPoint
|
||||||
|
gradientLayer.endPoint = endPoint
|
||||||
|
gradientLayer.locations = locations
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientLayer.frame = bounds
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setAnimationDuration(scrollAnimationDuration)
|
||||||
|
CATransaction.setAnimationTimingFunction(.init(name: .easeOut))
|
||||||
|
gradientLayer.frame.size.width = targetWidth
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
131
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorGradientView.swift
generated
Normal file
131
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorGradientView.swift
generated
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorGradientView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/16.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 整个背景是一个渐变色layer,通过gradientMaskLayer遮罩显示不同位置,达到不同文字底部有不同的渐变色。
|
||||||
|
open class JXSegmentedIndicatorGradientView: JXSegmentedIndicatorBaseView {
|
||||||
|
@available(*, deprecated, renamed: "indicatorWidthIncrement")
|
||||||
|
open var gradientViewWidthIncrement: CGFloat = 20 {
|
||||||
|
didSet {
|
||||||
|
indicatorWidthIncrement = gradientViewWidthIncrement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 渐变colors
|
||||||
|
open var gradientColors = [CGColor]()
|
||||||
|
/// 渐变CAGradientLayer,通过它设置startPoint、endPoint等其他属性
|
||||||
|
open var gradientLayer: CAGradientLayer {
|
||||||
|
return layer as! CAGradientLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
public let gradientMaskLayer: CAShapeLayer = CAShapeLayer()
|
||||||
|
open class override var layerClass: AnyClass {
|
||||||
|
return CAGradientLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
private var gradientMaskLayerFrame = CGRect.zero
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorWidthIncrement = 20
|
||||||
|
indicatorHeight = 26
|
||||||
|
indicatorPosition = .center
|
||||||
|
verticalOffset = 0
|
||||||
|
|
||||||
|
gradientColors = [UIColor(red: 194.0/255, green: 229.0/255, blue: 156.0/255, alpha: 1).cgColor, UIColor(red: 100.0/255, green: 179.0/255, blue: 244.0/255, alpha: 1).cgColor]
|
||||||
|
|
||||||
|
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||||
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
|
||||||
|
layer.mask = gradientMaskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
gradientLayer.colors = gradientColors
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
gradientMaskLayerFrame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
let path = UIBezierPath(roundedRect: gradientMaskLayerFrame, cornerRadius: getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame))
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientMaskLayer.path = path.cgPath
|
||||||
|
CATransaction.commit()
|
||||||
|
if let collectionViewContentSize = model.collectionViewContentSize {
|
||||||
|
frame = CGRect(x: 0, y: 0, width: collectionViewContentSize.width, height: collectionViewContentSize.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
var targetWidth = getIndicatorWidth(itemFrame: leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
|
||||||
|
let leftWidth = targetWidth
|
||||||
|
let rightWidth = getIndicatorWidth(itemFrame: rightItemFrame, itemContentWidth: model.rightItemContentWidth)
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - leftWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - rightWidth)/2
|
||||||
|
let targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat(percent))
|
||||||
|
if indicatorWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: leftWidth, to: rightWidth, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
gradientMaskLayerFrame.origin.x = targetX
|
||||||
|
gradientMaskLayerFrame.size.width = targetWidth
|
||||||
|
let path = UIBezierPath(roundedRect: gradientMaskLayerFrame, cornerRadius: getIndicatorCornerRadius(itemFrame: leftItemFrame))
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientMaskLayer.path = path.cgPath
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = gradientMaskLayerFrame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
toFrame.size.width = width
|
||||||
|
let path = UIBezierPath(roundedRect: toFrame, cornerRadius: getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame))
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
gradientMaskLayer.removeAnimation(forKey: "path")
|
||||||
|
let animation = CABasicAnimation(keyPath: "path")
|
||||||
|
animation.fromValue = gradientMaskLayer.path
|
||||||
|
animation.toValue = path.cgPath
|
||||||
|
animation.duration = scrollAnimationDuration
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
||||||
|
gradientMaskLayer.add(animation, forKey: "path")
|
||||||
|
gradientMaskLayer.path = path.cgPath
|
||||||
|
}else {
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientMaskLayer.path = path.cgPath
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorImageView.swift
generated
Normal file
81
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorImageView.swift
generated
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorImageView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/2.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorImageView: JXSegmentedIndicatorBaseView {
|
||||||
|
open var image: UIImage? {
|
||||||
|
didSet {
|
||||||
|
layer.contents = image?.cgImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorWidth = 20
|
||||||
|
indicatorHeight = 20
|
||||||
|
layer.contentsGravity = .resizeAspect
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = nil
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - targetWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - targetWidth)/2
|
||||||
|
let targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat(percent))
|
||||||
|
|
||||||
|
self.frame.origin.x = targetX
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = self.frame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - targetWidth)/2
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
UIView.animate(withDuration: scrollAnimationDuration, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
self.frame = toFrame
|
||||||
|
}) { (_) in
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
frame = toFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
116
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorLineView.swift
generated
Normal file
116
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorLineView.swift
generated
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorLineView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum JXSegmentedIndicatorLineStyle {
|
||||||
|
case normal
|
||||||
|
case lengthen
|
||||||
|
case lengthenOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorLineView: JXSegmentedIndicatorBaseView {
|
||||||
|
open var lineStyle: JXSegmentedIndicatorLineStyle = .normal
|
||||||
|
/// lineStyle为lengthenOffset时使用,滚动时x的偏移量
|
||||||
|
open var lineScrollOffsetX: CGFloat = 10
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorHeight = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = indicatorColor
|
||||||
|
layer.cornerRadius = getIndicatorCornerRadius(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
var targetX: CGFloat = leftItemFrame.origin.x
|
||||||
|
var targetWidth = getIndicatorWidth(itemFrame: leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
|
||||||
|
let leftWidth = targetWidth
|
||||||
|
let rightWidth = getIndicatorWidth(itemFrame: rightItemFrame, itemContentWidth: model.rightItemContentWidth)
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - leftWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - rightWidth)/2
|
||||||
|
|
||||||
|
switch lineStyle {
|
||||||
|
case .normal:
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat(percent))
|
||||||
|
if indicatorWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: leftWidth, to: rightWidth, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
case .lengthen:
|
||||||
|
//前50%,只增加width;后50%,移动x并减小width
|
||||||
|
let maxWidth = rightX - leftX + rightWidth
|
||||||
|
if percent <= 0.5 {
|
||||||
|
targetX = leftX
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: leftWidth, to: maxWidth, percent: CGFloat(percent*2))
|
||||||
|
}else {
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: maxWidth, to: rightWidth, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
}
|
||||||
|
case .lengthenOffset:
|
||||||
|
//前50%,增加width,并少量移动x;后50%,少量移动x并减小width
|
||||||
|
let maxWidth = rightX - leftX + rightWidth - lineScrollOffsetX*2
|
||||||
|
if percent <= 0.5 {
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from: leftX, to: leftX + lineScrollOffsetX, percent: CGFloat(percent*2))
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: leftWidth, to: maxWidth, percent: CGFloat(percent*2))
|
||||||
|
}else {
|
||||||
|
targetX = JXSegmentedViewTool.interpolate(from:leftX + lineScrollOffsetX, to: rightX, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
targetWidth = JXSegmentedViewTool.interpolate(from: maxWidth, to: rightWidth, percent: CGFloat((percent - 0.5)*2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frame.origin.x = targetX
|
||||||
|
self.frame.size.width = targetWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = self.frame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - targetWidth)/2
|
||||||
|
toFrame.size.width = targetWidth
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
UIView.animate(withDuration: scrollAnimationDuration, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
self.frame = toFrame
|
||||||
|
}) { (_) in
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
frame = toFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorParams.swift
generated
Normal file
52
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorParams.swift
generated
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorParamsModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
/**
|
||||||
|
指示器传递的数据模型,不同情况会对不同的属性赋值,根据不同情况的api说明确认。
|
||||||
|
为什么会通过model传递数据,因为指示器处理逻辑以后会扩展不同的使用场景,会新增参数。如果不通过model传递,就会在api新增参数,一旦修改api修改的地方就特别多了,而且会影响到之前自定义实现的开发者。
|
||||||
|
*/
|
||||||
|
public struct JXSegmentedIndicatorSelectedParams {
|
||||||
|
public let currentSelectedIndex: Int
|
||||||
|
public let currentSelectedItemFrame: CGRect
|
||||||
|
public let selectedType: JXSegmentedViewItemSelectedType
|
||||||
|
public let currentItemContentWidth: CGFloat
|
||||||
|
/// collectionView的contentSize
|
||||||
|
public var collectionViewContentSize: CGSize?
|
||||||
|
|
||||||
|
public init(currentSelectedIndex: Int, currentSelectedItemFrame: CGRect, selectedType: JXSegmentedViewItemSelectedType, currentItemContentWidth: CGFloat, collectionViewContentSize: CGSize?) {
|
||||||
|
self.currentSelectedIndex = currentSelectedIndex
|
||||||
|
self.currentSelectedItemFrame = currentSelectedItemFrame
|
||||||
|
self.selectedType = selectedType
|
||||||
|
self.currentItemContentWidth = currentItemContentWidth
|
||||||
|
self.collectionViewContentSize = collectionViewContentSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct JXSegmentedIndicatorTransitionParams {
|
||||||
|
public let currentSelectedIndex: Int
|
||||||
|
public let leftIndex: Int
|
||||||
|
public let leftItemFrame: CGRect
|
||||||
|
public let rightIndex: Int
|
||||||
|
public let rightItemFrame: CGRect
|
||||||
|
public let leftItemContentWidth: CGFloat
|
||||||
|
public let rightItemContentWidth: CGFloat
|
||||||
|
public let percent: CGFloat
|
||||||
|
|
||||||
|
public init(currentSelectedIndex: Int, leftIndex: Int, leftItemFrame: CGRect, leftItemContentWidth: CGFloat, rightIndex: Int, rightItemFrame: CGRect, rightItemContentWidth: CGFloat, percent: CGFloat) {
|
||||||
|
self.currentSelectedIndex = currentSelectedIndex
|
||||||
|
self.leftIndex = leftIndex
|
||||||
|
self.leftItemFrame = leftItemFrame
|
||||||
|
self.leftItemContentWidth = leftItemContentWidth
|
||||||
|
self.rightIndex = rightIndex
|
||||||
|
self.rightItemFrame = rightItemFrame
|
||||||
|
self.rightItemContentWidth = rightItemContentWidth
|
||||||
|
self.percent = percent
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorProtocol.swift
generated
Normal file
41
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorProtocol.swift
generated
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorProtocol.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol JXSegmentedIndicatorProtocol: UIView {
|
||||||
|
/// 是否需要将当前的indicator的frame转换到cell。辅助JXSegmentedTitleDataSourced的isTitleMaskEnabled属性使用。
|
||||||
|
/// 如果添加了多个indicator,仅能有一个indicator的isIndicatorConvertToItemFrameEnabled为true。
|
||||||
|
/// 如果有多个indicator的isIndicatorConvertToItemFrameEnabled为true,则以最后一个isIndicatorConvertToItemFrameEnabled为true的indicator为准。
|
||||||
|
var isIndicatorConvertToItemFrameEnabled: Bool { get }
|
||||||
|
|
||||||
|
/// 视图重置状态时调用,已当前选中的index更新状态
|
||||||
|
/// param selectedIndex 当前选中的index
|
||||||
|
/// param selectedCellFrame 当前选中的cellFrame
|
||||||
|
/// param contentSize collectionView的contentSize
|
||||||
|
/// - Parameter model: model description
|
||||||
|
func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams)
|
||||||
|
|
||||||
|
/// contentScrollView在进行手势滑动时,处理指示器跟随手势变化UI逻辑;
|
||||||
|
/// param selectedIndex 当前选中的index
|
||||||
|
/// param leftIndex 正在过渡中的两个cell,相对位置在左边的cell的index
|
||||||
|
/// param leftCellFrame 正在过渡中的两个cell,相对位置在左边的cell的frame
|
||||||
|
/// param rightIndex 正在过渡中的两个cell,相对位置在右边的cell的index
|
||||||
|
/// param rightCellFrame 正在过渡中的两个cell,相对位置在右边的cell的frame
|
||||||
|
/// param percent 过渡百分比
|
||||||
|
/// - Parameter model: model description
|
||||||
|
func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams)
|
||||||
|
|
||||||
|
/// 点击选中了某一个item
|
||||||
|
/// param selectedIndex 选中的index
|
||||||
|
/// param selectedCellFrame 选中的cellFrame
|
||||||
|
/// param selectedType 选中的类型
|
||||||
|
/// - Parameter model: model description
|
||||||
|
func selectItem(model: JXSegmentedIndicatorSelectedParams)
|
||||||
|
}
|
||||||
39
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorRainbowLineView.swift
generated
Normal file
39
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorRainbowLineView.swift
generated
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorRainbowLineView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
/// 会无视indicatorColor属性,以indicatorColors为准
|
||||||
|
open class JXSegmentedIndicatorRainbowLineView: JXSegmentedIndicatorLineView {
|
||||||
|
/// 数量需要与item的数量相等。默认空数组,必须要赋值该属性。segmentedView在reloadData的时候,也要一并更新该属性,不然会出现数组越界。
|
||||||
|
open var indicatorColors = [UIColor]()
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = indicatorColors[model.currentSelectedIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundColor = JXSegmentedViewTool.interpolateColor(from: indicatorColors[model.leftIndex], to: indicatorColors[model.rightIndex], percent: model.percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
backgroundColor = indicatorColors[model.currentSelectedIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
96
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorTriangleView.swift
generated
Normal file
96
Pods/JXSegmentedView/Sources/Indicator/JXSegmentedIndicatorTriangleView.swift
generated
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedIndicatorTriangleView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedIndicatorTriangleView: JXSegmentedIndicatorBaseView {
|
||||||
|
open override class var layerClass: AnyClass {
|
||||||
|
return CAShapeLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
private var path = UIBezierPath()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
indicatorWidth = 14
|
||||||
|
indicatorHeight = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshIndicatorState(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.refreshIndicatorState(model: model)
|
||||||
|
|
||||||
|
backgroundColor = nil
|
||||||
|
let shapeLayer = self.layer as! CAShapeLayer
|
||||||
|
shapeLayer.fillColor = indicatorColor.cgColor
|
||||||
|
|
||||||
|
let width = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
let height = getIndicatorHeight(itemFrame: model.currentSelectedItemFrame)
|
||||||
|
let x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - width)/2
|
||||||
|
var y: CGFloat = 0
|
||||||
|
switch indicatorPosition {
|
||||||
|
case .top:
|
||||||
|
y = verticalOffset
|
||||||
|
case .bottom:
|
||||||
|
y = model.currentSelectedItemFrame.size.height - height - verticalOffset
|
||||||
|
case .center:
|
||||||
|
y = (model.currentSelectedItemFrame.size.height - height)/2 + verticalOffset
|
||||||
|
}
|
||||||
|
frame = CGRect(x: x, y: y, width: width, height: height)
|
||||||
|
|
||||||
|
path = UIBezierPath()
|
||||||
|
if indicatorPosition == .bottom {
|
||||||
|
path.move(to: CGPoint(x: 0, y: height))
|
||||||
|
path.addLine(to: CGPoint(x: width/2, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: width, y: height))
|
||||||
|
}else {
|
||||||
|
path.move(to: CGPoint(x: 0, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: width/2, y: height))
|
||||||
|
path.addLine(to: CGPoint(x: width, y: 0))
|
||||||
|
}
|
||||||
|
path.close()
|
||||||
|
|
||||||
|
shapeLayer.path = path.cgPath
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func contentScrollViewDidScroll(model: JXSegmentedIndicatorTransitionParams) {
|
||||||
|
super.contentScrollViewDidScroll(model: model)
|
||||||
|
|
||||||
|
guard canHandleTransition(model: model) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let rightItemFrame = model.rightItemFrame
|
||||||
|
let leftItemFrame = model.leftItemFrame
|
||||||
|
let percent = model.percent
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.leftItemFrame, itemContentWidth: model.leftItemContentWidth)
|
||||||
|
|
||||||
|
let leftX = leftItemFrame.origin.x + (leftItemFrame.size.width - targetWidth)/2
|
||||||
|
let rightX = rightItemFrame.origin.x + (rightItemFrame.size.width - targetWidth)/2
|
||||||
|
let targetX = JXSegmentedViewTool.interpolate(from: leftX, to: rightX, percent: CGFloat(percent))
|
||||||
|
|
||||||
|
self.frame.origin.x = targetX
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func selectItem(model: JXSegmentedIndicatorSelectedParams) {
|
||||||
|
super.selectItem(model: model)
|
||||||
|
|
||||||
|
let targetWidth = getIndicatorWidth(itemFrame: model.currentSelectedItemFrame, itemContentWidth: model.currentItemContentWidth)
|
||||||
|
var toFrame = self.frame
|
||||||
|
toFrame.origin.x = model.currentSelectedItemFrame.origin.x + (model.currentSelectedItemFrame.size.width - targetWidth)/2
|
||||||
|
if canSelectedWithAnimation(model: model) {
|
||||||
|
UIView.animate(withDuration: scrollAnimationDuration, delay: 0, options: .curveEaseOut, animations: {
|
||||||
|
self.frame = toFrame
|
||||||
|
}) { (_) in
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
frame = toFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberCell.swift
generated
Normal file
52
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberCell.swift
generated
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedNumberCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedNumberCell: JXSegmentedTitleCell {
|
||||||
|
public let numberLabel = UILabel()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
numberLabel.isHidden = true
|
||||||
|
numberLabel.textAlignment = .center
|
||||||
|
numberLabel.layer.masksToBounds = true
|
||||||
|
contentView.addSubview(numberLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedNumberItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numberLabel.sizeToFit()
|
||||||
|
let height = myItemModel.numberHeight
|
||||||
|
numberLabel.layer.cornerRadius = height/2
|
||||||
|
numberLabel.bounds.size = CGSize(width: numberLabel.bounds.size.width + myItemModel.numberWidthIncrement, height: height)
|
||||||
|
numberLabel.center = CGPoint(x: titleLabel.frame.maxX + myItemModel.numberOffset.x, y: titleLabel.frame.minY + myItemModel.numberOffset.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedNumberItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numberLabel.backgroundColor = myItemModel.numberBackgroundColor
|
||||||
|
numberLabel.textColor = myItemModel.numberTextColor
|
||||||
|
numberLabel.text = myItemModel.numberString
|
||||||
|
numberLabel.font = myItemModel.numberFont
|
||||||
|
numberLabel.isHidden = myItemModel.number == 0
|
||||||
|
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberDataSource.swift
generated
Normal file
64
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberDataSource.swift
generated
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedNumberDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedNumberDataSource: JXSegmentedTitleDataSource {
|
||||||
|
/// 需要和titles数组数量一致,没有数字的item填0!!!
|
||||||
|
open var numbers = [Int]()
|
||||||
|
/// numberLabel的宽度补偿,numberLabel真实的宽度是文字内容的宽度加上补偿的宽度
|
||||||
|
open var numberWidthIncrement: CGFloat = 10
|
||||||
|
/// numberLabel的背景色
|
||||||
|
open var numberBackgroundColor: UIColor = .red
|
||||||
|
/// numberLabel的textColor
|
||||||
|
open var numberTextColor: UIColor = .white
|
||||||
|
/// numberLabel的font
|
||||||
|
open var numberFont: UIFont = UIFont.systemFont(ofSize: 11)
|
||||||
|
/// numberLabel的默认位置是center在titleLabel的右上角,可以通过numberOffset控制X、Y轴的偏移
|
||||||
|
open var numberOffset: CGPoint = CGPoint.zero
|
||||||
|
/// 如果业务需要处理超过999就像是999+,就可以通过这个闭包实现。默认显示不会对number进行处理
|
||||||
|
open var numberStringFormatterClosure: ((Int) -> String)?
|
||||||
|
/// numberLabel的高度,默认:14
|
||||||
|
open var numberHeight: CGFloat = 14
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedNumberItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let itemModel = itemModel as? JXSegmentedNumberItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModel.number = numbers[index]
|
||||||
|
if numberStringFormatterClosure != nil {
|
||||||
|
itemModel.numberString = numberStringFormatterClosure!(itemModel.number)
|
||||||
|
}else {
|
||||||
|
itemModel.numberString = "\(itemModel.number)"
|
||||||
|
}
|
||||||
|
itemModel.numberTextColor = numberTextColor
|
||||||
|
itemModel.numberBackgroundColor = numberBackgroundColor
|
||||||
|
itemModel.numberOffset = numberOffset
|
||||||
|
itemModel.numberWidthIncrement = numberWidthIncrement
|
||||||
|
itemModel.numberHeight = numberHeight
|
||||||
|
itemModel.numberFont = numberFont
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedNumberCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberItemModel.swift
generated
Normal file
21
Pods/JXSegmentedView/Sources/Number/JXSegmentedNumberItemModel.swift
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedNumberItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/28.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedNumberItemModel: JXSegmentedTitleItemModel {
|
||||||
|
open var number: Int = 0
|
||||||
|
open var numberString: String = "0"
|
||||||
|
open var numberBackgroundColor: UIColor = .red
|
||||||
|
open var numberTextColor: UIColor = .white
|
||||||
|
open var numberWidthIncrement: CGFloat = 0
|
||||||
|
open var numberFont: UIFont = UIFont.systemFont(ofSize: 11)
|
||||||
|
open var numberOffset: CGPoint = CGPoint.zero
|
||||||
|
open var numberHeight: CGFloat = 14
|
||||||
|
}
|
||||||
14
Pods/JXSegmentedView/Sources/PrivacyInfo.xcprivacy
generated
Normal file
14
Pods/JXSegmentedView/Sources/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array/>
|
||||||
|
<key>NSPrivacyTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
207
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleCell.swift
generated
Normal file
207
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleCell.swift
generated
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleCell: JXSegmentedBaseCell {
|
||||||
|
public let titleLabel = UILabel()
|
||||||
|
public let maskTitleLabel = UILabel()
|
||||||
|
public let titleMaskLayer = CALayer()
|
||||||
|
public let maskTitleMaskLayer = CALayer()
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
contentView.addSubview(titleLabel)
|
||||||
|
|
||||||
|
maskTitleLabel.textAlignment = .center
|
||||||
|
maskTitleLabel.isHidden = true
|
||||||
|
contentView.addSubview(maskTitleLabel)
|
||||||
|
|
||||||
|
titleMaskLayer.backgroundColor = UIColor.red.cgColor
|
||||||
|
|
||||||
|
maskTitleMaskLayer.backgroundColor = UIColor.red.cgColor
|
||||||
|
maskTitleLabel.layer.mask = maskTitleMaskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
//为什么使用`sizeThatFits`,而不用`sizeToFit`呢?在numberOfLines大于0的时候,cell进行重用的时候通过`sizeToFit`,label设置成错误的size。至于原因我用尽毕生所学,没有找到为什么。但是用`sizeThatFits`可以规避掉这个问题。
|
||||||
|
let labelSize = titleLabel.sizeThatFits(self.contentView.bounds.size)
|
||||||
|
let labelBounds = CGRect(x: 0, y: 0, width: labelSize.width, height: labelSize.height)
|
||||||
|
titleLabel.bounds = labelBounds
|
||||||
|
titleLabel.center = contentView.center
|
||||||
|
|
||||||
|
maskTitleLabel.bounds = labelBounds
|
||||||
|
maskTitleLabel.center = contentView.center
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.numberOfLines = myItemModel.titleNumberOfLines
|
||||||
|
maskTitleLabel.numberOfLines = myItemModel.titleNumberOfLines
|
||||||
|
|
||||||
|
if myItemModel.isTitleZoomEnabled {
|
||||||
|
//先把font设置为缩放的最大值,再缩小到最小值,最后根据当前的titleCurrentZoomScale值,进行缩放更新。这样就能避免transform从小到大时字体模糊
|
||||||
|
let maxScaleFont = UIFont(descriptor: myItemModel.titleNormalFont.fontDescriptor, size: myItemModel.titleNormalFont.pointSize*CGFloat(myItemModel.titleSelectedZoomScale))
|
||||||
|
let baseScale = myItemModel.titleNormalFont.lineHeight/maxScaleFont.lineHeight
|
||||||
|
|
||||||
|
if myItemModel.isSelectedAnimable && canStartSelectedAnimation(itemModel: itemModel, selectedType: selectedType) {
|
||||||
|
//允许动画且当前是点击的
|
||||||
|
let titleZoomClosure = preferredTitleZoomAnimateClosure(itemModel: myItemModel, baseScale: baseScale)
|
||||||
|
appendSelectedAnimationClosure(closure: titleZoomClosure)
|
||||||
|
}else {
|
||||||
|
titleLabel.font = maxScaleFont
|
||||||
|
maskTitleLabel.font = maxScaleFont
|
||||||
|
let currentTransform = CGAffineTransform(scaleX: baseScale*CGFloat(myItemModel.titleCurrentZoomScale), y: baseScale*CGFloat(myItemModel.titleCurrentZoomScale))
|
||||||
|
titleLabel.transform = currentTransform
|
||||||
|
maskTitleLabel.transform = currentTransform
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
if myItemModel.isSelected {
|
||||||
|
titleLabel.font = myItemModel.titleSelectedFont
|
||||||
|
maskTitleLabel.font = myItemModel.titleSelectedFont
|
||||||
|
}else {
|
||||||
|
titleLabel.font = myItemModel.titleNormalFont
|
||||||
|
maskTitleLabel.font = myItemModel.titleNormalFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = myItemModel.title ?? ""
|
||||||
|
let attriText = NSMutableAttributedString(string: title)
|
||||||
|
if myItemModel.isTitleStrokeWidthEnabled {
|
||||||
|
if myItemModel.isSelectedAnimable && canStartSelectedAnimation(itemModel: itemModel, selectedType: selectedType) {
|
||||||
|
//允许动画且当前是点击的
|
||||||
|
let titleStrokeWidthClosure = preferredTitleStrokeWidthAnimateClosure(itemModel: myItemModel, attriText: attriText)
|
||||||
|
appendSelectedAnimationClosure(closure: titleStrokeWidthClosure)
|
||||||
|
}else {
|
||||||
|
attriText.addAttributes([NSAttributedString.Key.strokeWidth: myItemModel.titleCurrentStrokeWidth], range: NSRange(location: 0, length: title.count))
|
||||||
|
titleLabel.attributedText = attriText
|
||||||
|
maskTitleLabel.attributedText = attriText
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
titleLabel.attributedText = attriText
|
||||||
|
maskTitleLabel.attributedText = attriText
|
||||||
|
}
|
||||||
|
|
||||||
|
if myItemModel.isTitleMaskEnabled {
|
||||||
|
//允许mask,maskTitleLabel在titleLabel上面,maskTitleLabel设置为titleSelectedColor。titleLabel设置为titleNormalColor
|
||||||
|
//为了显示效果,使用了双遮罩。即titleMaskLayer遮罩titleLabel,maskTitleMaskLayer遮罩maskTitleLabel
|
||||||
|
maskTitleLabel.isHidden = false
|
||||||
|
titleLabel.textColor = myItemModel.titleNormalColor
|
||||||
|
maskTitleLabel.textColor = myItemModel.titleSelectedColor
|
||||||
|
let labelSize = maskTitleLabel.sizeThatFits(self.contentView.bounds.size)
|
||||||
|
let labelBounds = CGRect(x: 0, y: 0, width: labelSize.width, height: labelSize.height)
|
||||||
|
maskTitleLabel.bounds = labelBounds
|
||||||
|
|
||||||
|
var topMaskFrame = myItemModel.indicatorConvertToItemFrame
|
||||||
|
topMaskFrame.origin.y = 0
|
||||||
|
var bottomMaskFrame = topMaskFrame
|
||||||
|
var maskStartX: CGFloat = 0
|
||||||
|
if maskTitleLabel.bounds.size.width >= bounds.size.width {
|
||||||
|
topMaskFrame.origin.x -= (maskTitleLabel.bounds.size.width - bounds.size.width)/2
|
||||||
|
bottomMaskFrame.size.width = maskTitleLabel.bounds.size.width
|
||||||
|
maskStartX = -(maskTitleLabel.bounds.size.width - bounds.size.width)/2
|
||||||
|
}else {
|
||||||
|
topMaskFrame.origin.x -= (bounds.size.width - maskTitleLabel.bounds.size.width)/2
|
||||||
|
bottomMaskFrame.size.width = bounds.size.width
|
||||||
|
maskStartX = 0
|
||||||
|
}
|
||||||
|
bottomMaskFrame.origin.x = topMaskFrame.origin.x
|
||||||
|
if topMaskFrame.origin.x > maskStartX {
|
||||||
|
bottomMaskFrame.origin.x = topMaskFrame.origin.x - bottomMaskFrame.size.width
|
||||||
|
}else {
|
||||||
|
bottomMaskFrame.origin.x = topMaskFrame.maxX
|
||||||
|
}
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
if topMaskFrame.size.width > 0 && topMaskFrame.intersects(maskTitleLabel.frame) {
|
||||||
|
titleLabel.layer.mask = titleMaskLayer
|
||||||
|
titleMaskLayer.frame = bottomMaskFrame
|
||||||
|
maskTitleMaskLayer.frame = topMaskFrame
|
||||||
|
}else {
|
||||||
|
titleLabel.layer.mask = nil
|
||||||
|
maskTitleMaskLayer.frame = topMaskFrame
|
||||||
|
}
|
||||||
|
CATransaction.commit()
|
||||||
|
}else {
|
||||||
|
maskTitleLabel.isHidden = true
|
||||||
|
titleLabel.layer.mask = nil
|
||||||
|
if myItemModel.isSelectedAnimable && canStartSelectedAnimation(itemModel: itemModel, selectedType: selectedType) {
|
||||||
|
//允许动画且当前是点击的
|
||||||
|
let titleColorClosure = preferredTitleColorAnimateClosure(itemModel: myItemModel)
|
||||||
|
appendSelectedAnimationClosure(closure: titleColorClosure)
|
||||||
|
}else {
|
||||||
|
titleLabel.textColor = myItemModel.titleCurrentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startSelectedAnimationIfNeeded(itemModel: itemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredTitleZoomAnimateClosure(itemModel: JXSegmentedTitleItemModel, baseScale: CGFloat) -> JXSegmentedCellSelectedAnimationClosure {
|
||||||
|
return {[weak self] (percnet) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中,scale从小到大插值渐变
|
||||||
|
itemModel.titleCurrentZoomScale = JXSegmentedViewTool.interpolate(from: itemModel.titleNormalZoomScale, to: itemModel.titleSelectedZoomScale, percent: percnet)
|
||||||
|
}else {
|
||||||
|
//将要取消选中,scale从大到小插值渐变
|
||||||
|
itemModel.titleCurrentZoomScale = JXSegmentedViewTool.interpolate(from: itemModel.titleSelectedZoomScale, to:itemModel.titleNormalZoomScale , percent: percnet)
|
||||||
|
}
|
||||||
|
let currentTransform = CGAffineTransform(scaleX: baseScale*itemModel.titleCurrentZoomScale, y: baseScale*itemModel.titleCurrentZoomScale)
|
||||||
|
self?.titleLabel.transform = currentTransform
|
||||||
|
self?.maskTitleLabel.transform = currentTransform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredTitleStrokeWidthAnimateClosure(itemModel: JXSegmentedTitleItemModel, attriText: NSMutableAttributedString) -> JXSegmentedCellSelectedAnimationClosure{
|
||||||
|
return {[weak self] (percent) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中,StrokeWidth从小到大插值渐变
|
||||||
|
itemModel.titleCurrentStrokeWidth = JXSegmentedViewTool.interpolate(from: itemModel.titleNormalStrokeWidth, to: itemModel.titleSelectedStrokeWidth, percent: percent)
|
||||||
|
}else {
|
||||||
|
//将要取消选中,StrokeWidth从大到小插值渐变
|
||||||
|
itemModel.titleCurrentStrokeWidth = JXSegmentedViewTool.interpolate(from: itemModel.titleSelectedStrokeWidth, to:itemModel.titleNormalStrokeWidth , percent: percent)
|
||||||
|
}
|
||||||
|
attriText.addAttributes([NSAttributedString.Key.strokeWidth: itemModel.titleCurrentStrokeWidth], range: NSRange(location: 0, length: attriText.string.count))
|
||||||
|
self?.titleLabel.attributedText = attriText
|
||||||
|
self?.maskTitleLabel.attributedText = attriText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func preferredTitleColorAnimateClosure(itemModel: JXSegmentedTitleItemModel) -> JXSegmentedCellSelectedAnimationClosure {
|
||||||
|
return {[weak self] (percent) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中,textColor从titleNormalColor到titleSelectedColor插值渐变
|
||||||
|
itemModel.titleCurrentColor = JXSegmentedViewTool.interpolateThemeColor(from: itemModel.titleNormalColor, to: itemModel.titleSelectedColor, percent: percent)
|
||||||
|
}else {
|
||||||
|
//将要取消选中,textColor从titleSelectedColor到titleNormalColor插值渐变
|
||||||
|
itemModel.titleCurrentColor = JXSegmentedViewTool.interpolateThemeColor(from: itemModel.titleSelectedColor, to: itemModel.titleNormalColor, percent: percent)
|
||||||
|
}
|
||||||
|
self?.titleLabel.textColor = itemModel.titleCurrentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelectedStyle(isSelected: Bool) {
|
||||||
|
if isSelected {
|
||||||
|
self.titleLabel.textColor = (self.itemModel as? JXSegmentedTitleItemModel)?.titleSelectedColor
|
||||||
|
} else {
|
||||||
|
self.titleLabel.textColor = (self.itemModel as? JXSegmentedTitleItemModel)?.titleNormalColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleDataSource.swift
generated
Normal file
202
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleDataSource.swift
generated
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleView.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleDataSource: JXSegmentedBaseDataSource {
|
||||||
|
/// title数组
|
||||||
|
open var titles = [String]()
|
||||||
|
/// 根据index配置cell的不同属性
|
||||||
|
open var configuration: JXSegmentedTitleDynamicConfiguration?
|
||||||
|
/// 如果将JXSegmentedView嵌套进UITableView的cell,每次重用的时候,JXSegmentedView进行reloadData时,会重新计算所有的title宽度。所以该应用场景,需要UITableView的cellModel缓存titles的文字宽度,再通过该闭包方法返回给JXSegmentedView。
|
||||||
|
open var widthForTitleClosure: ((String)->(CGFloat))?
|
||||||
|
/// label的numberOfLines
|
||||||
|
open var titleNumberOfLines: Int = 1
|
||||||
|
/// title普通状态的textColor
|
||||||
|
open var titleNormalColor: UIColor = .black
|
||||||
|
/// title选中状态的textColor
|
||||||
|
open var titleSelectedColor: UIColor = .red
|
||||||
|
/// title普通状态时的字体
|
||||||
|
open var titleNormalFont: UIFont = UIFont.systemFont(ofSize: 15)
|
||||||
|
/// title选中时的字体。如果不赋值,就默认与titleNormalFont一样
|
||||||
|
open var titleSelectedFont: UIFont?
|
||||||
|
/// title的颜色是否渐变过渡
|
||||||
|
open var isTitleColorGradientEnabled: Bool = false
|
||||||
|
/// title是否缩放。使用该效果时,务必保证titleNormalFont和titleSelectedFont值相同。
|
||||||
|
open var isTitleZoomEnabled: Bool = false
|
||||||
|
/// isTitleZoomEnabled为true才生效。是对字号的缩放,比如titleNormalFont的pointSize为10,放大之后字号就是10*1.2=12。
|
||||||
|
open var titleSelectedZoomScale: CGFloat = 1.2
|
||||||
|
/// title的线宽是否允许粗细。使用该效果时,务必保证titleNormalFont和titleSelectedFont值相同。
|
||||||
|
open var isTitleStrokeWidthEnabled: Bool = false
|
||||||
|
/// 用于控制字体的粗细(底层通过NSStrokeWidthAttributeName实现),负数越小字体越粗。
|
||||||
|
open var titleSelectedStrokeWidth: CGFloat = -2
|
||||||
|
/// title是否使用遮罩过渡
|
||||||
|
open var isTitleMaskEnabled: Bool = false
|
||||||
|
|
||||||
|
open override func preferredItemCount() -> Int {
|
||||||
|
return titles.count
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedTitleItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel( _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myItemModel.title = titles[index]
|
||||||
|
myItemModel.textWidth = widthForTitle(myItemModel.title ?? "", index)
|
||||||
|
myItemModel.titleNumberOfLines = innerTitleNumberOfLines(at: index)
|
||||||
|
myItemModel.isSelectedAnimable = isSelectedAnimable
|
||||||
|
myItemModel.titleNormalColor = innerTitleNormalColor(at: index)
|
||||||
|
myItemModel.titleSelectedColor = innerTitleSelectedColor(at: index)
|
||||||
|
myItemModel.titleNormalFont = innerTitleNormalFont(at: index)
|
||||||
|
if let selectedFont = innerTitleSelectedFont(at: index) {
|
||||||
|
myItemModel.titleSelectedFont = selectedFont
|
||||||
|
} else {
|
||||||
|
myItemModel.titleSelectedFont = innerTitleNormalFont(at: index)
|
||||||
|
}
|
||||||
|
myItemModel.isTitleZoomEnabled = isTitleZoomEnabled
|
||||||
|
myItemModel.isTitleStrokeWidthEnabled = isTitleStrokeWidthEnabled
|
||||||
|
myItemModel.isTitleMaskEnabled = isTitleMaskEnabled
|
||||||
|
myItemModel.titleNormalZoomScale = 1
|
||||||
|
myItemModel.titleSelectedZoomScale = titleSelectedZoomScale
|
||||||
|
myItemModel.titleSelectedStrokeWidth = titleSelectedStrokeWidth
|
||||||
|
myItemModel.titleNormalStrokeWidth = 0
|
||||||
|
if index == selectedIndex {
|
||||||
|
myItemModel.titleCurrentColor = innerTitleSelectedColor(at: index)
|
||||||
|
myItemModel.titleCurrentZoomScale = titleSelectedZoomScale
|
||||||
|
myItemModel.titleCurrentStrokeWidth = titleSelectedStrokeWidth
|
||||||
|
}else {
|
||||||
|
myItemModel.titleCurrentColor = innerTitleNormalColor(at: index)
|
||||||
|
myItemModel.titleCurrentZoomScale = 1
|
||||||
|
myItemModel.titleCurrentStrokeWidth = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func widthForTitle(_ title: String, _ index: Int) -> CGFloat {
|
||||||
|
if widthForTitleClosure != nil {
|
||||||
|
return widthForTitleClosure!(title)
|
||||||
|
}else {
|
||||||
|
let textWidth = NSString(string: title).boundingRect(with: CGSize(width: CGFloat.infinity, height: CGFloat.infinity), options: [.usesFontLeading, .usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font : innerTitleNormalFont(at: index)], context: nil).size.width
|
||||||
|
return CGFloat(ceilf(Float(textWidth)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 因为该方法会被频繁调用,所以应该在`preferredRefreshItemModel( _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)`方法里面,根据数据源计算好文字宽度,然后缓存起来。该方法直接使用已经计算好的文字宽度即可。
|
||||||
|
open override func preferredSegmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat {
|
||||||
|
var width = super.preferredSegmentedView(segmentedView, widthForItemAt: index)
|
||||||
|
if itemWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
width += (dataSource[index] as! JXSegmentedTitleItemModel).textWidth
|
||||||
|
}else {
|
||||||
|
width += itemWidth
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedTitleCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat {
|
||||||
|
let model = dataSource[index] as! JXSegmentedTitleItemModel
|
||||||
|
if isTitleZoomEnabled {
|
||||||
|
return model.textWidth*model.titleCurrentZoomScale
|
||||||
|
}else {
|
||||||
|
return model.textWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat) {
|
||||||
|
super.refreshItemModel(segmentedView, leftItemModel: leftItemModel, rightItemModel: rightItemModel, percent: percent)
|
||||||
|
|
||||||
|
guard let leftModel = leftItemModel as? JXSegmentedTitleItemModel, let rightModel = rightItemModel as? JXSegmentedTitleItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTitleZoomEnabled && isItemTransitionEnabled {
|
||||||
|
leftModel.titleCurrentZoomScale = JXSegmentedViewTool.interpolate(from: leftModel.titleSelectedZoomScale, to: leftModel.titleNormalZoomScale, percent: CGFloat(percent))
|
||||||
|
rightModel.titleCurrentZoomScale = JXSegmentedViewTool.interpolate(from: rightModel.titleNormalZoomScale, to: rightModel.titleSelectedZoomScale, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTitleStrokeWidthEnabled && isItemTransitionEnabled {
|
||||||
|
leftModel.titleCurrentStrokeWidth = JXSegmentedViewTool.interpolate(from: leftModel.titleSelectedStrokeWidth, to: leftModel.titleNormalStrokeWidth, percent: CGFloat(percent))
|
||||||
|
rightModel.titleCurrentStrokeWidth = JXSegmentedViewTool.interpolate(from: rightModel.titleNormalStrokeWidth, to: rightModel.titleSelectedStrokeWidth, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTitleColorGradientEnabled && isItemTransitionEnabled {
|
||||||
|
leftModel.titleCurrentColor = JXSegmentedViewTool.interpolateThemeColor(from: leftModel.titleSelectedColor, to: leftModel.titleNormalColor, percent: percent)
|
||||||
|
rightModel.titleCurrentColor = JXSegmentedViewTool.interpolateThemeColor(from:rightModel.titleNormalColor , to:rightModel.titleSelectedColor, percent: percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.refreshItemModel(segmentedView, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
guard let myCurrentSelectedItemModel = currentSelectedItemModel as? JXSegmentedTitleItemModel, let myWillSelectedItemModel = willSelectedItemModel as? JXSegmentedTitleItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myCurrentSelectedItemModel.titleCurrentColor = myCurrentSelectedItemModel.titleNormalColor
|
||||||
|
myCurrentSelectedItemModel.titleCurrentZoomScale = myCurrentSelectedItemModel.titleNormalZoomScale
|
||||||
|
myCurrentSelectedItemModel.titleCurrentStrokeWidth = myCurrentSelectedItemModel.titleNormalStrokeWidth
|
||||||
|
myCurrentSelectedItemModel.indicatorConvertToItemFrame = CGRect.zero
|
||||||
|
|
||||||
|
myWillSelectedItemModel.titleCurrentColor = myWillSelectedItemModel.titleSelectedColor
|
||||||
|
myWillSelectedItemModel.titleCurrentZoomScale = myWillSelectedItemModel.titleSelectedZoomScale
|
||||||
|
myWillSelectedItemModel.titleCurrentStrokeWidth = myWillSelectedItemModel.titleSelectedStrokeWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
private func innerTitleNumberOfLines(at index: Int) -> Int {
|
||||||
|
if let configuration {
|
||||||
|
return configuration.titleNumberOfLines(at: index)
|
||||||
|
} else {
|
||||||
|
return titleNumberOfLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private func innerTitleNormalColor(at index: Int) -> UIColor {
|
||||||
|
if let configuration {
|
||||||
|
return configuration.titleNormalColor(at: index)
|
||||||
|
} else {
|
||||||
|
return titleNormalColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private func innerTitleSelectedColor(at index: Int) -> UIColor {
|
||||||
|
if let configuration {
|
||||||
|
return configuration.titleSelectedColor(at: index)
|
||||||
|
} else {
|
||||||
|
return titleSelectedColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private func innerTitleNormalFont(at index: Int) -> UIFont {
|
||||||
|
if let configuration {
|
||||||
|
return configuration.titleNormalFont(at: index)
|
||||||
|
} else {
|
||||||
|
return titleNormalFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private func innerTitleSelectedFont(at index: Int) -> UIFont? {
|
||||||
|
if let configuration {
|
||||||
|
return configuration.titleSelectedFont(at: index)
|
||||||
|
} else {
|
||||||
|
return titleSelectedFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleDynamicConfiguration.swift
generated
Normal file
17
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleDynamicConfiguration.swift
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleBaseDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by Jiaxin Pu on 2024/4/16.
|
||||||
|
// Copyright © 2024 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public protocol JXSegmentedTitleDynamicConfiguration {
|
||||||
|
func titleNumberOfLines(at index: Int) -> Int
|
||||||
|
func titleNormalColor(at index: Int) -> UIColor
|
||||||
|
func titleSelectedColor(at index: Int) -> UIColor
|
||||||
|
func titleNormalFont(at index: Int) -> UIFont
|
||||||
|
func titleSelectedFont(at index: Int) -> UIFont?
|
||||||
|
}
|
||||||
29
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleItemModel.swift
generated
Normal file
29
Pods/JXSegmentedView/Sources/Title/JXSegmentedTitleItemModel.swift
generated
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/26.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleItemModel: JXSegmentedBaseItemModel {
|
||||||
|
open var title: String?
|
||||||
|
open var titleNumberOfLines: Int = 0
|
||||||
|
open var titleNormalColor: UIColor = .black
|
||||||
|
open var titleCurrentColor: UIColor = .black
|
||||||
|
open var titleSelectedColor: UIColor = .red
|
||||||
|
open var titleNormalFont: UIFont = UIFont.systemFont(ofSize: 15)
|
||||||
|
open var titleSelectedFont: UIFont = UIFont.systemFont(ofSize: 15)
|
||||||
|
open var isTitleZoomEnabled: Bool = false
|
||||||
|
open var titleNormalZoomScale: CGFloat = 0
|
||||||
|
open var titleCurrentZoomScale: CGFloat = 0
|
||||||
|
open var titleSelectedZoomScale: CGFloat = 0
|
||||||
|
open var isTitleStrokeWidthEnabled: Bool = false
|
||||||
|
open var titleNormalStrokeWidth: CGFloat = 0
|
||||||
|
open var titleCurrentStrokeWidth: CGFloat = 0
|
||||||
|
open var titleSelectedStrokeWidth: CGFloat = 0
|
||||||
|
open var isTitleMaskEnabled: Bool = false
|
||||||
|
open var textWidth: CGFloat = 0
|
||||||
|
}
|
||||||
76
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientCell.swift
generated
Normal file
76
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientCell.swift
generated
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleGradientCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/23.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleGradientCell: JXSegmentedTitleCell {
|
||||||
|
public let gradientLayer = CAGradientLayer()
|
||||||
|
private var canStartSelectedAnimation: Bool = false
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
titleLabel.removeFromSuperview()
|
||||||
|
maskTitleLabel.removeFromSuperview()
|
||||||
|
|
||||||
|
gradientLayer.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor]
|
||||||
|
contentView.layer.addSublayer(gradientLayer)
|
||||||
|
gradientLayer.mask = titleLabel.layer
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientLayer.frame = titleLabel.frame
|
||||||
|
CATransaction.commit()
|
||||||
|
titleLabel.frame = gradientLayer.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleGradientItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if myItemModel.isSelectedAnimable && canStartSelectedAnimation(itemModel: myItemModel, selectedType: selectedType) {
|
||||||
|
let closure: JXSegmentedCellSelectedAnimationClosure = {[weak self] (percent) in
|
||||||
|
if myItemModel.isSelected {
|
||||||
|
myItemModel.titleCurrentGradientColors = JXSegmentedViewTool.interpolateColors(from: myItemModel.titleNormalGradientColors, to: myItemModel.titleSelectedGradientColors, percent: percent)
|
||||||
|
}else {
|
||||||
|
myItemModel.titleCurrentGradientColors = JXSegmentedViewTool.interpolateColors(from: myItemModel.titleSelectedGradientColors, to: myItemModel.titleNormalGradientColors, percent: percent)
|
||||||
|
}
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
self?.gradientLayer.colors = myItemModel.titleCurrentGradientColors
|
||||||
|
CATransaction.commit()
|
||||||
|
self?.setNeedsLayout()
|
||||||
|
self?.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
appendSelectedAnimationClosure(closure: closure)
|
||||||
|
canStartSelectedAnimation = true
|
||||||
|
startSelectedAnimationIfNeeded(itemModel: myItemModel, selectedType: selectedType)
|
||||||
|
canStartSelectedAnimation = false
|
||||||
|
}else {
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
gradientLayer.startPoint = myItemModel.titleGradientStartPoint
|
||||||
|
gradientLayer.endPoint = myItemModel.titleGradientEndPoint
|
||||||
|
gradientLayer.colors = myItemModel.titleCurrentGradientColors
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func startSelectedAnimationIfNeeded(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
if canStartSelectedAnimation {
|
||||||
|
super.startSelectedAnimationIfNeeded(itemModel: itemModel, selectedType: selectedType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientDataSource.swift
generated
Normal file
76
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientDataSource.swift
generated
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleGradientDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/23.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleGradientDataSource: JXSegmentedTitleDataSource {
|
||||||
|
/// title普通状态下的渐变colors
|
||||||
|
open var titleNormalGradientColors: [CGColor] = [UIColor.black.cgColor, UIColor.black.cgColor, UIColor.black.cgColor]
|
||||||
|
/// title选中状态下的渐变colors
|
||||||
|
open var titleSelectedGradientColors: [CGColor] = [UIColor(red: 18/255.0, green: 194/255.0, blue: 233/255.0, alpha: 1).cgColor, UIColor(red: 196/255.0, green: 113/255.0, blue: 237/255.0, alpha: 1).cgColor, UIColor(red: 246/255.0, green: 79/255.0, blue: 89/255.0, alpha: 1).cgColor]
|
||||||
|
/// title渐变的StartPoint
|
||||||
|
open var titleGradientStartPoint: CGPoint = CGPoint(x: 0, y: 0)
|
||||||
|
/// title渐变的EndPoint
|
||||||
|
open var titleGradientEndPoint: CGPoint = CGPoint(x: 1, y: 0)
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedTitleGradientItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let itemModel = itemModel as? JXSegmentedTitleGradientItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModel.titleGradientStartPoint = titleGradientStartPoint
|
||||||
|
itemModel.titleGradientEndPoint = titleGradientEndPoint
|
||||||
|
itemModel.titleNormalGradientColors = titleNormalGradientColors
|
||||||
|
itemModel.titleSelectedGradientColors = titleSelectedGradientColors
|
||||||
|
if index == selectedIndex {
|
||||||
|
itemModel.titleCurrentGradientColors = itemModel.titleSelectedGradientColors
|
||||||
|
}else {
|
||||||
|
itemModel.titleCurrentGradientColors = itemModel.titleNormalGradientColors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedTitleGradientCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat) {
|
||||||
|
super.refreshItemModel(segmentedView, leftItemModel: leftItemModel, rightItemModel: rightItemModel, percent: percent)
|
||||||
|
|
||||||
|
guard let leftModel = leftItemModel as? JXSegmentedTitleGradientItemModel, let rightModel = rightItemModel as? JXSegmentedTitleGradientItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTitleColorGradientEnabled && isItemTransitionEnabled {
|
||||||
|
leftModel.titleCurrentGradientColors = JXSegmentedViewTool.interpolateColors(from: leftModel.titleSelectedGradientColors, to: leftModel.titleNormalGradientColors, percent: percent)
|
||||||
|
rightModel.titleCurrentGradientColors = JXSegmentedViewTool.interpolateColors(from: rightModel.titleNormalGradientColors, to: rightModel.titleSelectedGradientColors, percent: percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.refreshItemModel(segmentedView, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
guard let myCurrentSelectedItemModel = currentSelectedItemModel as? JXSegmentedTitleGradientItemModel, let myWillSelectedItemModel = willSelectedItemModel as? JXSegmentedTitleGradientItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myCurrentSelectedItemModel.titleCurrentGradientColors = myCurrentSelectedItemModel.titleNormalGradientColors
|
||||||
|
myWillSelectedItemModel.titleCurrentGradientColors = myWillSelectedItemModel.titleSelectedGradientColors
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientItemModel.swift
generated
Normal file
17
Pods/JXSegmentedView/Sources/TitleGradient/JXSegmentedTitleGradientItemModel.swift
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleGradientItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/23.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleGradientItemModel: JXSegmentedTitleItemModel {
|
||||||
|
open var titleNormalGradientColors: [CGColor] = [CGColor]()
|
||||||
|
open var titleCurrentGradientColors: [CGColor] = [CGColor]()
|
||||||
|
open var titleSelectedGradientColors: [CGColor] = [CGColor]()
|
||||||
|
open var titleGradientStartPoint: CGPoint = CGPoint.zero
|
||||||
|
open var titleGradientEndPoint: CGPoint = CGPoint.zero
|
||||||
|
}
|
||||||
100
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageCell.swift
generated
Normal file
100
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageCell.swift
generated
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleImageCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/29.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleImageCell: JXSegmentedTitleCell {
|
||||||
|
public let imageView = UIImageView()
|
||||||
|
private var currentImageInfo: String?
|
||||||
|
|
||||||
|
open override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
currentImageInfo = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageSize = myItemModel.imageSize
|
||||||
|
switch myItemModel.titleImageType {
|
||||||
|
case .topImage:
|
||||||
|
let contentHeight = imageSize.height + myItemModel.titleImageSpacing + titleLabel.bounds.size.height
|
||||||
|
imageView.center = CGPoint(x: contentView.bounds.size.width/2, y: (contentView.bounds.size.height - contentHeight)/2 + imageSize.height/2)
|
||||||
|
titleLabel.center = CGPoint(x: contentView.bounds.size.width/2, y: imageView.frame.maxY + myItemModel.titleImageSpacing + titleLabel.bounds.size.height/2)
|
||||||
|
case .leftImage:
|
||||||
|
let contentWidth = imageSize.width + myItemModel.titleImageSpacing + titleLabel.bounds.size.width
|
||||||
|
imageView.center = CGPoint(x: (contentView.bounds.size.width - contentWidth)/2 + imageSize.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
titleLabel.center = CGPoint(x: imageView.frame.maxX + myItemModel.titleImageSpacing + titleLabel.bounds.size.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
case .bottomImage:
|
||||||
|
let contentHeight = imageSize.height + myItemModel.titleImageSpacing + titleLabel.bounds.size.height
|
||||||
|
titleLabel.center = CGPoint(x: contentView.bounds.size.width/2, y: (contentView.bounds.size.height - contentHeight)/2 + titleLabel.bounds.size.height/2)
|
||||||
|
imageView.center = CGPoint(x: contentView.bounds.size.width/2, y: titleLabel.frame.maxY + myItemModel.titleImageSpacing + imageSize.height/2)
|
||||||
|
case .rightImage:
|
||||||
|
let contentWidth = imageSize.width + myItemModel.titleImageSpacing + titleLabel.bounds.size.width
|
||||||
|
titleLabel.center = CGPoint(x: (contentView.bounds.size.width - contentWidth)/2 + titleLabel.bounds.size.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
imageView.center = CGPoint(x: titleLabel.frame.maxX + myItemModel.titleImageSpacing + imageSize.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
case .onlyImage:
|
||||||
|
imageView.center = CGPoint(x: contentView.bounds.size.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
case .onlyTitle:
|
||||||
|
titleLabel.center = CGPoint(x: contentView.bounds.size.width/2, y: contentView.bounds.size.height/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
titleLabel.isHidden = false
|
||||||
|
imageView.isHidden = false
|
||||||
|
if myItemModel.titleImageType == .onlyTitle {
|
||||||
|
imageView.isHidden = true
|
||||||
|
}else if myItemModel.titleImageType == .onlyImage {
|
||||||
|
titleLabel.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.bounds = CGRect(x: 0, y: 0, width: myItemModel.imageSize.width, height: myItemModel.imageSize.height)
|
||||||
|
|
||||||
|
var normalImageInfo = myItemModel.normalImageInfo
|
||||||
|
if myItemModel.isSelected && myItemModel.selectedImageInfo != nil {
|
||||||
|
normalImageInfo = myItemModel.selectedImageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
//因为`func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType)`方法会回调多次,尤其是左右滚动的时候会调用无数次。如果每次都触发图片加载,会非常消耗性能。所以只会在图片发生了变化的时候,才进行图片加载。
|
||||||
|
if normalImageInfo != nil && normalImageInfo != currentImageInfo {
|
||||||
|
currentImageInfo = normalImageInfo
|
||||||
|
if myItemModel.loadImageClosure != nil {
|
||||||
|
myItemModel.loadImageClosure!(imageView, normalImageInfo!)
|
||||||
|
}else {
|
||||||
|
imageView.image = UIImage(named: normalImageInfo!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if myItemModel.isImageZoomEnabled {
|
||||||
|
imageView.transform = CGAffineTransform(scaleX: myItemModel.imageCurrentZoomScale, y: myItemModel.imageCurrentZoomScale)
|
||||||
|
}else {
|
||||||
|
imageView.transform = .identity
|
||||||
|
}
|
||||||
|
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageDataSource.swift
generated
Normal file
130
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageDataSource.swift
generated
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleImageDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/29.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum JXSegmentedTitleImageType {
|
||||||
|
case topImage
|
||||||
|
case leftImage
|
||||||
|
case bottomImage
|
||||||
|
case rightImage
|
||||||
|
case onlyImage
|
||||||
|
case onlyTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias LoadImageClosure = ((UIImageView, String) -> Void)
|
||||||
|
|
||||||
|
open class JXSegmentedTitleImageDataSource: JXSegmentedTitleDataSource {
|
||||||
|
open var titleImageType: JXSegmentedTitleImageType = .rightImage
|
||||||
|
/// 数量需要和item的数量保持一致。可以是ImageName或者图片网络地址
|
||||||
|
open var normalImageInfos: [String]?
|
||||||
|
/// 数量需要和item的数量保持一致。可以是ImageName或者图片网络地址。如果不赋值,选中时就不会处理图片切换。
|
||||||
|
open var selectedImageInfos: [String]?
|
||||||
|
/// 内部默认通过UIImage(named:)加载图片。如果传递的是图片网络地址或者想自己处理图片加载逻辑,可以通过该闭包处理。
|
||||||
|
open var loadImageClosure: LoadImageClosure?
|
||||||
|
/// 图片尺寸
|
||||||
|
open var imageSize: CGSize = CGSize(width: 20, height: 20)
|
||||||
|
/// title和image之间的间隔
|
||||||
|
open var titleImageSpacing: CGFloat = 5
|
||||||
|
/// 是否开启图片缩放
|
||||||
|
open var isImageZoomEnabled: Bool = false
|
||||||
|
/// 图片缩放选中时的scale
|
||||||
|
open var imageSelectedZoomScale: CGFloat = 1.2
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedTitleImageItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let itemModel = itemModel as? JXSegmentedTitleImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModel.titleImageType = titleImageType
|
||||||
|
itemModel.normalImageInfo = normalImageInfos?[index]
|
||||||
|
itemModel.selectedImageInfo = selectedImageInfos?[index]
|
||||||
|
itemModel.loadImageClosure = loadImageClosure
|
||||||
|
itemModel.imageSize = imageSize
|
||||||
|
itemModel.isImageZoomEnabled = isImageZoomEnabled
|
||||||
|
itemModel.imageNormalZoomScale = 1
|
||||||
|
itemModel.imageSelectedZoomScale = imageSelectedZoomScale
|
||||||
|
itemModel.titleImageSpacing = titleImageSpacing
|
||||||
|
if index == selectedIndex {
|
||||||
|
itemModel.imageCurrentZoomScale = itemModel.imageSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
itemModel.imageCurrentZoomScale = itemModel.imageNormalZoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredSegmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat {
|
||||||
|
var width = super.preferredSegmentedView(segmentedView, widthForItemAt: index)
|
||||||
|
if itemWidth == JXSegmentedViewAutomaticDimension {
|
||||||
|
switch titleImageType {
|
||||||
|
case .leftImage, .rightImage:
|
||||||
|
width += titleImageSpacing + imageSize.width
|
||||||
|
case .topImage, .bottomImage:
|
||||||
|
width = max(itemWidth, imageSize.width)
|
||||||
|
case .onlyImage:
|
||||||
|
width = imageSize.width
|
||||||
|
case .onlyTitle:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat {
|
||||||
|
var width = super.segmentedView(segmentedView, widthForItemContentAt: index)
|
||||||
|
switch titleImageType {
|
||||||
|
case .leftImage, .rightImage:
|
||||||
|
width += titleImageSpacing + imageSize.width
|
||||||
|
case .topImage, .bottomImage:
|
||||||
|
width = max(itemWidth, imageSize.width)
|
||||||
|
case .onlyImage:
|
||||||
|
width = imageSize.width
|
||||||
|
case .onlyTitle:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedTitleImageCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat) {
|
||||||
|
super.refreshItemModel(segmentedView, leftItemModel: leftItemModel, rightItemModel: rightItemModel, percent: percent)
|
||||||
|
|
||||||
|
guard let leftModel = leftItemModel as? JXSegmentedTitleImageItemModel, let rightModel = rightItemModel as? JXSegmentedTitleImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isImageZoomEnabled && isItemTransitionEnabled {
|
||||||
|
leftModel.imageCurrentZoomScale = JXSegmentedViewTool.interpolate(from: imageSelectedZoomScale, to: 1, percent: CGFloat(percent))
|
||||||
|
rightModel.imageCurrentZoomScale = JXSegmentedViewTool.interpolate(from: 1, to: imageSelectedZoomScale, percent: CGFloat(percent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.refreshItemModel(segmentedView, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||||||
|
|
||||||
|
guard let myCurrentSelectedItemModel = currentSelectedItemModel as? JXSegmentedTitleImageItemModel, let myWillSelectedItemModel = willSelectedItemModel as? JXSegmentedTitleImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myCurrentSelectedItemModel.imageCurrentZoomScale = myCurrentSelectedItemModel.imageNormalZoomScale
|
||||||
|
myWillSelectedItemModel.imageCurrentZoomScale = myWillSelectedItemModel.imageSelectedZoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageItemModel.swift
generated
Normal file
22
Pods/JXSegmentedView/Sources/TitleImage/JXSegmentedTitleImageItemModel.swift
generated
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleImageItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2018/12/29.
|
||||||
|
// Copyright © 2018 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleImageItemModel: JXSegmentedTitleItemModel {
|
||||||
|
open var titleImageType: JXSegmentedTitleImageType = .rightImage
|
||||||
|
open var normalImageInfo: String?
|
||||||
|
open var selectedImageInfo: String?
|
||||||
|
open var loadImageClosure: LoadImageClosure?
|
||||||
|
open var imageSize: CGSize = CGSize.zero
|
||||||
|
open var titleImageSpacing: CGFloat = 0
|
||||||
|
open var isImageZoomEnabled: Bool = false
|
||||||
|
open var imageNormalZoomScale: CGFloat = 0
|
||||||
|
open var imageCurrentZoomScale: CGFloat = 0
|
||||||
|
open var imageSelectedZoomScale: CGFloat = 0
|
||||||
|
}
|
||||||
141
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageCell.swift
generated
Normal file
141
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageCell.swift
generated
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleOrImageCell.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/22.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleOrImageCell: JXSegmentedTitleCell {
|
||||||
|
public let imageView = UIImageView()
|
||||||
|
private var currentImageInfo: String?
|
||||||
|
|
||||||
|
open override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
currentImageInfo = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func commonInit() {
|
||||||
|
super.commonInit()
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
imageView.center = contentView.center
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
|
||||||
|
super.reloadData(itemModel: itemModel, selectedType: selectedType )
|
||||||
|
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleOrImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if myItemModel.isSelected && myItemModel.selectedImageInfo != nil {
|
||||||
|
titleLabel.isHidden = true
|
||||||
|
imageView.isHidden = false
|
||||||
|
}else {
|
||||||
|
titleLabel.isHidden = false
|
||||||
|
imageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.bounds = CGRect(x: 0, y: 0, width: myItemModel.imageSize.width, height: myItemModel.imageSize.height)
|
||||||
|
|
||||||
|
//因为`func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType)`方法会回调多次,尤其是左右滚动的时候会调用无数次。如果每次都触发图片加载,会非常消耗性能。所以只会在图片发生了变化的时候,才进行图片加载。
|
||||||
|
if myItemModel.isSelected &&
|
||||||
|
myItemModel.selectedImageInfo != nil &&
|
||||||
|
myItemModel.selectedImageInfo != currentImageInfo {
|
||||||
|
currentImageInfo = myItemModel.selectedImageInfo
|
||||||
|
if myItemModel.loadImageClosure != nil {
|
||||||
|
myItemModel.loadImageClosure!(imageView, myItemModel.selectedImageInfo!)
|
||||||
|
}else {
|
||||||
|
imageView.image = UIImage(named: myItemModel.selectedImageInfo!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredTitleZoomAnimateClosure(itemModel: JXSegmentedTitleItemModel, baseScale: CGFloat) -> JXSegmentedCellSelectedAnimationClosure {
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleOrImageItemModel else {
|
||||||
|
return super.preferredTitleZoomAnimateClosure(itemModel: itemModel, baseScale: baseScale)
|
||||||
|
}
|
||||||
|
if myItemModel.selectedImageInfo == nil && myItemModel.isSelected {
|
||||||
|
//当前item没有选中图片且是将要选中的时候才做动画
|
||||||
|
return super.preferredTitleZoomAnimateClosure(itemModel: itemModel, baseScale: baseScale)
|
||||||
|
}else {
|
||||||
|
let closure: JXSegmentedCellSelectedAnimationClosure = {[weak self] (percent) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中
|
||||||
|
itemModel.titleCurrentZoomScale = itemModel.titleSelectedZoomScale
|
||||||
|
}else {
|
||||||
|
//将要取消选中
|
||||||
|
itemModel.titleCurrentZoomScale = itemModel.titleNormalZoomScale
|
||||||
|
}
|
||||||
|
let currentTransform = CGAffineTransform(scaleX: baseScale*itemModel.titleCurrentZoomScale, y: baseScale*itemModel.titleCurrentZoomScale)
|
||||||
|
self?.titleLabel.transform = currentTransform
|
||||||
|
self?.maskTitleLabel.transform = currentTransform
|
||||||
|
}
|
||||||
|
//手动调用closure,更新到最新状态
|
||||||
|
closure(0)
|
||||||
|
return closure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredTitleStrokeWidthAnimateClosure(itemModel: JXSegmentedTitleItemModel, attriText: NSMutableAttributedString) -> JXSegmentedCellSelectedAnimationClosure {
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleOrImageItemModel else {
|
||||||
|
return super.preferredTitleStrokeWidthAnimateClosure(itemModel: itemModel, attriText: attriText)
|
||||||
|
}
|
||||||
|
if myItemModel.selectedImageInfo == nil && myItemModel.isSelected {
|
||||||
|
//当前item没有选中图片且是将要选中的时候才做动画
|
||||||
|
return super.preferredTitleStrokeWidthAnimateClosure(itemModel: itemModel, attriText: attriText)
|
||||||
|
}else {
|
||||||
|
let closure: JXSegmentedCellSelectedAnimationClosure = {[weak self] (percent) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中
|
||||||
|
itemModel.titleCurrentStrokeWidth = itemModel.titleSelectedStrokeWidth
|
||||||
|
}else {
|
||||||
|
//将要取消选中
|
||||||
|
itemModel.titleCurrentStrokeWidth = itemModel.titleNormalStrokeWidth
|
||||||
|
}
|
||||||
|
attriText.addAttributes([NSAttributedString.Key.strokeWidth: itemModel.titleCurrentStrokeWidth], range: NSRange(location: 0, length: attriText.string.count))
|
||||||
|
self?.titleLabel.attributedText = attriText
|
||||||
|
}
|
||||||
|
//手动调用closure,更新到最新状态
|
||||||
|
closure(0)
|
||||||
|
return closure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredTitleColorAnimateClosure(itemModel: JXSegmentedTitleItemModel) -> JXSegmentedCellSelectedAnimationClosure {
|
||||||
|
guard let myItemModel = itemModel as? JXSegmentedTitleOrImageItemModel else {
|
||||||
|
return super.preferredTitleColorAnimateClosure(itemModel: itemModel)
|
||||||
|
}
|
||||||
|
if myItemModel.selectedImageInfo == nil && myItemModel.isSelected {
|
||||||
|
//当前item没有选中图片且是将要选中的时候才做动画
|
||||||
|
return super.preferredTitleColorAnimateClosure(itemModel: itemModel)
|
||||||
|
}else {
|
||||||
|
let closure: JXSegmentedCellSelectedAnimationClosure = {[weak self] (percent) in
|
||||||
|
if itemModel.isSelected {
|
||||||
|
//将要选中
|
||||||
|
itemModel.titleCurrentColor = itemModel.titleSelectedColor
|
||||||
|
}else {
|
||||||
|
//将要取消选中
|
||||||
|
itemModel.titleCurrentColor = itemModel.titleNormalColor
|
||||||
|
}
|
||||||
|
self?.titleLabel.textColor = itemModel.titleCurrentColor
|
||||||
|
}
|
||||||
|
//手动调用closure,更新到最新状态
|
||||||
|
closure(0)
|
||||||
|
return closure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageDataSource.swift
generated
Normal file
51
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageDataSource.swift
generated
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleOrImageDataSource.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/22.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleOrImageDataSource: JXSegmentedTitleDataSource {
|
||||||
|
/// 数量需要和item的数量保持一致。可以是ImageName或者图片地址。选中时不显示图片就填nil
|
||||||
|
open var selectedImageInfos: [String?]?
|
||||||
|
/// 内部默认通过UIImage(named:)加载图片。如果传递的是图片地址或者想自己处理图片加载逻辑,可以通过该闭包处理。
|
||||||
|
open var loadImageClosure: LoadImageClosure?
|
||||||
|
/// 图片尺寸
|
||||||
|
open var imageSize: CGSize = CGSize(width: 30, height: 30)
|
||||||
|
|
||||||
|
open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel {
|
||||||
|
return JXSegmentedTitleOrImageItemModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func reloadData(selectedIndex: Int) {
|
||||||
|
selectedAnimationDuration = 0.1
|
||||||
|
|
||||||
|
super.reloadData(selectedIndex: selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func preferredRefreshItemModel( _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) {
|
||||||
|
super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex)
|
||||||
|
|
||||||
|
guard let itemModel = itemModel as? JXSegmentedTitleOrImageItemModel else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
itemModel.selectedImageInfo = selectedImageInfos?[index]
|
||||||
|
itemModel.loadImageClosure = loadImageClosure
|
||||||
|
itemModel.imageSize = imageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - JXSegmentedViewDataSource
|
||||||
|
open override func registerCellClass(in segmentedView: JXSegmentedView) {
|
||||||
|
segmentedView.collectionView.register(JXSegmentedTitleOrImageCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
|
||||||
|
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageItemModel.swift
generated
Normal file
15
Pods/JXSegmentedView/Sources/TitleOrImage/JXSegmentedTitleOrImageItemModel.swift
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// JXSegmentedTitleOrImageItemModel.swift
|
||||||
|
// JXSegmentedView
|
||||||
|
//
|
||||||
|
// Created by jiaxin on 2019/1/22.
|
||||||
|
// Copyright © 2019 jiaxin. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
open class JXSegmentedTitleOrImageItemModel: JXSegmentedTitleItemModel {
|
||||||
|
open var selectedImageInfo: String?
|
||||||
|
open var loadImageClosure: LoadImageClosure?
|
||||||
|
open var imageSize: CGSize = CGSize.zero
|
||||||
|
}
|
||||||
10
Pods/Manifest.lock
generated
10
Pods/Manifest.lock
generated
@ -1,6 +1,8 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.9.1)
|
- Alamofire (5.9.1)
|
||||||
- IQKeyboardManagerSwift (6.5.13)
|
- IQKeyboardManagerSwift (6.5.13)
|
||||||
|
- JXPagingView/Paging (2.1.3)
|
||||||
|
- JXSegmentedView (1.3.3)
|
||||||
- Kingfisher (7.11.0)
|
- Kingfisher (7.11.0)
|
||||||
- SnapKit (5.6.0)
|
- SnapKit (5.6.0)
|
||||||
- SVProgressHUD (2.2.5)
|
- SVProgressHUD (2.2.5)
|
||||||
@ -9,6 +11,8 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JXPagingView/Paging
|
||||||
|
- JXSegmentedView
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- SnapKit
|
- SnapKit
|
||||||
- SVProgressHUD
|
- SVProgressHUD
|
||||||
@ -18,6 +22,8 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- Alamofire
|
- Alamofire
|
||||||
- IQKeyboardManagerSwift
|
- IQKeyboardManagerSwift
|
||||||
|
- JXPagingView
|
||||||
|
- JXSegmentedView
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- SnapKit
|
- SnapKit
|
||||||
- SVProgressHUD
|
- SVProgressHUD
|
||||||
@ -26,11 +32,13 @@ SPEC REPOS:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c
|
||||||
IQKeyboardManagerSwift: 69b5fb9960edff37263d45c44fd97427e1f8c22b
|
IQKeyboardManagerSwift: 69b5fb9960edff37263d45c44fd97427e1f8c22b
|
||||||
|
JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e
|
||||||
|
JXSegmentedView: 651b60fcf705258ba9395edd53876dbd2853fb68
|
||||||
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
Kingfisher: b9c985d864d43515f404f1ef4a8ce7d802ace3ac
|
||||||
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
|
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
|
||||||
SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
|
SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
|
||||||
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
SwiftDate: 72d28954e8e1c6c1c0f917ccc8005e4f83c7d4b2
|
||||||
|
|
||||||
PODFILE CHECKSUM: d3eda313abd465652ca5c62aaab483d8f02ed2d2
|
PODFILE CHECKSUM: c0bbca53cdc7c53ea7a9dbc8d75b7ae964a1dbc2
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
3020
Pods/Pods.xcodeproj/project.pbxproj
generated
3020
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
26
Pods/Target Support Files/JXPagingView/JXPagingView-Info.plist
generated
Normal file
26
Pods/Target Support Files/JXPagingView/JXPagingView-Info.plist
generated
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>${EXECUTABLE_NAME}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>${PRODUCT_NAME}</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>2.1.3</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
5
Pods/Target Support Files/JXPagingView/JXPagingView-dummy.m
generated
Normal file
5
Pods/Target Support Files/JXPagingView/JXPagingView-dummy.m
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
@interface PodsDummy_JXPagingView : NSObject
|
||||||
|
@end
|
||||||
|
@implementation PodsDummy_JXPagingView
|
||||||
|
@end
|
||||||
12
Pods/Target Support Files/JXPagingView/JXPagingView-prefix.pch
generated
Normal file
12
Pods/Target Support Files/JXPagingView/JXPagingView-prefix.pch
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#ifdef __OBJC__
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#else
|
||||||
|
#ifndef FOUNDATION_EXPORT
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
#define FOUNDATION_EXPORT extern "C"
|
||||||
|
#else
|
||||||
|
#define FOUNDATION_EXPORT extern
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
16
Pods/Target Support Files/JXPagingView/JXPagingView-umbrella.h
generated
Normal file
16
Pods/Target Support Files/JXPagingView/JXPagingView-umbrella.h
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#ifdef __OBJC__
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#else
|
||||||
|
#ifndef FOUNDATION_EXPORT
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
#define FOUNDATION_EXPORT extern "C"
|
||||||
|
#else
|
||||||
|
#define FOUNDATION_EXPORT extern
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
FOUNDATION_EXPORT double JXPagingViewVersionNumber;
|
||||||
|
FOUNDATION_EXPORT const unsigned char JXPagingViewVersionString[];
|
||||||
|
|
||||||
14
Pods/Target Support Files/JXPagingView/JXPagingView.debug.xcconfig
generated
Normal file
14
Pods/Target Support Files/JXPagingView/JXPagingView.debug.xcconfig
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JXPagingView
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
6
Pods/Target Support Files/JXPagingView/JXPagingView.modulemap
generated
Normal file
6
Pods/Target Support Files/JXPagingView/JXPagingView.modulemap
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
framework module JXPagingView {
|
||||||
|
umbrella header "JXPagingView-umbrella.h"
|
||||||
|
|
||||||
|
export *
|
||||||
|
module * { export * }
|
||||||
|
}
|
||||||
14
Pods/Target Support Files/JXPagingView/JXPagingView.release.xcconfig
generated
Normal file
14
Pods/Target Support Files/JXPagingView/JXPagingView.release.xcconfig
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||||
|
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JXPagingView
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||||
|
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||||
|
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||||
|
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||||
|
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||||
|
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||||
|
PODS_ROOT = ${SRCROOT}
|
||||||
|
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JXPagingView
|
||||||
|
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||||
|
SKIP_INSTALL = YES
|
||||||
|
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||||
24
Pods/Target Support Files/JXPagingView/ResourceBundle-JXPagingView-JXPagingView-Info.plist
generated
Normal file
24
Pods/Target Support Files/JXPagingView/ResourceBundle-JXPagingView-JXPagingView-Info.plist
generated
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>${PRODUCT_NAME}</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>2.1.3</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
26
Pods/Target Support Files/JXSegmentedView/JXSegmentedView-Info.plist
generated
Normal file
26
Pods/Target Support Files/JXSegmentedView/JXSegmentedView-Info.plist
generated
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>${PODS_DEVELOPMENT_LANGUAGE}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>${EXECUTABLE_NAME}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>${PRODUCT_NAME}</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.3.3</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
5
Pods/Target Support Files/JXSegmentedView/JXSegmentedView-dummy.m
generated
Normal file
5
Pods/Target Support Files/JXSegmentedView/JXSegmentedView-dummy.m
generated
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
@interface PodsDummy_JXSegmentedView : NSObject
|
||||||
|
@end
|
||||||
|
@implementation PodsDummy_JXSegmentedView
|
||||||
|
@end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user