├── .DS_Store ├── Demo ├── .DS_Store ├── SPCycleScrollView.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── leshengping.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── leshengping.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── SPCycleScrollView │ ├── .DS_Store │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── currentDot.imageset │ │ │ ├── Contents.json │ │ │ ├── 切换图标@2x.png │ │ │ └── 切换图标@3x.png │ │ └── otherDot.imageset │ │ │ ├── Contents.json │ │ │ ├── 切换小圆@2x.png │ │ │ └── 切换小圆@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── Resource │ │ ├── dog.gif │ │ ├── placeholder.png │ │ ├── 景1.jpg │ │ ├── 景2.jpg │ │ ├── 景3.jpg │ │ ├── 景4.jpg │ │ ├── 景5.jpg │ │ ├── 笑脸red.png │ │ └── 笑脸yellow.png │ ├── SPCycleScrollView │ │ ├── SPCycleScrollView.h │ │ ├── SPCycleScrollView.m │ │ ├── SPPageControl.h │ │ └── SPageControl.m │ ├── SecondViewController.h │ ├── SecondViewController.m │ ├── ViewController.h │ ├── ViewController.m │ └── main.m ├── SPCycleScrollViewTests │ ├── Info.plist │ └── SPCycleScrollViewTests.m └── SPCycleScrollViewUITests │ ├── Info.plist │ └── SPCycleScrollViewUITests.m ├── README.md ├── SPCycleScrollView.podspec └── SPCycleScrollView ├── SPCycleScrollView.h ├── SPCycleScrollView.m ├── SPPageControl.h └── SPageControl.m /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/.DS_Store -------------------------------------------------------------------------------- /Demo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/.DS_Store -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 52010DC72186DEA50094EFB0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DC62186DEA50094EFB0 /* AppDelegate.m */; }; 11 | 52010DCA2186DEA50094EFB0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DC92186DEA50094EFB0 /* ViewController.m */; }; 12 | 52010DCD2186DEA50094EFB0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52010DCB2186DEA50094EFB0 /* Main.storyboard */; }; 13 | 52010DCF2186DEA80094EFB0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52010DCE2186DEA80094EFB0 /* Assets.xcassets */; }; 14 | 52010DD22186DEA80094EFB0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52010DD02186DEA80094EFB0 /* LaunchScreen.storyboard */; }; 15 | 52010DD52186DEA80094EFB0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DD42186DEA80094EFB0 /* main.m */; }; 16 | 52010DDF2186DEA80094EFB0 /* SPCycleScrollViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DDE2186DEA80094EFB0 /* SPCycleScrollViewTests.m */; }; 17 | 52010DEA2186DEA80094EFB0 /* SPCycleScrollViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DE92186DEA80094EFB0 /* SPCycleScrollViewUITests.m */; }; 18 | 52010DF92186DEE00094EFB0 /* SPCycleScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010DF82186DEE00094EFB0 /* SPCycleScrollView.m */; }; 19 | 52010E382186E1330094EFB0 /* SPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010E362186E1320094EFB0 /* SPageControl.m */; }; 20 | 52010E3B2186E22B0094EFB0 /* SecondViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 52010E392186E22A0094EFB0 /* SecondViewController.m */; }; 21 | 52010E442186E2520094EFB0 /* 笑脸red.png in Resources */ = {isa = PBXBuildFile; fileRef = 52010E3D2186E2500094EFB0 /* 笑脸red.png */; }; 22 | 52010E452186E2520094EFB0 /* 景2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 52010E3E2186E2500094EFB0 /* 景2.jpg */; }; 23 | 52010E462186E2520094EFB0 /* 景1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 52010E3F2186E2510094EFB0 /* 景1.jpg */; }; 24 | 52010E472186E2520094EFB0 /* 笑脸yellow.png in Resources */ = {isa = PBXBuildFile; fileRef = 52010E402186E2510094EFB0 /* 笑脸yellow.png */; }; 25 | 52010E482186E2520094EFB0 /* 景3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 52010E412186E2510094EFB0 /* 景3.jpg */; }; 26 | 52010E492186E2520094EFB0 /* 景4.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 52010E422186E2510094EFB0 /* 景4.jpg */; }; 27 | 52010E4A2186E2520094EFB0 /* 景5.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 52010E432186E2510094EFB0 /* 景5.jpg */; }; 28 | 5202FA532193E0B600FD9E03 /* dog.gif in Resources */ = {isa = PBXBuildFile; fileRef = 5202FA522193E0B600FD9E03 /* dog.gif */; }; 29 | 5202FA552194020400FD9E03 /* placeholder.png in Resources */ = {isa = PBXBuildFile; fileRef = 5202FA542194020300FD9E03 /* placeholder.png */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | 52010DDB2186DEA80094EFB0 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 52010DBA2186DEA50094EFB0 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 52010DC12186DEA50094EFB0; 38 | remoteInfo = SPCycleScrollView; 39 | }; 40 | 52010DE62186DEA80094EFB0 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 52010DBA2186DEA50094EFB0 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 52010DC12186DEA50094EFB0; 45 | remoteInfo = SPCycleScrollView; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 52010DC22186DEA50094EFB0 /* SPCycleScrollView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SPCycleScrollView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 52010DC52186DEA50094EFB0 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 52 | 52010DC62186DEA50094EFB0 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 53 | 52010DC82186DEA50094EFB0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 54 | 52010DC92186DEA50094EFB0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 55 | 52010DCC2186DEA50094EFB0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 52010DCE2186DEA80094EFB0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 52010DD12186DEA80094EFB0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 52010DD32186DEA80094EFB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 52010DD42186DEA80094EFB0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 60 | 52010DDA2186DEA80094EFB0 /* SPCycleScrollViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPCycleScrollViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 52010DDE2186DEA80094EFB0 /* SPCycleScrollViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPCycleScrollViewTests.m; sourceTree = ""; }; 62 | 52010DE02186DEA80094EFB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | 52010DE52186DEA80094EFB0 /* SPCycleScrollViewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPCycleScrollViewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 52010DE92186DEA80094EFB0 /* SPCycleScrollViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPCycleScrollViewUITests.m; sourceTree = ""; }; 65 | 52010DEB2186DEA80094EFB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 52010DF72186DEE00094EFB0 /* SPCycleScrollView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPCycleScrollView.h; sourceTree = ""; }; 67 | 52010DF82186DEE00094EFB0 /* SPCycleScrollView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPCycleScrollView.m; sourceTree = ""; }; 68 | 52010E362186E1320094EFB0 /* SPageControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPageControl.m; sourceTree = ""; }; 69 | 52010E372186E1320094EFB0 /* SPPageControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPPageControl.h; sourceTree = ""; }; 70 | 52010E392186E22A0094EFB0 /* SecondViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecondViewController.m; sourceTree = ""; }; 71 | 52010E3A2186E22B0094EFB0 /* SecondViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SecondViewController.h; sourceTree = ""; }; 72 | 52010E3D2186E2500094EFB0 /* 笑脸red.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "笑脸red.png"; sourceTree = ""; }; 73 | 52010E3E2186E2500094EFB0 /* 景2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "景2.jpg"; sourceTree = ""; }; 74 | 52010E3F2186E2510094EFB0 /* 景1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "景1.jpg"; sourceTree = ""; }; 75 | 52010E402186E2510094EFB0 /* 笑脸yellow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "笑脸yellow.png"; sourceTree = ""; }; 76 | 52010E412186E2510094EFB0 /* 景3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "景3.jpg"; sourceTree = ""; }; 77 | 52010E422186E2510094EFB0 /* 景4.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "景4.jpg"; sourceTree = ""; }; 78 | 52010E432186E2510094EFB0 /* 景5.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "景5.jpg"; sourceTree = ""; }; 79 | 5202FA522193E0B600FD9E03 /* dog.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = dog.gif; sourceTree = ""; }; 80 | 5202FA542194020300FD9E03 /* placeholder.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = placeholder.png; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 52010DBF2186DEA50094EFB0 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 52010DD72186DEA80094EFB0 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | 52010DE22186DEA80094EFB0 /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | ); 103 | runOnlyForDeploymentPostprocessing = 0; 104 | }; 105 | /* End PBXFrameworksBuildPhase section */ 106 | 107 | /* Begin PBXGroup section */ 108 | 52010DB92186DEA50094EFB0 = { 109 | isa = PBXGroup; 110 | children = ( 111 | 52010DC42186DEA50094EFB0 /* SPCycleScrollView */, 112 | 52010DDD2186DEA80094EFB0 /* SPCycleScrollViewTests */, 113 | 52010DE82186DEA80094EFB0 /* SPCycleScrollViewUITests */, 114 | 52010DC32186DEA50094EFB0 /* Products */, 115 | ); 116 | sourceTree = ""; 117 | }; 118 | 52010DC32186DEA50094EFB0 /* Products */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 52010DC22186DEA50094EFB0 /* SPCycleScrollView.app */, 122 | 52010DDA2186DEA80094EFB0 /* SPCycleScrollViewTests.xctest */, 123 | 52010DE52186DEA80094EFB0 /* SPCycleScrollViewUITests.xctest */, 124 | ); 125 | name = Products; 126 | sourceTree = ""; 127 | }; 128 | 52010DC42186DEA50094EFB0 /* SPCycleScrollView */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 52010DFA2186DEF80094EFB0 /* SPCycleScrollView */, 132 | 52010DC52186DEA50094EFB0 /* AppDelegate.h */, 133 | 52010DC62186DEA50094EFB0 /* AppDelegate.m */, 134 | 52010DC82186DEA50094EFB0 /* ViewController.h */, 135 | 52010DC92186DEA50094EFB0 /* ViewController.m */, 136 | 52010E3A2186E22B0094EFB0 /* SecondViewController.h */, 137 | 52010E392186E22A0094EFB0 /* SecondViewController.m */, 138 | 52010DCB2186DEA50094EFB0 /* Main.storyboard */, 139 | 52010DCE2186DEA80094EFB0 /* Assets.xcassets */, 140 | 52010DD02186DEA80094EFB0 /* LaunchScreen.storyboard */, 141 | 52010DD32186DEA80094EFB0 /* Info.plist */, 142 | 52010DD42186DEA80094EFB0 /* main.m */, 143 | 52010E3C2186E23E0094EFB0 /* Resource */, 144 | ); 145 | path = SPCycleScrollView; 146 | sourceTree = ""; 147 | }; 148 | 52010DDD2186DEA80094EFB0 /* SPCycleScrollViewTests */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 52010DDE2186DEA80094EFB0 /* SPCycleScrollViewTests.m */, 152 | 52010DE02186DEA80094EFB0 /* Info.plist */, 153 | ); 154 | path = SPCycleScrollViewTests; 155 | sourceTree = ""; 156 | }; 157 | 52010DE82186DEA80094EFB0 /* SPCycleScrollViewUITests */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 52010DE92186DEA80094EFB0 /* SPCycleScrollViewUITests.m */, 161 | 52010DEB2186DEA80094EFB0 /* Info.plist */, 162 | ); 163 | path = SPCycleScrollViewUITests; 164 | sourceTree = ""; 165 | }; 166 | 52010DFA2186DEF80094EFB0 /* SPCycleScrollView */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 52010DF72186DEE00094EFB0 /* SPCycleScrollView.h */, 170 | 52010DF82186DEE00094EFB0 /* SPCycleScrollView.m */, 171 | 52010E372186E1320094EFB0 /* SPPageControl.h */, 172 | 52010E362186E1320094EFB0 /* SPageControl.m */, 173 | ); 174 | path = SPCycleScrollView; 175 | sourceTree = ""; 176 | }; 177 | 52010E3C2186E23E0094EFB0 /* Resource */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 52010E3F2186E2510094EFB0 /* 景1.jpg */, 181 | 52010E3E2186E2500094EFB0 /* 景2.jpg */, 182 | 52010E412186E2510094EFB0 /* 景3.jpg */, 183 | 52010E422186E2510094EFB0 /* 景4.jpg */, 184 | 52010E432186E2510094EFB0 /* 景5.jpg */, 185 | 52010E3D2186E2500094EFB0 /* 笑脸red.png */, 186 | 52010E402186E2510094EFB0 /* 笑脸yellow.png */, 187 | 5202FA522193E0B600FD9E03 /* dog.gif */, 188 | 5202FA542194020300FD9E03 /* placeholder.png */, 189 | ); 190 | path = Resource; 191 | sourceTree = ""; 192 | }; 193 | /* End PBXGroup section */ 194 | 195 | /* Begin PBXNativeTarget section */ 196 | 52010DC12186DEA50094EFB0 /* SPCycleScrollView */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = 52010DEE2186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollView" */; 199 | buildPhases = ( 200 | 52010DBE2186DEA50094EFB0 /* Sources */, 201 | 52010DBF2186DEA50094EFB0 /* Frameworks */, 202 | 52010DC02186DEA50094EFB0 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | ); 208 | name = SPCycleScrollView; 209 | productName = SPCycleScrollView; 210 | productReference = 52010DC22186DEA50094EFB0 /* SPCycleScrollView.app */; 211 | productType = "com.apple.product-type.application"; 212 | }; 213 | 52010DD92186DEA80094EFB0 /* SPCycleScrollViewTests */ = { 214 | isa = PBXNativeTarget; 215 | buildConfigurationList = 52010DF12186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollViewTests" */; 216 | buildPhases = ( 217 | 52010DD62186DEA80094EFB0 /* Sources */, 218 | 52010DD72186DEA80094EFB0 /* Frameworks */, 219 | 52010DD82186DEA80094EFB0 /* Resources */, 220 | ); 221 | buildRules = ( 222 | ); 223 | dependencies = ( 224 | 52010DDC2186DEA80094EFB0 /* PBXTargetDependency */, 225 | ); 226 | name = SPCycleScrollViewTests; 227 | productName = SPCycleScrollViewTests; 228 | productReference = 52010DDA2186DEA80094EFB0 /* SPCycleScrollViewTests.xctest */; 229 | productType = "com.apple.product-type.bundle.unit-test"; 230 | }; 231 | 52010DE42186DEA80094EFB0 /* SPCycleScrollViewUITests */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 52010DF42186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollViewUITests" */; 234 | buildPhases = ( 235 | 52010DE12186DEA80094EFB0 /* Sources */, 236 | 52010DE22186DEA80094EFB0 /* Frameworks */, 237 | 52010DE32186DEA80094EFB0 /* Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | 52010DE72186DEA80094EFB0 /* PBXTargetDependency */, 243 | ); 244 | name = SPCycleScrollViewUITests; 245 | productName = SPCycleScrollViewUITests; 246 | productReference = 52010DE52186DEA80094EFB0 /* SPCycleScrollViewUITests.xctest */; 247 | productType = "com.apple.product-type.bundle.ui-testing"; 248 | }; 249 | /* End PBXNativeTarget section */ 250 | 251 | /* Begin PBXProject section */ 252 | 52010DBA2186DEA50094EFB0 /* Project object */ = { 253 | isa = PBXProject; 254 | attributes = { 255 | LastUpgradeCheck = 1000; 256 | ORGANIZATIONNAME = "乐升平"; 257 | TargetAttributes = { 258 | 52010DC12186DEA50094EFB0 = { 259 | CreatedOnToolsVersion = 10.0; 260 | }; 261 | 52010DD92186DEA80094EFB0 = { 262 | CreatedOnToolsVersion = 10.0; 263 | TestTargetID = 52010DC12186DEA50094EFB0; 264 | }; 265 | 52010DE42186DEA80094EFB0 = { 266 | CreatedOnToolsVersion = 10.0; 267 | TestTargetID = 52010DC12186DEA50094EFB0; 268 | }; 269 | }; 270 | }; 271 | buildConfigurationList = 52010DBD2186DEA50094EFB0 /* Build configuration list for PBXProject "SPCycleScrollView" */; 272 | compatibilityVersion = "Xcode 9.3"; 273 | developmentRegion = en; 274 | hasScannedForEncodings = 0; 275 | knownRegions = ( 276 | en, 277 | Base, 278 | ); 279 | mainGroup = 52010DB92186DEA50094EFB0; 280 | productRefGroup = 52010DC32186DEA50094EFB0 /* Products */; 281 | projectDirPath = ""; 282 | projectRoot = ""; 283 | targets = ( 284 | 52010DC12186DEA50094EFB0 /* SPCycleScrollView */, 285 | 52010DD92186DEA80094EFB0 /* SPCycleScrollViewTests */, 286 | 52010DE42186DEA80094EFB0 /* SPCycleScrollViewUITests */, 287 | ); 288 | }; 289 | /* End PBXProject section */ 290 | 291 | /* Begin PBXResourcesBuildPhase section */ 292 | 52010DC02186DEA50094EFB0 /* Resources */ = { 293 | isa = PBXResourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 52010E492186E2520094EFB0 /* 景4.jpg in Resources */, 297 | 5202FA552194020400FD9E03 /* placeholder.png in Resources */, 298 | 52010E472186E2520094EFB0 /* 笑脸yellow.png in Resources */, 299 | 52010E452186E2520094EFB0 /* 景2.jpg in Resources */, 300 | 52010E442186E2520094EFB0 /* 笑脸red.png in Resources */, 301 | 52010DD22186DEA80094EFB0 /* LaunchScreen.storyboard in Resources */, 302 | 52010E4A2186E2520094EFB0 /* 景5.jpg in Resources */, 303 | 52010E462186E2520094EFB0 /* 景1.jpg in Resources */, 304 | 52010DCF2186DEA80094EFB0 /* Assets.xcassets in Resources */, 305 | 52010DCD2186DEA50094EFB0 /* Main.storyboard in Resources */, 306 | 5202FA532193E0B600FD9E03 /* dog.gif in Resources */, 307 | 52010E482186E2520094EFB0 /* 景3.jpg in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | 52010DD82186DEA80094EFB0 /* Resources */ = { 312 | isa = PBXResourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | 52010DE32186DEA80094EFB0 /* Resources */ = { 319 | isa = PBXResourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | /* End PBXResourcesBuildPhase section */ 326 | 327 | /* Begin PBXSourcesBuildPhase section */ 328 | 52010DBE2186DEA50094EFB0 /* Sources */ = { 329 | isa = PBXSourcesBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | 52010E3B2186E22B0094EFB0 /* SecondViewController.m in Sources */, 333 | 52010DCA2186DEA50094EFB0 /* ViewController.m in Sources */, 334 | 52010E382186E1330094EFB0 /* SPageControl.m in Sources */, 335 | 52010DD52186DEA80094EFB0 /* main.m in Sources */, 336 | 52010DC72186DEA50094EFB0 /* AppDelegate.m in Sources */, 337 | 52010DF92186DEE00094EFB0 /* SPCycleScrollView.m in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 52010DD62186DEA80094EFB0 /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 52010DDF2186DEA80094EFB0 /* SPCycleScrollViewTests.m in Sources */, 346 | ); 347 | runOnlyForDeploymentPostprocessing = 0; 348 | }; 349 | 52010DE12186DEA80094EFB0 /* Sources */ = { 350 | isa = PBXSourcesBuildPhase; 351 | buildActionMask = 2147483647; 352 | files = ( 353 | 52010DEA2186DEA80094EFB0 /* SPCycleScrollViewUITests.m in Sources */, 354 | ); 355 | runOnlyForDeploymentPostprocessing = 0; 356 | }; 357 | /* End PBXSourcesBuildPhase section */ 358 | 359 | /* Begin PBXTargetDependency section */ 360 | 52010DDC2186DEA80094EFB0 /* PBXTargetDependency */ = { 361 | isa = PBXTargetDependency; 362 | target = 52010DC12186DEA50094EFB0 /* SPCycleScrollView */; 363 | targetProxy = 52010DDB2186DEA80094EFB0 /* PBXContainerItemProxy */; 364 | }; 365 | 52010DE72186DEA80094EFB0 /* PBXTargetDependency */ = { 366 | isa = PBXTargetDependency; 367 | target = 52010DC12186DEA50094EFB0 /* SPCycleScrollView */; 368 | targetProxy = 52010DE62186DEA80094EFB0 /* PBXContainerItemProxy */; 369 | }; 370 | /* End PBXTargetDependency section */ 371 | 372 | /* Begin PBXVariantGroup section */ 373 | 52010DCB2186DEA50094EFB0 /* Main.storyboard */ = { 374 | isa = PBXVariantGroup; 375 | children = ( 376 | 52010DCC2186DEA50094EFB0 /* Base */, 377 | ); 378 | name = Main.storyboard; 379 | sourceTree = ""; 380 | }; 381 | 52010DD02186DEA80094EFB0 /* LaunchScreen.storyboard */ = { 382 | isa = PBXVariantGroup; 383 | children = ( 384 | 52010DD12186DEA80094EFB0 /* Base */, 385 | ); 386 | name = LaunchScreen.storyboard; 387 | sourceTree = ""; 388 | }; 389 | /* End PBXVariantGroup section */ 390 | 391 | /* Begin XCBuildConfiguration section */ 392 | 52010DEC2186DEA80094EFB0 /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ALWAYS_SEARCH_USER_PATHS = NO; 396 | CLANG_ANALYZER_NONNULL = YES; 397 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 399 | CLANG_CXX_LIBRARY = "libc++"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_ENABLE_OBJC_WEAK = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 409 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 410 | CLANG_WARN_EMPTY_BODY = YES; 411 | CLANG_WARN_ENUM_CONVERSION = YES; 412 | CLANG_WARN_INFINITE_RECURSION = YES; 413 | CLANG_WARN_INT_CONVERSION = YES; 414 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 416 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | CODE_SIGN_IDENTITY = "iPhone Developer"; 425 | COPY_PHASE_STRIP = NO; 426 | DEBUG_INFORMATION_FORMAT = dwarf; 427 | ENABLE_STRICT_OBJC_MSGSEND = YES; 428 | ENABLE_TESTABILITY = YES; 429 | GCC_C_LANGUAGE_STANDARD = gnu11; 430 | GCC_DYNAMIC_NO_PIC = NO; 431 | GCC_NO_COMMON_BLOCKS = YES; 432 | GCC_OPTIMIZATION_LEVEL = 0; 433 | GCC_PREPROCESSOR_DEFINITIONS = ( 434 | "DEBUG=1", 435 | "$(inherited)", 436 | ); 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 439 | GCC_WARN_UNDECLARED_SELECTOR = YES; 440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 441 | GCC_WARN_UNUSED_FUNCTION = YES; 442 | GCC_WARN_UNUSED_VARIABLE = YES; 443 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 444 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 445 | MTL_FAST_MATH = YES; 446 | ONLY_ACTIVE_ARCH = YES; 447 | SDKROOT = iphoneos; 448 | }; 449 | name = Debug; 450 | }; 451 | 52010DED2186DEA80094EFB0 /* Release */ = { 452 | isa = XCBuildConfiguration; 453 | buildSettings = { 454 | ALWAYS_SEARCH_USER_PATHS = NO; 455 | CLANG_ANALYZER_NONNULL = YES; 456 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 457 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 458 | CLANG_CXX_LIBRARY = "libc++"; 459 | CLANG_ENABLE_MODULES = YES; 460 | CLANG_ENABLE_OBJC_ARC = YES; 461 | CLANG_ENABLE_OBJC_WEAK = YES; 462 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 463 | CLANG_WARN_BOOL_CONVERSION = YES; 464 | CLANG_WARN_COMMA = YES; 465 | CLANG_WARN_CONSTANT_CONVERSION = YES; 466 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 467 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 468 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 469 | CLANG_WARN_EMPTY_BODY = YES; 470 | CLANG_WARN_ENUM_CONVERSION = YES; 471 | CLANG_WARN_INFINITE_RECURSION = YES; 472 | CLANG_WARN_INT_CONVERSION = YES; 473 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 474 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 475 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 476 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 477 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 478 | CLANG_WARN_STRICT_PROTOTYPES = YES; 479 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 480 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 481 | CLANG_WARN_UNREACHABLE_CODE = YES; 482 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 483 | CODE_SIGN_IDENTITY = "iPhone Developer"; 484 | COPY_PHASE_STRIP = NO; 485 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 486 | ENABLE_NS_ASSERTIONS = NO; 487 | ENABLE_STRICT_OBJC_MSGSEND = YES; 488 | GCC_C_LANGUAGE_STANDARD = gnu11; 489 | GCC_NO_COMMON_BLOCKS = YES; 490 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 491 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 492 | GCC_WARN_UNDECLARED_SELECTOR = YES; 493 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 494 | GCC_WARN_UNUSED_FUNCTION = YES; 495 | GCC_WARN_UNUSED_VARIABLE = YES; 496 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 497 | MTL_ENABLE_DEBUG_INFO = NO; 498 | MTL_FAST_MATH = YES; 499 | SDKROOT = iphoneos; 500 | VALIDATE_PRODUCT = YES; 501 | }; 502 | name = Release; 503 | }; 504 | 52010DEF2186DEA80094EFB0 /* Debug */ = { 505 | isa = XCBuildConfiguration; 506 | buildSettings = { 507 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 508 | CODE_SIGN_STYLE = Automatic; 509 | DEVELOPMENT_TEAM = MP8K63J94Y; 510 | INFOPLIST_FILE = SPCycleScrollView/Info.plist; 511 | LD_RUNPATH_SEARCH_PATHS = ( 512 | "$(inherited)", 513 | "@executable_path/Frameworks", 514 | ); 515 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollView; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | TARGETED_DEVICE_FAMILY = "1,2"; 518 | }; 519 | name = Debug; 520 | }; 521 | 52010DF02186DEA80094EFB0 /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 525 | CODE_SIGN_STYLE = Automatic; 526 | DEVELOPMENT_TEAM = MP8K63J94Y; 527 | INFOPLIST_FILE = SPCycleScrollView/Info.plist; 528 | LD_RUNPATH_SEARCH_PATHS = ( 529 | "$(inherited)", 530 | "@executable_path/Frameworks", 531 | ); 532 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollView; 533 | PRODUCT_NAME = "$(TARGET_NAME)"; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | }; 536 | name = Release; 537 | }; 538 | 52010DF22186DEA80094EFB0 /* Debug */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | BUNDLE_LOADER = "$(TEST_HOST)"; 542 | CODE_SIGN_STYLE = Automatic; 543 | INFOPLIST_FILE = SPCycleScrollViewTests/Info.plist; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/Frameworks", 547 | "@loader_path/Frameworks", 548 | ); 549 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollViewTests; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | TARGETED_DEVICE_FAMILY = "1,2"; 552 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SPCycleScrollView.app/SPCycleScrollView"; 553 | }; 554 | name = Debug; 555 | }; 556 | 52010DF32186DEA80094EFB0 /* Release */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | BUNDLE_LOADER = "$(TEST_HOST)"; 560 | CODE_SIGN_STYLE = Automatic; 561 | INFOPLIST_FILE = SPCycleScrollViewTests/Info.plist; 562 | LD_RUNPATH_SEARCH_PATHS = ( 563 | "$(inherited)", 564 | "@executable_path/Frameworks", 565 | "@loader_path/Frameworks", 566 | ); 567 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollViewTests; 568 | PRODUCT_NAME = "$(TARGET_NAME)"; 569 | TARGETED_DEVICE_FAMILY = "1,2"; 570 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SPCycleScrollView.app/SPCycleScrollView"; 571 | }; 572 | name = Release; 573 | }; 574 | 52010DF52186DEA80094EFB0 /* Debug */ = { 575 | isa = XCBuildConfiguration; 576 | buildSettings = { 577 | CODE_SIGN_STYLE = Automatic; 578 | INFOPLIST_FILE = SPCycleScrollViewUITests/Info.plist; 579 | LD_RUNPATH_SEARCH_PATHS = ( 580 | "$(inherited)", 581 | "@executable_path/Frameworks", 582 | "@loader_path/Frameworks", 583 | ); 584 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollViewUITests; 585 | PRODUCT_NAME = "$(TARGET_NAME)"; 586 | TARGETED_DEVICE_FAMILY = "1,2"; 587 | TEST_TARGET_NAME = SPCycleScrollView; 588 | }; 589 | name = Debug; 590 | }; 591 | 52010DF62186DEA80094EFB0 /* Release */ = { 592 | isa = XCBuildConfiguration; 593 | buildSettings = { 594 | CODE_SIGN_STYLE = Automatic; 595 | INFOPLIST_FILE = SPCycleScrollViewUITests/Info.plist; 596 | LD_RUNPATH_SEARCH_PATHS = ( 597 | "$(inherited)", 598 | "@executable_path/Frameworks", 599 | "@loader_path/Frameworks", 600 | ); 601 | PRODUCT_BUNDLE_IDENTIFIER = com.idress.SPCycleScrollViewUITests; 602 | PRODUCT_NAME = "$(TARGET_NAME)"; 603 | TARGETED_DEVICE_FAMILY = "1,2"; 604 | TEST_TARGET_NAME = SPCycleScrollView; 605 | }; 606 | name = Release; 607 | }; 608 | /* End XCBuildConfiguration section */ 609 | 610 | /* Begin XCConfigurationList section */ 611 | 52010DBD2186DEA50094EFB0 /* Build configuration list for PBXProject "SPCycleScrollView" */ = { 612 | isa = XCConfigurationList; 613 | buildConfigurations = ( 614 | 52010DEC2186DEA80094EFB0 /* Debug */, 615 | 52010DED2186DEA80094EFB0 /* Release */, 616 | ); 617 | defaultConfigurationIsVisible = 0; 618 | defaultConfigurationName = Release; 619 | }; 620 | 52010DEE2186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollView" */ = { 621 | isa = XCConfigurationList; 622 | buildConfigurations = ( 623 | 52010DEF2186DEA80094EFB0 /* Debug */, 624 | 52010DF02186DEA80094EFB0 /* Release */, 625 | ); 626 | defaultConfigurationIsVisible = 0; 627 | defaultConfigurationName = Release; 628 | }; 629 | 52010DF12186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollViewTests" */ = { 630 | isa = XCConfigurationList; 631 | buildConfigurations = ( 632 | 52010DF22186DEA80094EFB0 /* Debug */, 633 | 52010DF32186DEA80094EFB0 /* Release */, 634 | ); 635 | defaultConfigurationIsVisible = 0; 636 | defaultConfigurationName = Release; 637 | }; 638 | 52010DF42186DEA80094EFB0 /* Build configuration list for PBXNativeTarget "SPCycleScrollViewUITests" */ = { 639 | isa = XCConfigurationList; 640 | buildConfigurations = ( 641 | 52010DF52186DEA80094EFB0 /* Debug */, 642 | 52010DF62186DEA80094EFB0 /* Release */, 643 | ); 644 | defaultConfigurationIsVisible = 0; 645 | defaultConfigurationName = Release; 646 | }; 647 | /* End XCConfigurationList section */ 648 | }; 649 | rootObject = 52010DBA2186DEA50094EFB0 /* Project object */; 650 | } 651 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/project.xcworkspace/xcuserdata/leshengping.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView.xcodeproj/project.xcworkspace/xcuserdata/leshengping.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/xcuserdata/leshengping.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView.xcodeproj/xcuserdata/leshengping.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SPCycleScrollView.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/.DS_Store -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/currentDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "切换图标@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "切换图标@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/currentDot.imageset/切换图标@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Assets.xcassets/currentDot.imageset/切换图标@2x.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/currentDot.imageset/切换图标@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Assets.xcassets/currentDot.imageset/切换图标@3x.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/otherDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "切换小圆@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "切换小圆@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/otherDot.imageset/切换小圆@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Assets.xcassets/otherDot.imageset/切换小圆@2x.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Assets.xcassets/otherDot.imageset/切换小圆@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Assets.xcassets/otherDot.imageset/切换小圆@3x.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 2.0.1 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/dog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/dog.gif -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/placeholder.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/景1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/景1.jpg -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/景2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/景2.jpg -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/景3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/景3.jpg -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/景4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/景4.jpg -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/景5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/景5.jpg -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/笑脸red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/笑脸red.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/Resource/笑脸yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SPStore/SPCycleScrollView/f291e26a7f1715948fb0fcb0b54fa4b974e68b5b/Demo/SPCycleScrollView/Resource/笑脸yellow.png -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SPCycleScrollView/SPCycleScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollView.h 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | // 本框架的github地址:https://github.com/SPStore/SPCycleScrollView 10 | 11 | #import 12 | #import "SPPageControl.h" 13 | 14 | @class SPCycleScrollView; 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | 19 | @protocol SPCycleScrollViewDelegate 20 | @optional 21 | // 轮播图的图片被点击时触发的代理方法,index为点击的图片下标 22 | - (void)cycleScrollView:(SPCycleScrollView *)cycleScrollView clickedImageAtIndex:(NSUInteger)index; 23 | 24 | @end 25 | 26 | typedef void(^ClickedImageBlock)(NSUInteger index); 27 | 28 | typedef NS_ENUM(NSInteger, SPPageContolPosition) { 29 | SPPageContolPositionBottomCenter, // 底部中心 30 | SPPageContolPositionBottomRight, // 底部右边 31 | SPPageContolPositionBottomLeft // 底部左边 32 | }; 33 | 34 | @interface SPCycleScrollView : UIView 35 | 36 | // 提供类方法创建轮播图 这种创建方式有个局限性,那就是必须在创建时就传入数组。 37 | /** 本地图片 */ 38 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame localImages:(nonnull NSArray *)localImages placeholderImage:(nullable UIImage *)image; 39 | 40 | /** 网络图片 */ 41 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame urlImages:(nonnull NSArray *)urlImages placeholderImage:(nullable UIImage *)image; 42 | 43 | 44 | // 为了消除类方法创建的局限性,提供下面两个属性,轮播图的图片数组。适用于创建时用alloc init,然后在以后的某个时刻传入数组。 45 | @property(strong, nonatomic) NSArray *localImages; // 本地图片 46 | @property(strong, nonatomic) NSArray *urlImages; // 网络图片 47 | 48 | @property (nonatomic, strong) NSArray *titles; // 图片对应的标题数组,如果标题个数小于图片个数,内部会用空字符串补足 49 | @property (nonatomic, strong) UIColor *titleLabelBackgroundColor; // 图片上label的背景色,默认是[UIColor colorWithWhite:0 alpha:0.5] 50 | @property (nonatomic, strong) UIColor *titleLabelTextColor; // 图片上label的文字颜色,默认是白色 51 | @property (nonatomic, strong) UIFont *titleLabelFont; // 图片上label的字体 52 | 53 | @property(weak, nonatomic) id delegate; // 代理 54 | @property (nonatomic, copy) ClickedImageBlock clickedImageBlock; // 轮播图的图片被点击时回调的block,与代理功能一致,开发者可二选其一.如果两种方式不小心同时实现了,则默认block方式 55 | 56 | - (void)adjustWhenControllerViewWillAppear; // 解决viewWillAppear时出现时轮播图卡在一半的问题,在控制器viewWillAppear时调用此方法 57 | 58 | @property(assign ,nonatomic) NSTimeInterval duration; // 图片自动切换间隔时间, 默认设置为 2s 59 | 60 | @property (assign ,nonatomic, getter=isAutoScroll) BOOL autoScroll; // 是否自动轮播,默认为YES 61 | 62 | @property (nonatomic, assign) UIViewContentMode imageMode; // 设置图片的内容模式,默认为UIViewContentModeScaleToFill 63 | 64 | @property (nonatomic, strong) UIImage *placeholderImage; // 占位图,默认nil,必须在设置图片数组之前设置才有效 65 | 66 | @property (nonatomic, assign) BOOL autoCache;// 是否开启图片缓存,默认为YES 67 | 68 | @property (nonatomic, strong, readonly) SPPageControl *pageControl; // 自定义的pageControl,可拿到此对象自行定义你所需要的样式,例如小圆点的颜色,图片,隐藏等 69 | @property (assign, nonatomic) SPPageContolPosition pageControlPosition; // pageControl的位置,分左,中,右 70 | 71 | + (void)clearDiskCache; 72 | @end 73 | 74 | NS_ASSUME_NONNULL_END 75 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SPCycleScrollView/SPCycleScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollView.m 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import "SPCycleScrollView.h" 10 | 11 | #define kWidth ceilf(self.bounds.size.width) // 加ceil取上整,是为了保证scrollView.contentOffset.x能够更加准确的和kwidth作比较,例如在scrollViewDidScroll:方法中就有这样的比较,当外界进行了下拉放大时,kWidth可能是一个很长的浮点数,而scrollView.contentOffset.x直接依赖于kWidth会导致比较不准确,所以kWidth也取上整 12 | #define kHeight self.bounds.size.height 13 | 14 | #define kPageControlMargin 10.0f 15 | 16 | typedef NS_ENUM(NSInteger, SPCycleImageViewLabelPosition) { 17 | SPCycleImageViewLabelPositionCenter, // 底部中心 18 | SPCycleImageViewLabelPositionRight, // 底部右边 19 | SPCycleImageViewLabelPositionLeft // 底部左边 20 | }; 21 | 22 | @interface SPCycleImageView : UIImageView 23 | @property (nonatomic, strong) UIView *labelContentView; 24 | @property (nonatomic, strong) UILabel *titleLabel; 25 | @property (nonatomic, copy) NSString *title; 26 | 27 | - (void)setLabelPosition:(SPCycleImageViewLabelPosition)labelPosition remainderSpacing:(CGFloat)remainderSpacing; 28 | @property (nonatomic, assign) CGFloat remainderSpacing; // label的剩余空间,比pageControl的宽度略大 29 | @property (nonatomic, assign) SPCycleImageViewLabelPosition labelPosition; // label的位置 30 | @end 31 | 32 | @implementation SPCycleImageView 33 | 34 | - (instancetype)initWithFrame:(CGRect)frame { 35 | if (self = [super initWithFrame:frame]) { 36 | [self setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 37 | [self addSubview:self.labelContentView]; 38 | [self.labelContentView addSubview:self.titleLabel]; 39 | } 40 | return self; 41 | } 42 | 43 | - (void)setTitle:(NSString *)title { 44 | _title = title; 45 | self.titleLabel.text = title; 46 | } 47 | 48 | - (void)setLabelPosition:(SPCycleImageViewLabelPosition)labelPosition remainderSpacing:(CGFloat)remainderSpacing { 49 | _labelPosition = labelPosition; 50 | _remainderSpacing = remainderSpacing; 51 | switch (_labelPosition) { 52 | case SPCycleImageViewLabelPositionCenter: 53 | self.titleLabel.textAlignment = NSTextAlignmentCenter; 54 | break; 55 | case SPCycleImageViewLabelPositionRight: 56 | self.titleLabel.textAlignment = NSTextAlignmentRight; 57 | break; 58 | case SPCycleImageViewLabelPositionLeft: 59 | self.titleLabel.textAlignment = NSTextAlignmentLeft; 60 | break; 61 | default: 62 | break; 63 | } 64 | [self setNeedsLayout]; 65 | [self layoutIfNeeded]; 66 | } 67 | 68 | - (void)layoutSubviews { 69 | [super layoutSubviews]; 70 | 71 | CGFloat width = self.bounds.size.width; 72 | CGFloat height = self.bounds.size.height; 73 | CGFloat labelH = 30; 74 | 75 | self.labelContentView.frame = CGRectMake(0, height-labelH, width, labelH); 76 | 77 | switch (_labelPosition) { 78 | case SPCycleImageViewLabelPositionCenter: 79 | self.titleLabel.frame = CGRectMake(10, 0, width-20, labelH); 80 | break; 81 | case SPCycleImageViewLabelPositionRight: 82 | self.titleLabel.frame = CGRectMake(_remainderSpacing+20, 0, width-(_remainderSpacing+20+10), labelH); 83 | break; 84 | case SPCycleImageViewLabelPositionLeft: 85 | self.titleLabel.frame = CGRectMake(10, 0, width-(_remainderSpacing+20+10), labelH); 86 | break; 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | - (UIView *)labelContentView { 93 | if (!_labelContentView) { 94 | _labelContentView = [[UIView alloc] init]; 95 | _labelContentView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; 96 | _labelContentView.hidden = YES; 97 | } 98 | return _labelContentView; 99 | } 100 | 101 | - (UILabel *)titleLabel { 102 | if (!_titleLabel) { 103 | _titleLabel = [[UILabel alloc] init]; 104 | _titleLabel.textColor = [UIColor whiteColor]; 105 | } 106 | return _titleLabel; 107 | } 108 | 109 | @end 110 | 111 | typedef NS_ENUM(NSInteger, SPCycleScrollViewImagesDataType){ 112 | SPCycleScrollViewImagesDataTypeInLocal,// 本地图片标记 113 | SPCycleScrollViewImagesDataTypeInURL // URL图片标记 114 | }; 115 | 116 | @interface SPCycleScrollView () 117 | 118 | @property(strong, nonatomic) UIScrollView *scrollView; 119 | @property(strong, nonatomic) SPPageControl *pageControl; 120 | 121 | // 前一个视图,当前视图,下一个视图 122 | @property(strong, nonatomic) SPCycleImageView *lastImgView; 123 | @property(strong, nonatomic) SPCycleImageView *currentImgView; 124 | @property(strong, nonatomic) SPCycleImageView *nextImgView; 125 | 126 | // 图片来源(本地或URL) 127 | @property(nonatomic) SPCycleScrollViewImagesDataType carouseImagesType; 128 | 129 | @property(strong, nonatomic) NSTimer *timer; 130 | 131 | // kImageCount = array.count,图片数组个数 132 | @property(assign, nonatomic) NSInteger kImageCount; 133 | 134 | // 记录nextImageView的下标 默认从1开始 135 | @property(assign, nonatomic) NSInteger nextPhotoIndex; 136 | // 记录lastImageView的下标 默认从 _kImageCount - 1 开始 137 | @property(assign, nonatomic) NSInteger lastPhotoIndex; 138 | 139 | //pageControl图片大小 140 | @property (nonatomic, assign) CGSize pageImageSize; 141 | 142 | @property (nonatomic, strong) NSOperationQueue *queue; 143 | 144 | @end 145 | 146 | static NSString *cache; 147 | 148 | @implementation SPCycleScrollView 149 | 150 | #pragma mark - 初始化方法 151 | - (instancetype)initWithFrame:(CGRect)frame{ 152 | self = [super initWithFrame:frame]; 153 | if (self) { 154 | [self initialize]; 155 | } 156 | return self; 157 | } 158 | 159 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 160 | if (self = [super initWithCoder:aDecoder]) { 161 | [self initialize]; 162 | } 163 | return self; 164 | } 165 | 166 | - (void)initialize { 167 | _duration = 2.0; 168 | _autoScroll = YES; 169 | _autoCache = YES; 170 | _titleLabelBackgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; 171 | _titleLabelTextColor = [UIColor whiteColor]; 172 | _titleLabelFont = [UIFont systemFontOfSize:17]; 173 | 174 | cache = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"XLsn0wLoop"]; 175 | BOOL isDir = NO; 176 | BOOL isExists = [[NSFileManager defaultManager] fileExistsAtPath:cache isDirectory:&isDir]; 177 | if (!isExists || !isDir) { 178 | [[NSFileManager defaultManager] createDirectoryAtPath:cache withIntermediateDirectories:YES attributes:nil error:nil]; 179 | } 180 | } 181 | 182 | #pragma mark - Public Method 183 | // 如果是本地图片调用此方法 184 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame localImages:(NSArray *)localImages placeholderImage:(UIImage *)image { 185 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] initWithFrame:frame]; 186 | cycleScrollView.placeholderImage = image; 187 | // 调用set方法 188 | cycleScrollView.localImages = localImages; 189 | return cycleScrollView; 190 | } 191 | 192 | // 如果是网络图片调用此方法 193 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame urlImages:(NSArray *)urlImages placeholderImage:(UIImage *)image { 194 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] initWithFrame:frame]; 195 | cycleScrollView.placeholderImage = image; 196 | // 调用set方法 197 | cycleScrollView.urlImages = urlImages; 198 | return cycleScrollView; 199 | } 200 | 201 | - (void)adjustWhenControllerViewWillAppear { 202 | // 将轮播图的偏移量设回中间位置 203 | if (self.kImageCount > 1) { 204 | self.scrollView.contentOffset = CGPointMake(kWidth, 0); 205 | } 206 | } 207 | 208 | #pragma maek - Private Method 209 | // 开启定时器 210 | - (void)openTimer { 211 | // 开启之前一定要先将上一次开启的定时器关闭,否则会跟新的定时器重叠 212 | [self closeTimer]; 213 | if (_autoScroll && self.kImageCount > 1) { 214 | _timer = [NSTimer scheduledTimerWithTimeInterval:self.duration target:self selector:@selector(timerAction) userInfo:self repeats:YES]; 215 | // 当外界滑动其他scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer事件. 216 | // 因此,要保证timer事件不中断,就必须把_timer加入到NSRunLoopCommonModes模式下的 RunLoop中。也可以加入到UITrackingRunLoopMode 217 | [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; 218 | } 219 | } 220 | 221 | // 关闭定时器 222 | - (void)closeTimer { 223 | [_timer invalidate]; 224 | _timer = nil; 225 | } 226 | 227 | // timer事件 228 | - (void)timerAction{ 229 | // 定时器每次触发都让当前图片为轮播图的第三张ImageView的image 230 | [_scrollView setContentOffset:CGPointMake(kWidth*2, 0) animated:YES]; 231 | } 232 | 233 | - (void)configure{ 234 | [self addSubview:self.scrollView]; 235 | // 添加最初的三张imageView 236 | if (self.kImageCount > 1) { 237 | [self.scrollView addSubview:self.lastImgView]; 238 | [self.scrollView addSubview:self.currentImgView]; 239 | [self.scrollView addSubview:self.nextImgView]; 240 | 241 | // 将上一张图片设置为数组中最后一张图片 242 | [self setImageView:_lastImgView withSubscript:(_kImageCount-1)]; 243 | // 将当前图片设置为数组中第一张图片 244 | [self setImageView:_currentImgView withSubscript:0]; 245 | // 将下一张图片设置为数组中第二张图片,如果数组只有一张图片,则上、中、下图片全部是数组中的第一张图片 246 | [self setImageView:_nextImgView withSubscript:_kImageCount == 1 ? 0 : 1]; 247 | 248 | if (self.titles.count) { // 如果设置标题数组在设置图片数组之前,在这里需要再调一次titles的setter方法 249 | self.titles = self.titles; 250 | } 251 | self.titleLabelBackgroundColor = self.titleLabelBackgroundColor; 252 | self.titleLabelTextColor = self.titleLabelTextColor; 253 | self.titleLabelFont = self.titleLabelFont; 254 | 255 | } else { 256 | [self.scrollView addSubview:self.currentImgView]; 257 | [self setImageView:_currentImgView withSubscript:0]; 258 | } 259 | 260 | [self addSubview:self.pageControl]; 261 | _pageControl.numberOfPages = self.kImageCount; 262 | _pageControl.currentPage = 0; 263 | 264 | self.nextPhotoIndex = 1; 265 | self.lastPhotoIndex = _kImageCount - 1; 266 | 267 | [self layoutIfNeeded]; 268 | } 269 | 270 | // 根据下标设置imgView的image 271 | - (void)setImageView:(SPCycleImageView *)imgView withSubscript:(NSInteger)subcript{ 272 | if (_placeholderImage) { // 先给一张 273 | imgView.image = _placeholderImage; 274 | } 275 | if (self.carouseImagesType == SPCycleScrollViewImagesDataTypeInLocal) { 276 | NSString *localImgString = self.localImages[subcript]; 277 | if ([[localImgString.pathExtension lowercaseString] isEqualToString:@"gif"]) { 278 | imgView.image = gifImageNamed(localImgString); 279 | } else { 280 | imgView.image = [UIImage imageNamed:self.localImages[subcript]]; 281 | } 282 | } else{ 283 | // 网络图片设置 284 | [self sp_setImageWithImageView:imgView URL:self.urlImages[subcript]]; 285 | } 286 | if (self.titles.count) { 287 | imgView.title = self.titles[subcript]; 288 | } 289 | } 290 | 291 | #pragma mark - setter 292 | // 本地图片 293 | - (void)setLocalImages:(NSArray *)localImages { 294 | if (localImages.count == 0) return; 295 | if (![_localImages isEqualToArray:localImages]) { 296 | _localImages = nil; 297 | _localImages = [localImages copy]; 298 | // 标记图片来源 299 | self.carouseImagesType = SPCycleScrollViewImagesDataTypeInLocal; 300 | //获取数组个数 301 | self.kImageCount = _localImages.count; 302 | [self configure]; 303 | 304 | [self openTimer]; 305 | } 306 | } 307 | 308 | // 网络图片 309 | - (void)setUrlImages:(NSArray *)urlImages { 310 | if (urlImages.count == 0) return; 311 | if (![_urlImages isEqualToArray:urlImages]) { 312 | _urlImages = nil; 313 | _urlImages = [urlImages copy]; 314 | // 标记图片来源 315 | self.carouseImagesType = SPCycleScrollViewImagesDataTypeInURL; 316 | self.kImageCount = _urlImages.count; 317 | [self configure]; 318 | 319 | [self openTimer]; 320 | } 321 | } 322 | 323 | - (void)setTitles:(NSArray *)titles { 324 | if (titles.count == 0) { 325 | _lastImgView.labelContentView.hidden = YES; 326 | _currentImgView.labelContentView.hidden = YES; 327 | _nextImgView.labelContentView.hidden = YES; 328 | } else { 329 | 330 | _lastImgView.labelContentView.hidden = NO; 331 | _currentImgView.labelContentView.hidden = NO; 332 | _nextImgView.labelContentView.hidden = NO; 333 | 334 | NSArray *images = [NSArray array]; 335 | if (self.carouseImagesType == SPCycleScrollViewImagesDataTypeInURL) { 336 | images = self.urlImages; 337 | } else { 338 | images = self.localImages; 339 | } 340 | if (titles.count < images.count) { // 如果标题个数小于图片个数,则用空字符串补齐 341 | NSMutableArray *newTitles = [NSMutableArray arrayWithArray:titles]; 342 | for (NSInteger i = titles.count; i < images.count; i++) { 343 | [newTitles addObject:@""]; 344 | } 345 | _titles = newTitles; 346 | } else { // 标题个数大于或等于图片个数,如果是大于了,下标最大只会是图片最大个数-1,所以不用考虑截取到和图片个数一样大 347 | _titles = titles; 348 | } 349 | _lastImgView.title = _titles[(_titles.count-1)]; 350 | _currentImgView.title = _titles[0]; 351 | _nextImgView.title = _titles[_titles.count == 1 ? 0 : 1]; 352 | 353 | self.pageControlPosition = self.pageControlPosition; // 重新设置pageControl的位置 354 | } 355 | } 356 | 357 | - (void)setTitleLabelBackgroundColor:(UIColor *)titleLabelBackgroundColor { 358 | _titleLabelBackgroundColor = titleLabelBackgroundColor; 359 | if (_lastImgView && _currentImgView && _nextImgView) { 360 | _lastImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 361 | _currentImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 362 | _nextImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 363 | } 364 | } 365 | 366 | - (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor { 367 | _titleLabelTextColor = titleLabelTextColor; 368 | if (_lastImgView && _currentImgView && _nextImgView) { 369 | _lastImgView.titleLabel.textColor = _titleLabelTextColor; 370 | _currentImgView.titleLabel.textColor = _titleLabelTextColor; 371 | _nextImgView.titleLabel.textColor = _titleLabelTextColor; 372 | } 373 | } 374 | 375 | - (void)setTitleLabelFont:(UIFont *)titleLabelFont { 376 | _titleLabelFont = titleLabelFont; 377 | if (_lastImgView && _currentImgView && _nextImgView) { 378 | _lastImgView.titleLabel.font = _titleLabelFont; 379 | _currentImgView.titleLabel.font = _titleLabelFont; 380 | _nextImgView.titleLabel.font = _titleLabelFont; 381 | } 382 | } 383 | 384 | // 是否自动轮播 385 | - (void)setAutoScroll:(BOOL)autoScroll { 386 | _autoScroll = autoScroll; 387 | 388 | if (autoScroll) { 389 | // 开启新的定时器 390 | [self openTimer]; 391 | } else { 392 | // 关闭定时器 393 | [self closeTimer]; 394 | } 395 | } 396 | 397 | // 重写duration的set方法,用户可以在外界设置轮播图间隔时间 398 | - (void)setDuration:(NSTimeInterval)duration{ 399 | _duration = duration; 400 | if (duration < 1.0f) { // 如果外界不小心设置的时间小于1秒,强制默认2秒。 401 | duration = 2.0f; 402 | } 403 | [self openTimer]; 404 | } 405 | 406 | // 设置pageControl的位置 407 | - (void)setPageControlPosition:(SPPageContolPosition)pageControlPosition { 408 | _pageControlPosition = pageControlPosition; 409 | 410 | if (_pageControl.hidden) return; 411 | 412 | CGSize size; 413 | if (!_pageImageSize.width) {// 没有设置图片,系统原有样式 414 | size = [_pageControl sizeForNumberOfPages:_pageControl.numberOfPages]; 415 | } else { // 设置图片了 416 | size = CGSizeMake(_pageImageSize.width * (_pageControl.numberOfPages * 2 - 1), _pageImageSize.height); 417 | } 418 | 419 | _pageControl.frame = CGRectMake(0, 0, size.width, size.height); 420 | 421 | CGFloat pointY = kHeight - size.height - kPageControlMargin; 422 | 423 | switch (pageControlPosition) { 424 | case SPPageContolPositionBottomCenter: 425 | // 底部中间 426 | if (self.titles.count) { 427 | _pageControl.frame = CGRectMake((kWidth-size.width)*0.5, pointY-30, size.width, size.height); 428 | } else { 429 | _pageControl.frame = CGRectMake((kWidth-size.width)*0.5, pointY, size.width, size.height); 430 | } 431 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 432 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 433 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 434 | 435 | break; 436 | case SPPageContolPositionBottomRight: 437 | // 底部右边 438 | _pageControl.frame = CGRectMake(kWidth - size.width - kPageControlMargin, pointY, size.width, size.height); 439 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 440 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 441 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 442 | break; 443 | case SPPageContolPositionBottomLeft: 444 | // 底部左边 445 | _pageControl.frame = CGRectMake(kPageControlMargin, pointY, size.width, size.height); 446 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 447 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 448 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 449 | break; 450 | default: 451 | break; 452 | } 453 | } 454 | 455 | // 设置imageView的内容模式 456 | - (void)setImageMode:(UIViewContentMode)imageMode { 457 | _imageMode = imageMode; 458 | self.nextImgView.contentMode = self.currentImgView.contentMode = self.lastImgView.contentMode = imageMode; 459 | } 460 | 461 | #pragma mark - scrollView代理方法 462 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 463 | if (CGSizeEqualToSize(CGSizeZero, scrollView.contentSize)) return; 464 | 465 | CGFloat offsetX = scrollView.contentOffset.x; 466 | 467 | // 到第一张图片时 (一上来,当前图片的x值是kWidth) 468 | if (ceil(offsetX) <= 0) { // 右滑 469 | 470 | _nextImgView.image = _currentImgView.image; 471 | _currentImgView.image = _lastImgView.image; 472 | _nextImgView.title = _currentImgView.title; 473 | _currentImgView.title = _lastImgView.title; 474 | // 将轮播图的偏移量设回中间位置 475 | scrollView.contentOffset = CGPointMake(kWidth, 0); 476 | _lastImgView.image = nil; 477 | // 一定要小于等于,否则数组中只有一张图片时会出错 478 | if (_lastPhotoIndex <= 0) { 479 | _lastPhotoIndex = _kImageCount - 1; 480 | _nextPhotoIndex = _lastPhotoIndex - (_kImageCount - 2); 481 | } else { 482 | _lastPhotoIndex--; 483 | if (_nextPhotoIndex == 0) { 484 | _nextPhotoIndex = _kImageCount - 1; 485 | } else { 486 | _nextPhotoIndex--; 487 | } 488 | } 489 | [self setImageView:_lastImgView withSubscript:_lastPhotoIndex]; 490 | } 491 | // 到最后一张图片时(最后一张就是轮播图的第三张) 492 | if (ceil(offsetX) >= kWidth*2) { // 左滑 493 | _lastImgView.image = _currentImgView.image; 494 | _currentImgView.image = _nextImgView.image; 495 | _lastImgView.title = _currentImgView.title; 496 | _currentImgView.title = _nextImgView.title; 497 | scrollView.contentOffset = CGPointMake(kWidth, 0); 498 | _nextImgView.image = nil; 499 | // 一定要是大于等于,否则数组中只有一张图片时会出错 500 | if (_nextPhotoIndex >= _kImageCount - 1 ) { 501 | _nextPhotoIndex = 0; 502 | _lastPhotoIndex = _nextPhotoIndex + (_kImageCount - 2); 503 | } else{ 504 | _nextPhotoIndex++; 505 | if (_lastPhotoIndex == _kImageCount - 1) { 506 | _lastPhotoIndex = 0; 507 | } else { 508 | _lastPhotoIndex++; 509 | } 510 | } 511 | [self setImageView:_nextImgView withSubscript:_nextPhotoIndex]; 512 | } 513 | 514 | if (_nextPhotoIndex - 1 < 0) { 515 | self.pageControl.currentPage = _kImageCount - 1; 516 | } else { 517 | self.pageControl.currentPage = _nextPhotoIndex - 1; 518 | } 519 | 520 | } 521 | 522 | // 用户将要拖拽时将定时器关闭 523 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ 524 | // 关闭定时器 525 | [self closeTimer]; 526 | } 527 | 528 | // 用户结束拖拽时将定时器开启(在打开自动轮播的前提下) 529 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { 530 | if (self.autoScroll) { 531 | [self openTimer]; 532 | } 533 | } 534 | 535 | #pragma mark 下载网络图片 536 | - (void)sp_setImageWithImageView:(UIImageView *)imageView URL:(NSString *)urlString { 537 | NSString *subURLString = [urlString substringFromIndex:30]; 538 | NSString *imageName = [[subURLString stringByReplacingOccurrencesOfString:@"/" withString:@""] stringByReplacingOccurrencesOfString:@"_" withString:@""]; 539 | NSString *path = [cache stringByAppendingPathComponent:imageName]; 540 | if (_autoCache) { //如果开启了缓存功能,先从沙盒中取图片 541 | NSData *data = [NSData dataWithContentsOfFile:path]; 542 | if (data) { 543 | imageView.image = getImageWithData(data); 544 | return; 545 | } 546 | } 547 | //下载图片 548 | NSBlockOperation *download = [NSBlockOperation blockOperationWithBlock:^{ 549 | NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]]; 550 | if (!data) return; 551 | UIImage *image = getImageWithData(data); 552 | //取到的data有可能不是图片 553 | if (image) { 554 | dispatch_async(dispatch_get_main_queue(), ^{ 555 | imageView.image = image; 556 | }); 557 | if (self.autoCache) [data writeToFile:path atomically:YES]; 558 | } 559 | }]; 560 | [self.queue addOperation:download]; 561 | } 562 | 563 | #pragma mark 下载图片,如果是gif则计算动画时长 564 | UIImage *getImageWithData(NSData *data) { 565 | CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); 566 | size_t count = CGImageSourceGetCount(imageSource); 567 | if (count <= 1) { //非gif 568 | CFRelease(imageSource); 569 | return [[UIImage alloc] initWithData:data]; 570 | } else { //gif图片 571 | NSMutableArray *images = [NSMutableArray array]; 572 | NSTimeInterval duration = 0; 573 | for (size_t i = 0; i < count; i++) { 574 | CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); 575 | if (!image) continue; 576 | duration += durationWithSourceAtIndex(imageSource, i); 577 | [images addObject:[UIImage imageWithCGImage:image]]; 578 | CGImageRelease(image); 579 | } 580 | if (!duration) duration = 0.1 * count; 581 | CFRelease(imageSource); 582 | return [UIImage animatedImageWithImages:images duration:duration]; 583 | } 584 | } 585 | 586 | 587 | #pragma mark 获取每一帧图片的时长 588 | float durationWithSourceAtIndex(CGImageSourceRef source, NSUInteger index) { 589 | float duration = 0.1f; 590 | CFDictionaryRef propertiesRef = CGImageSourceCopyPropertiesAtIndex(source, index, nil); 591 | NSDictionary *properties = (__bridge NSDictionary *)propertiesRef; 592 | NSDictionary *gifProperties = properties[(NSString *)kCGImagePropertyGIFDictionary]; 593 | 594 | NSNumber *delayTime = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; 595 | if (delayTime) duration = delayTime.floatValue; 596 | else { 597 | delayTime = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; 598 | if (delayTime) duration = delayTime.floatValue; 599 | } 600 | CFRelease(propertiesRef); 601 | return duration; 602 | } 603 | 604 | UIImage *gifImageNamed(NSString *imageName) { 605 | 606 | if (![imageName hasSuffix:@".gif"]) { 607 | imageName = [imageName stringByAppendingString:@".gif"]; 608 | } 609 | 610 | NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:nil]; 611 | NSData *data = [NSData dataWithContentsOfFile:imagePath]; 612 | if (data) return getImageWithData(data); 613 | 614 | return [UIImage imageNamed:imageName]; 615 | } 616 | 617 | #pragma mark 清除沙盒中的图片缓存 618 | + (void)clearDiskCache { 619 | NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:cache error:NULL]; 620 | for (NSString *fileName in contents) { 621 | [[NSFileManager defaultManager] removeItemAtPath:[cache stringByAppendingPathComponent:fileName] error:nil]; 622 | } 623 | } 624 | 625 | #pragma mark - 手势点击事件 626 | -(void)handleTapActionInImageView:(UITapGestureRecognizer *)tap { 627 | if (self.clickedImageBlock) { 628 | // 如果_nextPhotoIndex == 0,那么中间那张图片一定是数组中最后一张,我们要传的就是中间那张图片在数组中的下标 629 | if (_nextPhotoIndex == 0) { 630 | self.clickedImageBlock(_kImageCount-1); 631 | }else{ 632 | self.clickedImageBlock(_nextPhotoIndex-1); 633 | } 634 | } else if (_delegate && [_delegate respondsToSelector:@selector(cycleScrollView:clickedImageAtIndex:)]) { 635 | // 如果_nextPhotoIndex == 0,那么中间那张图片一定是数组中最后一张,我们要传的就是中间那张图片在数组中的下标 636 | if (_nextPhotoIndex == 0) { 637 | [_delegate cycleScrollView:self clickedImageAtIndex:_kImageCount-1]; 638 | }else{ 639 | [_delegate cycleScrollView:self clickedImageAtIndex:_nextPhotoIndex-1]; 640 | } 641 | } 642 | } 643 | 644 | - (void)layoutSubviews { 645 | [super layoutSubviews]; 646 | 647 | self.scrollView.frame = self.bounds; 648 | //有导航控制器时,会默认在scrollview上方添加64的内边距,这里强制设置为0 649 | self.scrollView.contentInset = UIEdgeInsetsZero; 650 | 651 | if (self.kImageCount > 1) { 652 | // 重新设置contentOffset和contentSize对于轮播图下拉放大以及里面的图片跟随放大起着关键作用,因为scrollView放大了,如果不手动设置contentOffset和contentSize,则会导致scrollView的容量不够大,从而导致图片越出scrollview边界的问题 653 | self.scrollView.contentSize = CGSizeMake(kWidth * 3, 0); 654 | // 这里如果采用动画效果设置偏移量将不起任何作用 655 | self.scrollView.contentOffset = CGPointMake(kWidth, 0); 656 | 657 | self.lastImgView.frame = CGRectMake(0, 0, kWidth, kHeight); 658 | self.currentImgView.frame = CGRectMake(kWidth, 0, kWidth, kHeight); 659 | self.nextImgView.frame = CGRectMake(kWidth * 2, 0, kWidth, kHeight); 660 | 661 | } else { 662 | self.scrollView.contentSize = CGSizeZero; 663 | self.scrollView.contentOffset = CGPointMake(0, 0); 664 | self.currentImgView.frame = CGRectMake(0, 0, kWidth, kHeight); 665 | } 666 | 667 | // 等号左边是调setter方法,右边调用getter方法 668 | self.pageControlPosition = self.pageControlPosition; 669 | } 670 | 671 | #pragma mark - 懒加载 672 | -(UIScrollView *)scrollView{ 673 | if (_scrollView == nil) { 674 | _scrollView = [[UIScrollView alloc] init]; 675 | _scrollView.pagingEnabled = YES; 676 | _scrollView.bounces = NO; 677 | _scrollView.showsHorizontalScrollIndicator = NO; 678 | _scrollView.showsVerticalScrollIndicator = NO; 679 | _scrollView.delegate = self; 680 | _scrollView.clipsToBounds = YES; 681 | _scrollView.layer.masksToBounds = YES; 682 | } 683 | return _scrollView; 684 | } 685 | 686 | - (SPPageControl *)pageControl{ 687 | if (_pageControl == nil) { 688 | _pageControl = [[SPPageControl alloc] init]; 689 | _pageControl.userInteractionEnabled = NO; 690 | _pageControl.hidesForSinglePage = YES; 691 | _pageControl.pageIndicatorTintColor = [UIColor grayColor]; 692 | _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; 693 | _pageControl.currentPage = 0; 694 | } 695 | return _pageControl; 696 | } 697 | 698 | - (SPCycleImageView *)lastImgView{ 699 | if (_lastImgView == nil) { 700 | _lastImgView = [[SPCycleImageView alloc] init]; 701 | _lastImgView.layer.masksToBounds = YES; 702 | } 703 | return _lastImgView; 704 | } 705 | 706 | - (SPCycleImageView *)currentImgView{ 707 | if (_currentImgView == nil) { 708 | _currentImgView = [[SPCycleImageView alloc] init]; 709 | _currentImgView.layer.masksToBounds = YES; 710 | // 给当前图片添加手势 711 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleTapActionInImageView:)]; 712 | [_currentImgView addGestureRecognizer:tap]; 713 | _currentImgView.userInteractionEnabled = YES; 714 | } 715 | return _currentImgView; 716 | } 717 | 718 | - (SPCycleImageView *)nextImgView{ 719 | if (_nextImgView == nil) { 720 | _nextImgView = [[SPCycleImageView alloc] init]; 721 | _nextImgView.layer.masksToBounds = YES; 722 | } 723 | return _nextImgView; 724 | } 725 | 726 | - (NSOperationQueue *)queue { 727 | if (!_queue) { 728 | _queue = [[NSOperationQueue alloc] init]; 729 | } 730 | return _queue; 731 | } 732 | 733 | #pragma mark - 系统方法 734 | -(void)willMoveToSuperview:(UIView *)newSuperview { 735 | if (!newSuperview) { 736 | [self closeTimer]; 737 | } 738 | } 739 | 740 | -(void)dealloc { 741 | _scrollView.delegate = nil; 742 | } 743 | 744 | @end 745 | 746 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SPCycleScrollView/SPPageControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPPageControl.h 3 | // SPPageControl 4 | // 5 | // Created by Jerry Jones on 10/13/12. 6 | // Copyright (c) 2012 Spaceman Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, SPPageControlAlignment) { 12 | SPPageControlAlignmentLeft = 1, 13 | SPPageControlAlignmentCenter, 14 | SPPageControlAlignmentRight 15 | }; 16 | 17 | typedef NS_ENUM(NSUInteger, SPPageControlVerticalAlignment) { 18 | SPPageControlVerticalAlignmentTop = 1, 19 | SPPageControlVerticalAlignmentMiddle, 20 | SPPageControlVerticalAlignmentBottom 21 | }; 22 | 23 | typedef NS_ENUM(NSUInteger, SPPageControlTapBehavior) { 24 | SPPageControlTapBehaviorStep = 1, 25 | SPPageControlTapBehaviorJump 26 | }; 27 | 28 | @interface SPPageControl : UIControl 29 | 30 | @property (nonatomic) NSInteger numberOfPages; 31 | @property (nonatomic) NSInteger currentPage; 32 | @property (nonatomic) CGFloat indicatorMargin UI_APPEARANCE_SELECTOR; // deafult is 10 33 | @property (nonatomic) CGFloat indicatorDiameter UI_APPEARANCE_SELECTOR; // deafult is 6 34 | @property (nonatomic) CGFloat minHeight UI_APPEARANCE_SELECTOR; // default is 36, cannot be less than indicatorDiameter 35 | @property (nonatomic) SPPageControlAlignment alignment UI_APPEARANCE_SELECTOR; // deafult is Center 36 | @property (nonatomic) SPPageControlVerticalAlignment verticalAlignment UI_APPEARANCE_SELECTOR; // deafult is Middle 37 | 38 | @property (nonatomic, strong) UIImage *pageIndicatorImage UI_APPEARANCE_SELECTOR; 39 | @property (nonatomic, strong) UIImage *pageIndicatorMaskImage UI_APPEARANCE_SELECTOR; // ignored if pageIndicatorImage is set 40 | @property (nonatomic, strong) UIColor *pageIndicatorTintColor UI_APPEARANCE_SELECTOR; // ignored if pageIndicatorImage is set 41 | @property (nonatomic, strong) UIImage *currentPageIndicatorImage UI_APPEARANCE_SELECTOR; 42 | @property (nonatomic, strong) UIColor *currentPageIndicatorTintColor UI_APPEARANCE_SELECTOR; // ignored if currentPageIndicatorImage is set 43 | 44 | @property (nonatomic) BOOL hidesForSinglePage; // hide the the indicator if there is only one page. default is NO 45 | @property (nonatomic) BOOL defersCurrentPageDisplay; // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO 46 | 47 | @property (nonatomic) SPPageControlTapBehavior tapBehavior; // SPPageControlTapBehaviorStep provides an increment/decrement behavior exactly like UIPageControl. SPPageControlTapBehaviorJump allows specific pages to be selected by tapping their respective indicator. Default is SPPageControlTapBehaviorStep 48 | 49 | - (void)updateCurrentPageDisplay; // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately 50 | 51 | - (CGRect)rectForPageIndicator:(NSInteger)pageIndex; 52 | - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount; 53 | 54 | - (void)setImage:(UIImage *)image forPage:(NSInteger)pageIndex; 55 | - (void)setCurrentImage:(UIImage *)image forPage:(NSInteger)pageIndex; 56 | - (void)setImageMask:(UIImage *)image forPage:(NSInteger)pageIndex; 57 | 58 | - (UIImage *)imageForPage:(NSInteger)pageIndex; 59 | - (UIImage *)currentImageForPage:(NSInteger)pageIndex; 60 | - (UIImage *)imageMaskForPage:(NSInteger)pageIndex; 61 | 62 | - (void)updatePageNumberForScrollView:(UIScrollView *)scrollView; 63 | - (void)setScrollViewContentOffsetForCurrentPage:(UIScrollView *)scrollView animated:(BOOL)animated; 64 | 65 | #pragma mark - UIAccessibility 66 | 67 | // SPPageControl mirrors UIPageControl's standard accessibility functionality by default. 68 | // Basically, the accessibility label is set to "[current page index + 1] of [page count]". 69 | 70 | // SPPageControl extends UIPageControl's functionality by allowing you to name specific pages. This is especially useful when using 71 | // the per-page indicator images, and allows you to provide more context to the user. 72 | 73 | - (void)setName:(NSString *)name forPage:(NSInteger)pageIndex; 74 | - (NSString *)nameForPage:(NSInteger)pageIndex; 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SPCycleScrollView/SPageControl.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPPageControl.m 3 | // SPPageControl 4 | // 5 | // Created by Jerry Jones on 10/13/12. 6 | // Copyright (c) 2012 Spaceman Labs. All rights reserved. 7 | // 8 | 9 | #import "SPPageControl.h" 10 | 11 | #if ! __has_feature(objc_arc) 12 | #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). 13 | #endif 14 | 15 | 16 | #define DEFAULT_INDICATOR_WIDTH 6.0f 17 | #define DEFAULT_INDICATOR_MARGIN 10.0f 18 | #define DEFAULT_MIN_HEIGHT 36.0f 19 | 20 | #define DEFAULT_INDICATOR_WIDTH_LARGE 7.0f 21 | #define DEFAULT_INDICATOR_MARGIN_LARGE 9.0f 22 | #define DEFAULT_MIN_HEIGHT_LARGE 36.0f 23 | 24 | typedef NS_ENUM(NSUInteger, SPPageControlImageType) { 25 | SPPageControlImageTypeNormal = 1, 26 | SPPageControlImageTypeCurrent, 27 | SPPageControlImageTypeMask 28 | }; 29 | 30 | typedef NS_ENUM(NSUInteger, SPPageControlStyleDefaults) { 31 | SPPageControlDefaultStyleClassic = 0, 32 | SPPageControlDefaultStyleModern 33 | }; 34 | 35 | static SPPageControlStyleDefaults _defaultStyleForSystemVersion; 36 | 37 | @interface SPPageControl () 38 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageNames; 39 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageImages; 40 | @property (strong, readonly, nonatomic) NSMutableDictionary *currentPageImages; 41 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageImageMasks; 42 | @property (strong, readonly, nonatomic) NSMutableDictionary *cgImageMasks; 43 | @property (strong, readwrite, nonatomic) NSArray *pageRects; 44 | 45 | // Page Control used for stealing page number localizations for accessibility labels 46 | // I'm not sure I love this technique, but it's the best way to get exact translations for all the languages 47 | // that Apple supports out of the box 48 | @property (nonatomic, strong) UIPageControl *accessibilityPageControl; 49 | @end 50 | 51 | @implementation SPPageControl 52 | { 53 | @private 54 | NSInteger _displayedPage; 55 | CGFloat _measuredIndicatorWidth; 56 | CGFloat _measuredIndicatorHeight; 57 | CGImageRef _pageImageMask; 58 | } 59 | 60 | @synthesize pageNames = _pageNames; 61 | @synthesize pageImages = _pageImages; 62 | @synthesize currentPageImages = _currentPageImages; 63 | @synthesize pageImageMasks = _pageImageMasks; 64 | @synthesize cgImageMasks = _cgImageMasks; 65 | 66 | + (void)initialize 67 | { 68 | NSString *reqSysVer = @"7.0"; 69 | NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; 70 | if ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending) { 71 | _defaultStyleForSystemVersion = SPPageControlDefaultStyleModern; 72 | } else { 73 | _defaultStyleForSystemVersion = SPPageControlDefaultStyleClassic; 74 | } 75 | } 76 | 77 | - (void)_initialize 78 | { 79 | _numberOfPages = 0; 80 | _tapBehavior = SPPageControlTapBehaviorStep; 81 | 82 | self.backgroundColor = [UIColor clearColor]; 83 | 84 | // If the app wasn't linked against iOS 7 or newer, always use the classic style 85 | // otherwise, use the style of the current OS. 86 | #ifdef __IPHONE_7_0 87 | [self setStyleWithDefaults:_defaultStyleForSystemVersion]; 88 | #else 89 | [self setStyleWithDefaults:SPPageControlDefaultStyleClassic]; 90 | #endif 91 | 92 | _alignment = SPPageControlAlignmentCenter; 93 | _verticalAlignment = SPPageControlVerticalAlignmentMiddle; 94 | 95 | self.isAccessibilityElement = YES; 96 | self.accessibilityTraits = UIAccessibilityTraitUpdatesFrequently; 97 | self.accessibilityPageControl = [[UIPageControl alloc] init]; 98 | self.contentMode = UIViewContentModeRedraw; 99 | } 100 | 101 | - (id)initWithFrame:(CGRect)frame 102 | { 103 | self = [super initWithFrame:frame]; 104 | if (nil == self) { 105 | return nil; 106 | } 107 | 108 | [self _initialize]; 109 | return self; 110 | } 111 | 112 | - (id)initWithCoder:(NSCoder *)aDecoder 113 | { 114 | self = [super initWithCoder:aDecoder]; 115 | if (nil == self) { 116 | return nil; 117 | } 118 | 119 | [self _initialize]; 120 | return self; 121 | } 122 | 123 | - (void)dealloc 124 | { 125 | if (_pageImageMask) { 126 | CGImageRelease(_pageImageMask); 127 | } 128 | } 129 | 130 | - (void)drawRect:(CGRect)rect 131 | { 132 | CGContextRef context = UIGraphicsGetCurrentContext(); 133 | [self _renderPages:context rect:rect]; 134 | } 135 | 136 | - (void)_renderPages:(CGContextRef)context rect:(CGRect)rect 137 | { 138 | NSMutableArray *pageRects = [NSMutableArray arrayWithCapacity:self.numberOfPages]; 139 | 140 | if (_numberOfPages < 2 && _hidesForSinglePage) { 141 | return; 142 | } 143 | 144 | CGFloat left = [self _leftOffset]; 145 | 146 | CGFloat xOffset = left; 147 | CGFloat yOffset = 0.0f; 148 | UIColor *fillColor = nil; 149 | UIImage *image = nil; 150 | CGImageRef maskingImage = nil; 151 | CGSize maskSize = CGSizeZero; 152 | 153 | for (NSInteger i = 0; i < _numberOfPages; i++) { 154 | NSNumber *indexNumber = @(i); 155 | 156 | if (i == _displayedPage) { 157 | fillColor = _currentPageIndicatorTintColor ? _currentPageIndicatorTintColor : [UIColor whiteColor]; 158 | image = _currentPageImages[indexNumber]; 159 | if (nil == image) { 160 | image = _currentPageIndicatorImage; 161 | } 162 | } else { 163 | fillColor = _pageIndicatorTintColor ? _pageIndicatorTintColor : [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; 164 | image = _pageImages[indexNumber]; 165 | if (nil == image) { 166 | image = _pageIndicatorImage; 167 | } 168 | } 169 | 170 | // If no finished images have been set, try a masking image 171 | if (nil == image) { 172 | maskingImage = (__bridge CGImageRef)_cgImageMasks[indexNumber]; 173 | UIImage *originalImage = _pageImageMasks[indexNumber]; 174 | maskSize = originalImage.size; 175 | 176 | // If no per page mask is set, try for a global page mask! 177 | if (nil == maskingImage) { 178 | maskingImage = _pageImageMask; 179 | maskSize = _pageIndicatorMaskImage.size; 180 | } 181 | } 182 | 183 | [fillColor set]; 184 | CGRect indicatorRect; 185 | if (image) { 186 | yOffset = [self _topOffsetForHeight:image.size.height rect:rect]; 187 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - image.size.width) / 2.0f); 188 | [image drawAtPoint:CGPointMake(centeredXOffset, yOffset)]; 189 | indicatorRect = CGRectMake(centeredXOffset, yOffset, image.size.width, image.size.height); 190 | } else if (maskingImage) { 191 | yOffset = [self _topOffsetForHeight:maskSize.height rect:rect]; 192 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - maskSize.width) / 2.0f); 193 | indicatorRect = CGRectMake(centeredXOffset, yOffset, maskSize.width, maskSize.height); 194 | CGContextDrawImage(context, indicatorRect, maskingImage); 195 | } else { 196 | yOffset = [self _topOffsetForHeight:_indicatorDiameter rect:rect]; 197 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - _indicatorDiameter) / 2.0f); 198 | indicatorRect = CGRectMake(centeredXOffset, yOffset, _indicatorDiameter, _indicatorDiameter); 199 | CGContextFillEllipseInRect(context, indicatorRect); 200 | } 201 | 202 | [pageRects addObject:[NSValue valueWithCGRect:indicatorRect]]; 203 | maskingImage = NULL; 204 | xOffset += _measuredIndicatorWidth + _indicatorMargin; 205 | } 206 | 207 | self.pageRects = pageRects; 208 | 209 | } 210 | 211 | - (CGFloat)_leftOffset 212 | { 213 | CGRect rect = self.bounds; 214 | CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; 215 | CGFloat left = 0.0f; 216 | switch (_alignment) { 217 | case SPPageControlAlignmentCenter: 218 | left = ceilf(CGRectGetMidX(rect) - (size.width / 2.0f)); 219 | break; 220 | case SPPageControlAlignmentRight: 221 | left = CGRectGetMaxX(rect) - size.width; 222 | break; 223 | default: 224 | break; 225 | } 226 | 227 | return left; 228 | } 229 | 230 | - (CGFloat)_topOffsetForHeight:(CGFloat)height rect:(CGRect)rect 231 | { 232 | CGFloat top = 0.0f; 233 | switch (_verticalAlignment) { 234 | case SPPageControlVerticalAlignmentMiddle: 235 | top = CGRectGetMidY(rect) - (height / 2.0f); 236 | break; 237 | case SPPageControlVerticalAlignmentBottom: 238 | top = CGRectGetMaxY(rect) - height; 239 | break; 240 | default: 241 | break; 242 | } 243 | 244 | return top; 245 | } 246 | 247 | - (void)updateCurrentPageDisplay 248 | { 249 | _displayedPage = _currentPage; 250 | [self setNeedsDisplay]; 251 | } 252 | 253 | - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount 254 | { 255 | CGFloat marginSpace = MAX(0, pageCount - 1) * _indicatorMargin; 256 | CGFloat indicatorSpace = pageCount * _measuredIndicatorWidth; 257 | CGSize size = CGSizeMake(marginSpace + indicatorSpace, _measuredIndicatorHeight); 258 | return size; 259 | } 260 | 261 | - (CGRect)rectForPageIndicator:(NSInteger)pageIndex 262 | { 263 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 264 | return CGRectZero; 265 | } 266 | 267 | CGFloat left = [self _leftOffset]; 268 | CGSize size = [self sizeForNumberOfPages:pageIndex + 1]; 269 | CGRect rect = CGRectMake(left + size.width - _measuredIndicatorWidth, 0, _measuredIndicatorWidth, _measuredIndicatorWidth); 270 | return rect; 271 | } 272 | 273 | - (void)_setImage:(UIImage *)image forPage:(NSInteger)pageIndex type:(SPPageControlImageType)type 274 | { 275 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 276 | return; 277 | } 278 | 279 | NSMutableDictionary *dictionary = nil; 280 | switch (type) { 281 | case SPPageControlImageTypeCurrent: 282 | dictionary = self.currentPageImages; 283 | break; 284 | case SPPageControlImageTypeNormal: 285 | dictionary = self.pageImages; 286 | break; 287 | case SPPageControlImageTypeMask: 288 | dictionary = self.pageImageMasks; 289 | break; 290 | default: 291 | break; 292 | } 293 | 294 | if (image) { 295 | dictionary[@(pageIndex)] = image; 296 | } else { 297 | [dictionary removeObjectForKey:@(pageIndex)]; 298 | } 299 | } 300 | 301 | - (void)setImage:(UIImage *)image forPage:(NSInteger)pageIndex 302 | { 303 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeNormal]; 304 | [self _updateMeasuredIndicatorSizes]; 305 | } 306 | 307 | - (void)setCurrentImage:(UIImage *)image forPage:(NSInteger)pageIndex 308 | { 309 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeCurrent];; 310 | [self _updateMeasuredIndicatorSizes]; 311 | } 312 | 313 | - (void)setImageMask:(UIImage *)image forPage:(NSInteger)pageIndex 314 | { 315 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeMask]; 316 | 317 | if (nil == image) { 318 | [self.cgImageMasks removeObjectForKey:@(pageIndex)]; 319 | return; 320 | } 321 | 322 | CGImageRef maskImage = [self createMaskForImage:image]; 323 | 324 | if (maskImage) { 325 | self.cgImageMasks[@(pageIndex)] = (__bridge id)maskImage; 326 | CGImageRelease(maskImage); 327 | [self _updateMeasuredIndicatorSizeWithSize:image.size]; 328 | [self setNeedsDisplay]; 329 | } 330 | } 331 | 332 | - (id)_imageForPage:(NSInteger)pageIndex type:(SPPageControlImageType)type 333 | { 334 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 335 | return nil; 336 | } 337 | 338 | NSDictionary *dictionary = nil; 339 | switch (type) { 340 | case SPPageControlImageTypeCurrent: 341 | dictionary = _currentPageImages; 342 | break; 343 | case SPPageControlImageTypeNormal: 344 | dictionary = _pageImages; 345 | break; 346 | case SPPageControlImageTypeMask: 347 | dictionary = _pageImageMasks; 348 | break; 349 | default: 350 | break; 351 | } 352 | 353 | return dictionary[@(pageIndex)]; 354 | } 355 | 356 | - (UIImage *)imageForPage:(NSInteger)pageIndex 357 | { 358 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeNormal]; 359 | } 360 | 361 | - (UIImage *)currentImageForPage:(NSInteger)pageIndex 362 | { 363 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeCurrent]; 364 | } 365 | 366 | - (UIImage *)imageMaskForPage:(NSInteger)pageIndex 367 | { 368 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeMask]; 369 | } 370 | 371 | - (CGSize)sizeThatFits:(CGSize)size 372 | { 373 | CGSize sizeThatFits = [self sizeForNumberOfPages:self.numberOfPages]; 374 | sizeThatFits.height = MAX(sizeThatFits.height, _minHeight); 375 | return sizeThatFits; 376 | } 377 | 378 | - (CGSize)intrinsicContentSize 379 | { 380 | if (_numberOfPages < 1 || (_numberOfPages < 2 && _hidesForSinglePage)) { 381 | return CGSizeMake(UIViewNoIntrinsicMetric, 0.0f); 382 | } 383 | CGSize intrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, MAX(_measuredIndicatorHeight, _minHeight)); 384 | return intrinsicContentSize; 385 | } 386 | 387 | - (void)updatePageNumberForScrollView:(UIScrollView *)scrollView 388 | { 389 | NSInteger page = (int)floorf(scrollView.contentOffset.x / scrollView.bounds.size.width); 390 | self.currentPage = page; 391 | } 392 | 393 | - (void)setScrollViewContentOffsetForCurrentPage:(UIScrollView *)scrollView animated:(BOOL)animated 394 | { 395 | CGPoint offset = scrollView.contentOffset; 396 | offset.x = scrollView.bounds.size.width * self.currentPage; 397 | [scrollView setContentOffset:offset animated:animated]; 398 | } 399 | 400 | - (void)setStyleWithDefaults:(SPPageControlStyleDefaults)defaultStyle 401 | { 402 | switch (defaultStyle) { 403 | case SPPageControlDefaultStyleModern: 404 | self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH_LARGE; 405 | self.indicatorMargin = DEFAULT_INDICATOR_MARGIN_LARGE; 406 | self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2f]; 407 | self.minHeight = DEFAULT_MIN_HEIGHT_LARGE; 408 | break; 409 | case SPPageControlDefaultStyleClassic: 410 | default: 411 | self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH; 412 | self.indicatorMargin = DEFAULT_INDICATOR_MARGIN; 413 | self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; 414 | self.minHeight = DEFAULT_MIN_HEIGHT; 415 | break; 416 | } 417 | } 418 | 419 | #pragma mark - 420 | 421 | - (CGImageRef)createMaskForImage:(UIImage *)image CF_RETURNS_RETAINED 422 | { 423 | size_t pixelsWide = image.size.width * image.scale; 424 | size_t pixelsHigh = image.size.height * image.scale; 425 | size_t bitmapBytesPerRow = (pixelsWide * 1); 426 | CGContextRef context = CGBitmapContextCreate(NULL, pixelsWide, pixelsHigh, CGImageGetBitsPerComponent(image.CGImage), bitmapBytesPerRow, NULL, (CGBitmapInfo)kCGImageAlphaOnly); 427 | CGContextTranslateCTM(context, 0.f, pixelsHigh); 428 | CGContextScaleCTM(context, 1.0f, -1.0f); 429 | 430 | CGContextDrawImage(context, CGRectMake(0, 0, pixelsWide, pixelsHigh), image.CGImage); 431 | CGImageRef maskImage = CGBitmapContextCreateImage(context); 432 | CGContextRelease(context); 433 | 434 | return maskImage; 435 | } 436 | 437 | - (void)_updateMeasuredIndicatorSizeWithSize:(CGSize)size 438 | { 439 | _measuredIndicatorWidth = MAX(_measuredIndicatorWidth, size.width); 440 | _measuredIndicatorHeight = MAX(_measuredIndicatorHeight, size.height); 441 | } 442 | 443 | - (void)_updateMeasuredIndicatorSizes 444 | { 445 | _measuredIndicatorWidth = _indicatorDiameter; 446 | _measuredIndicatorHeight = _indicatorDiameter; 447 | 448 | // If we're only using images, ignore the _indicatorDiameter 449 | if ( (self.pageIndicatorImage || self.pageIndicatorMaskImage) && self.currentPageIndicatorImage ) 450 | { 451 | _measuredIndicatorWidth = 0; 452 | _measuredIndicatorHeight = 0; 453 | } 454 | 455 | if (self.pageIndicatorImage) { 456 | [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorImage.size]; 457 | } 458 | 459 | if (self.currentPageIndicatorImage) { 460 | [self _updateMeasuredIndicatorSizeWithSize:self.currentPageIndicatorImage.size]; 461 | } 462 | 463 | if (self.pageIndicatorMaskImage) { 464 | [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorMaskImage.size]; 465 | } 466 | 467 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 468 | [self invalidateIntrinsicContentSize]; 469 | } 470 | } 471 | 472 | 473 | #pragma mark - Tap Gesture 474 | 475 | // We're using touchesEnded: because we want to mimick UIPageControl as close as possible 476 | // As of iOS 6, UIPageControl still (as far as we know) does not use a tap gesture recognizer. This means that actions like 477 | // touching down, sliding around, and releasing, still results in the page incrementing or decrementing. 478 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 479 | { 480 | UITouch *touch = [touches anyObject]; 481 | CGPoint point = [touch locationInView:self]; 482 | 483 | if (SPPageControlTapBehaviorJump == self.tapBehavior) { 484 | 485 | __block NSInteger tappedIndicatorIndex = NSNotFound; 486 | 487 | [self.pageRects enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger index, BOOL *stop) { 488 | CGRect indicatorRect = [value CGRectValue]; 489 | 490 | if (CGRectContainsPoint(indicatorRect, point)) { 491 | tappedIndicatorIndex = index; 492 | *stop = YES; 493 | } 494 | }]; 495 | 496 | if (NSNotFound != tappedIndicatorIndex) { 497 | [self setCurrentPage:tappedIndicatorIndex sendEvent:YES canDefer:YES]; 498 | return; 499 | } 500 | } 501 | 502 | CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; 503 | CGFloat left = [self _leftOffset]; 504 | CGFloat middle = left + (size.width / 2.0f); 505 | if (point.x < middle) { 506 | [self setCurrentPage:self.currentPage - 1 sendEvent:YES canDefer:YES]; 507 | } else { 508 | [self setCurrentPage:self.currentPage + 1 sendEvent:YES canDefer:YES]; 509 | } 510 | 511 | } 512 | 513 | #pragma mark - Accessors 514 | 515 | - (void)setFrame:(CGRect)frame 516 | { 517 | [super setFrame:frame]; 518 | [self setNeedsDisplay]; 519 | } 520 | 521 | - (void)setIndicatorDiameter:(CGFloat)indicatorDiameter 522 | { 523 | if (indicatorDiameter == _indicatorDiameter) { 524 | return; 525 | } 526 | 527 | _indicatorDiameter = indicatorDiameter; 528 | 529 | // Absolute minimum height of the control is the indicator diameter 530 | if (_minHeight < indicatorDiameter) { 531 | self.minHeight = indicatorDiameter; 532 | } 533 | 534 | [self _updateMeasuredIndicatorSizes]; 535 | [self setNeedsDisplay]; 536 | } 537 | 538 | - (void)setIndicatorMargin:(CGFloat)indicatorMargin 539 | { 540 | if (indicatorMargin == _indicatorMargin) { 541 | return; 542 | } 543 | 544 | _indicatorMargin = indicatorMargin; 545 | [self setNeedsDisplay]; 546 | } 547 | 548 | - (void)setMinHeight:(CGFloat)minHeight 549 | { 550 | if (minHeight == _minHeight) { 551 | return; 552 | } 553 | 554 | // Absolute minimum height of the control is the indicator diameter 555 | if (minHeight < _indicatorDiameter) { 556 | minHeight = _indicatorDiameter; 557 | } 558 | 559 | _minHeight = minHeight; 560 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 561 | [self invalidateIntrinsicContentSize]; 562 | } 563 | [self setNeedsLayout]; 564 | } 565 | 566 | - (void)setNumberOfPages:(NSInteger)numberOfPages 567 | { 568 | if (numberOfPages == _numberOfPages) { 569 | return; 570 | } 571 | 572 | self.accessibilityPageControl.numberOfPages = numberOfPages; 573 | 574 | _numberOfPages = MAX(0, numberOfPages); 575 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 576 | [self invalidateIntrinsicContentSize]; 577 | } 578 | [self updateAccessibilityValue]; 579 | [self setNeedsDisplay]; 580 | } 581 | 582 | - (void)setCurrentPage:(NSInteger)currentPage 583 | { 584 | [self setCurrentPage:currentPage sendEvent:NO canDefer:NO]; 585 | } 586 | 587 | - (void)setCurrentPage:(NSInteger)currentPage sendEvent:(BOOL)sendEvent canDefer:(BOOL)defer 588 | { 589 | _currentPage = MIN(MAX(0, currentPage), _numberOfPages - 1); 590 | self.accessibilityPageControl.currentPage = self.currentPage; 591 | 592 | [self updateAccessibilityValue]; 593 | 594 | if (NO == self.defersCurrentPageDisplay || NO == defer) { 595 | _displayedPage = _currentPage; 596 | [self setNeedsDisplay]; 597 | } 598 | 599 | if (sendEvent) { 600 | [self sendActionsForControlEvents:UIControlEventValueChanged]; 601 | } 602 | } 603 | 604 | - (void)setCurrentPageIndicatorImage:(UIImage *)currentPageIndicatorImage 605 | { 606 | if ([currentPageIndicatorImage isEqual:_currentPageIndicatorImage]) { 607 | return; 608 | } 609 | 610 | _currentPageIndicatorImage = currentPageIndicatorImage; 611 | [self _updateMeasuredIndicatorSizes]; 612 | [self setNeedsDisplay]; 613 | } 614 | 615 | - (void)setPageIndicatorImage:(UIImage *)pageIndicatorImage 616 | { 617 | if ([pageIndicatorImage isEqual:_pageIndicatorImage]) { 618 | return; 619 | } 620 | 621 | _pageIndicatorImage = pageIndicatorImage; 622 | [self _updateMeasuredIndicatorSizes]; 623 | [self setNeedsDisplay]; 624 | } 625 | 626 | - (void)setPageIndicatorMaskImage:(UIImage *)pageIndicatorMaskImage 627 | { 628 | if ([pageIndicatorMaskImage isEqual:_pageIndicatorMaskImage]) { 629 | return; 630 | } 631 | 632 | _pageIndicatorMaskImage = pageIndicatorMaskImage; 633 | 634 | if (_pageImageMask) { 635 | CGImageRelease(_pageImageMask); 636 | } 637 | 638 | _pageImageMask = [self createMaskForImage:_pageIndicatorMaskImage]; 639 | 640 | [self _updateMeasuredIndicatorSizes]; 641 | [self setNeedsDisplay]; 642 | } 643 | 644 | - (NSMutableDictionary *)pageNames 645 | { 646 | if (nil != _pageNames) { 647 | return _pageNames; 648 | } 649 | 650 | _pageNames = [[NSMutableDictionary alloc] init]; 651 | return _pageNames; 652 | } 653 | 654 | - (NSMutableDictionary *)pageImages 655 | { 656 | if (nil != _pageImages) { 657 | return _pageImages; 658 | } 659 | 660 | _pageImages = [[NSMutableDictionary alloc] init]; 661 | return _pageImages; 662 | } 663 | 664 | - (NSMutableDictionary *)currentPageImages 665 | { 666 | if (nil != _currentPageImages) { 667 | return _currentPageImages; 668 | } 669 | 670 | _currentPageImages = [[NSMutableDictionary alloc] init]; 671 | return _currentPageImages; 672 | } 673 | 674 | - (NSMutableDictionary *)pageImageMasks 675 | { 676 | if (nil != _pageImageMasks) { 677 | return _pageImageMasks; 678 | } 679 | 680 | _pageImageMasks = [[NSMutableDictionary alloc] init]; 681 | return _pageImageMasks; 682 | } 683 | 684 | - (NSMutableDictionary *)cgImageMasks 685 | { 686 | if (nil != _cgImageMasks) { 687 | return _cgImageMasks; 688 | } 689 | 690 | _cgImageMasks = [[NSMutableDictionary alloc] init]; 691 | return _cgImageMasks; 692 | } 693 | 694 | #pragma mark - UIAccessibility 695 | 696 | - (void)setName:(NSString *)name forPage:(NSInteger)pageIndex 697 | { 698 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 699 | return; 700 | } 701 | 702 | self.pageNames[@(pageIndex)] = name; 703 | 704 | } 705 | 706 | - (NSString *)nameForPage:(NSInteger)pageIndex 707 | { 708 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 709 | return nil; 710 | } 711 | 712 | return self.pageNames[@(pageIndex)]; 713 | } 714 | 715 | - (void)updateAccessibilityValue 716 | { 717 | NSString *pageName = [self nameForPage:self.currentPage]; 718 | NSString *accessibilityValue = self.accessibilityPageControl.accessibilityValue; 719 | 720 | if (pageName) { 721 | self.accessibilityValue = [NSString stringWithFormat:@"%@ - %@", pageName, accessibilityValue]; 722 | } else { 723 | self.accessibilityValue = accessibilityValue; 724 | } 725 | } 726 | 727 | @end 728 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SecondViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.h 3 | // SPCarouselView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 iDress. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface SecondViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/SecondViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.m 3 | // SPCarouselView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 iDress. All rights reserved. 7 | // 8 | 9 | #import "SecondViewController.h" 10 | 11 | @interface SecondViewController () 12 | 13 | @end 14 | 15 | @implementation SecondViewController 16 | 17 | - (void)viewDidLoad { 18 | [super viewDidLoad]; 19 | self.view.backgroundColor = [UIColor whiteColor]; 20 | } 21 | 22 | - (void)viewWillAppear:(BOOL)animated { 23 | [super viewWillAppear:animated]; 24 | self.navigationController.navigationBarHidden = NO; 25 | } 26 | 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "SPCycleScrollView.h" 11 | #import "SecondViewController.h" 12 | 13 | #define kScreenWidth [UIScreen mainScreen].bounds.size.width 14 | #define kScreenHeight [UIScreen mainScreen].bounds.size.height 15 | 16 | #define kCycleScrollViewH 200 17 | 18 | #define isIPhoneX [UIScreen mainScreen].bounds.size.height >= 812 19 | #define topMargin (isIPhoneX ? 44 : 0) 20 | #define bottomMargin (isIPhoneX ? 34 : 0) 21 | #define statusBarH (isIPhoneX ? 44 : 20) 22 | 23 | @interface ViewController () 24 | 25 | @property (nonatomic, strong) UITableView *tableView; 26 | 27 | @property (nonatomic, strong) SPCycleScrollView *cycleScrollView; 28 | 29 | @property (nonatomic, strong) NSArray *localPhotos; 30 | 31 | @property (nonatomic, strong) NSArray *urlPhotos; 32 | 33 | @end 34 | 35 | @implementation ViewController 36 | 37 | - (void)viewDidLoad { 38 | [super viewDidLoad]; 39 | 40 | [self.view addSubview:self.tableView]; 41 | if (@available(iOS 11.0, *)) { 42 | self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 43 | } else { 44 | self.automaticallyAdjustsScrollViewInsets = NO; 45 | } 46 | 47 | 48 | // 本地图片数组 49 | self.localPhotos = @[@"景1.jpg",@"景2.jpg",@"景3.jpg",@"景4.jpg",@"景5.jpg",@"dog.gif"]; 50 | 51 | // 网络图片数组 52 | self.urlPhotos = @[ 53 | @"http://pic34.nipic.com/20131028/2455348_171218804000_2.jpg", 54 | @"http://img1.3lian.com/2015/a2/228/d/129.jpg", 55 | @"http://img.boqiicdn.com/Data/Bbs/Pushs/img79891399602390.jpg", 56 | @"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1541655680047&di=bdaf0cda4be4d9bb45a5ca82d7190c49&imgtype=0&src=http%3A%2F%2Fimg2.cache.netease.com%2Fnews%2F2016%2F9%2F6%2F20160906095242ffc74.gif", 57 | @"http://img1.3lian.com/2015/a2/243/d/187.jpg", 58 | @"http://pic7.nipic.com/20100503/1792030_163333013611_2.jpg", 59 | @"http://www.microfotos.com/pic/0/90/9023/902372preview4.jpg", 60 | @"http://pic1.win4000.com/wallpaper/b/55b9e2271b119.jpg" 61 | ]; 62 | 63 | // 示例1 64 | // [self localTest1]; 65 | 66 | // 示例2 67 | // [self localTest2]; 68 | 69 | // 示例3 70 | // [self urlTest3]; 71 | 72 | // 示例4 73 | [self urlTest4]; 74 | 75 | 76 | // 不要直接把self.headerView赋值给tableView.tableHeaderView,否则无法实现下拉放大 77 | UIView *tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kCycleScrollViewH)]; 78 | [tableHeaderView addSubview:self.cycleScrollView]; 79 | 80 | self.tableView.tableHeaderView = tableHeaderView; 81 | 82 | } 83 | 84 | - (void)viewWillAppear:(BOOL)animated { 85 | [super viewWillAppear:animated]; 86 | [self.cycleScrollView adjustWhenControllerViewWillAppear]; 87 | 88 | self.navigationController.navigationBarHidden = YES; 89 | } 90 | 91 | #pragma mark - 本地图片示例 92 | // 示例1 本地图片,类方法创建 93 | - (void)localTest1 { 94 | 95 | SPCycleScrollView *cycleScrollView = [SPCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 0, kScreenWidth, kCycleScrollViewH) localImages:self.localPhotos placeholderImage:[UIImage imageNamed:@"placeholder"]]; 96 | self.cycleScrollView = cycleScrollView; 97 | } 98 | 99 | // 示例2 本地图片,alloc init创建 100 | - (void)localTest2 { 101 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] init]; 102 | self.cycleScrollView = cycleScrollView; 103 | cycleScrollView.frame = CGRectMake(0, 0, kScreenWidth, kCycleScrollViewH); 104 | cycleScrollView.pageControl.pageIndicatorTintColor = [UIColor redColor]; 105 | cycleScrollView.pageControl.hidesForSinglePage = NO; 106 | cycleScrollView.localImages = self.localPhotos; 107 | // cycleScrollView.autoScroll = NO; 108 | 109 | } 110 | 111 | #pragma mark - 网络图片示例 112 | // 示例3 网络图片,类方法创建 113 | - (void)urlTest3 { 114 | SPCycleScrollView *cycleScrollView = [SPCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 0, kScreenWidth, kCycleScrollViewH) urlImages:self.urlPhotos placeholderImage:[UIImage imageNamed:@"placeholder"]]; 115 | cycleScrollView.pageControl.pageIndicatorTintColor = [UIColor blackColor]; 116 | cycleScrollView.pageControl.currentPageIndicatorTintColor = [UIColor redColor]; 117 | cycleScrollView.placeholderImage = [UIImage imageNamed:@"placeholder"]; 118 | self.cycleScrollView = cycleScrollView; 119 | } 120 | 121 | // 示例4 网络图片,alloc init创建 122 | - (void)urlTest4 { 123 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] init]; 124 | cycleScrollView.frame = CGRectMake(0, 0, kScreenWidth, kCycleScrollViewH); 125 | cycleScrollView.pageControl.currentPageIndicatorImage = [UIImage imageNamed:@"currentDot"]; 126 | cycleScrollView.pageControl.pageIndicatorImage = [UIImage imageNamed:@"otherDot"]; 127 | cycleScrollView.pageControlPosition = SPPageContolPositionBottomRight; 128 | 129 | // cycleScrollView.titleLabelBackgroundColor = [UIColor clearColor]; 130 | // cycleScrollView.titleLabelTextColor = [UIColor redColor]; 131 | // cycleScrollView.titleLabelFont = [UIFont boldSystemFontOfSize:16]; 132 | cycleScrollView.urlImages = self.urlPhotos; 133 | cycleScrollView.titles = @[@"两只小猫咪",@"两只小狗",@"戴毛巾的狗",@"gif图",@"好难过",@"含情脉脉",@"3只狗吐舌头",@"一只浪漫多情的狗"]; 134 | 135 | self.cycleScrollView = cycleScrollView; 136 | 137 | } 138 | 139 | #pragma mark - SPCycleScrollViewDelegate 140 | - (void)cycleScrollView:(SPCycleScrollView *)cycleScrollView clickedImageAtIndex:(NSUInteger)index { 141 | NSLog(@"代理方式:点击了第%zd张图片",index); 142 | } 143 | 144 | #pragma mark - TableView DataSource 145 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 146 | return 20; 147 | } 148 | 149 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 150 | static NSString *ID = @"cell"; 151 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; 152 | if (cell == nil) { 153 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID]; 154 | } 155 | cell.textLabel.text = [NSString stringWithFormat:@"第%zd行",indexPath.row]; 156 | return cell; 157 | } 158 | 159 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 160 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 161 | SecondViewController *secVc = [[SecondViewController alloc] init]; 162 | [self.navigationController pushViewController:secVc animated:YES]; 163 | } 164 | 165 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 166 | 167 | CGFloat width = self.view.frame.size.width; 168 | 169 | CGFloat offsetY = scrollView.contentOffset.y; 170 | 171 | if (scrollView == self.tableView) { 172 | 173 | // 偏移的y值 174 | if(offsetY < 0) { 175 | CGFloat totalOffset = kCycleScrollViewH + fabs(offsetY); 176 | CGFloat f = totalOffset / kCycleScrollViewH; 177 | // 拉伸后的图片的frame应该是同比例缩放。 178 | self.cycleScrollView.frame = CGRectMake(-(width*f-width) / 2.0, offsetY, width * f, totalOffset); 179 | } 180 | } 181 | } 182 | 183 | // 这2个方法几乎可以不实现,这里z实现的目的为了解决下拉放大时的那一刻刚好碰到轮播图在切换的时候可能会闪一下,尽管这种几率很小,实现下面两个代理方法后就可以避免这个问题 184 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 185 | self.cycleScrollView.autoScroll = NO; 186 | } 187 | 188 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 189 | self.cycleScrollView.autoScroll = YES; 190 | } 191 | 192 | - (UITableView *)tableView { 193 | 194 | if (!_tableView) { 195 | _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, topMargin, kScreenWidth, kScreenHeight-topMargin-bottomMargin) style:UITableViewStylePlain]; 196 | _tableView.dataSource = self; 197 | _tableView.delegate = self; 198 | } 199 | return _tableView; 200 | } 201 | 202 | - (void)didReceiveMemoryWarning { 203 | [super didReceiveMemoryWarning]; 204 | 205 | } 206 | 207 | @end 208 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollView/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollViewTests/SPCycleScrollViewTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollViewTests.m 3 | // SPCycleScrollViewTests 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SPCycleScrollViewTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation SPCycleScrollViewTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollViewUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/SPCycleScrollViewUITests/SPCycleScrollViewUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollViewUITests.m 3 | // SPCycleScrollViewUITests 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SPCycleScrollViewUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation SPCycleScrollViewUITests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | self.continueAfterFailure = NO; 22 | 23 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 24 | [[[XCUIApplication alloc] init] launch]; 25 | 26 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 27 | } 28 | 29 | - (void)tearDown { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | } 32 | 33 | - (void)testExample { 34 | // Use recording to get started writing UI tests. 35 | // Use XCTAssert and related functions to verify your tests produce the correct results. 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | ``` 3 | target 'MyApp' do 4 | pod 'SPCycleScrollView', '~> 2.0.1' 5 | end 6 | ``` 7 | ## 功能 8 | - [x] 支持本地图片和网络图片 9 | - [x] 支持本地gif和网络gif  10 | - [x] 自定义pageControl,可以对pageControl自由定制各种属性 11 | - [x] pageControl支持当前图片和其余图片的设置,当前图片和其余图片大小可以不一致 12 | - [x] 可以设置pageControl的位置(提供左中右3种位置) 13 | - [x] 可以设置图片的内容填充模式 14 | - [x] 可显示图片对应的标题 15 | - [x] 支持autoLayout、xib和storyboard 16 | - [x] 支持轮播图下拉放大 17 | 18 | ## 主要内容 19 | ``` 20 | // 提供类方法创建轮播图 这种创建方式有个局限性,那就是必须在创建时就传入数组。 21 | /** 本地图片 */ 22 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame localImages:(nonnull NSArray *)localImages placeholderImage:(nullable UIImage *)image; 23 | 24 | /** 网络图片 */ 25 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame urlImages:(nonnull NSArray *)urlImages placeholderImage:(nullable UIImage *)image; 26 | 27 | 28 | // 为了消除类方法创建的局限性,提供下面两个属性,轮播图的图片数组。适用于创建时用alloc init,然后在以后的某个时刻传入数组。 29 | @property(strong, nonatomic) NSArray *localImages; // 本地图片 30 | @property(strong, nonatomic) NSArray *urlImages; // 网络图片 31 | 32 | @property (nonatomic, strong) NSArray *titles; // 图片对应的标题数组,如果标题个数小于图片个数,内部会用空字符串补足 33 | @property (nonatomic, strong) UIColor *titleLabelBackgroundColor; // 图片上label的背景色,默认是[UIColor colorWithWhite:0 alpha:0.5] 34 | @property (nonatomic, strong) UIColor *titleLabelTextColor; // 图片上label的文字颜色,默认是白色 35 | @property (nonatomic, strong) UIFont *titleLabelFont; // 图片上label的字体 36 | 37 | @property(weak, nonatomic) id delegate; // 代理 38 | @property (nonatomic, copy) ClickedImageBlock clickedImageBlock; // 轮播图的图片被点击时回调的block,与代理功能一致,开发者可二选其一.如果两种方式不小心同时实现了,则默认block方式 39 | 40 | - (void)adjustWhenControllerViewWillAppear; // 解决viewWillAppear时出现时轮播图卡在一半的问题,在控制器viewWillAppear时调用此方法 41 | 42 | @property(assign ,nonatomic) NSTimeInterval duration; // 图片自动切换间隔时间, 默认设置为 2s 43 | 44 | @property (assign ,nonatomic, getter=isAutoScroll) BOOL autoScroll; // 是否自动轮播,默认为YES 45 | 46 | @property (nonatomic, assign) UIViewContentMode imageMode; // 设置图片的内容模式,默认为UIViewContentModeScaleToFill 47 | 48 | @property (nonatomic, strong) UIImage *placeholderImage; // 占位图,默认nil,必须在设置图片数组之前设置才有效 49 | 50 | @property (nonatomic, assign) BOOL autoCache;// 是否开启图片缓存,默认为YES 51 | 52 | @property (nonatomic, strong, readonly) SPPageControl *pageControl; // 自定义的pageControl,可拿到此对象自行定义你所需要的样式,例如小圆点的颜色,图片,隐藏等 53 | @property (assign, nonatomic) SPPageContolPosition pageControlPosition; // pageControl的位置,分左,中,右 54 | 55 | + (void)clearDiskCache; 56 | ``` 57 | ## 具体效果和使用明细可参考简书:http://www.jianshu.com/p/35bdf1e9c8b6 58 | -------------------------------------------------------------------------------- /SPCycleScrollView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint SPCycleScrollView.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "SPCycleScrollView" 19 | s.version = "2.0.1" 20 | s.summary = "轮播图." 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | s.description = <<-DESC 28 | 这是一个轮播图 29 | DESC 30 | 31 | s.homepage = "https://github.com/SPStore/SPCycleScrollView" 32 | # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" 33 | 34 | 35 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 36 | # 37 | # Licensing your code is important. See http://choosealicense.com for more info. 38 | # CocoaPods will detect a license file if there is a named LICENSE* 39 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 40 | # 41 | 42 | s.license = "MIT" 43 | # s.license = { :type => "MIT", :file => "FILE_LICENSE" } 44 | 45 | 46 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 47 | # 48 | # Specify the authors of the library, with email addresses. Email addresses 49 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 50 | # accepts just a name if you'd rather not provide an email address. 51 | # 52 | # Specify a social_media_url where others can refer to, for example a twitter 53 | # profile URL. 54 | # 55 | 56 | s.author = { "SPStore" => "lesp163@163.com" } 57 | # Or just: s.author = "SPStore" 58 | # s.authors = { "SPStore" => "" } 59 | # s.social_media_url = "http://twitter.com/SPStore" 60 | 61 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 62 | # 63 | # If this Pod runs only on iOS or OS X, then specify the platform and 64 | # the deployment target. You can optionally include the target after the platform. 65 | # 66 | 67 | s.platform = :ios 68 | s.platform = :ios, "8.0" 69 | 70 | # When using multiple platforms 71 | s.ios.deployment_target = "8.0" 72 | # s.osx.deployment_target = "10.7" 73 | # s.watchos.deployment_target = "2.0" 74 | # s.tvos.deployment_target = "9.0" 75 | 76 | 77 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 78 | # 79 | # Specify the location from where the source should be retrieved. 80 | # Supports git, hg, bzr, svn and HTTP. 81 | # 82 | 83 | s.source = { :git => "https://github.com/SPStore/SPCycleScrollView.git", :tag => s.version } 84 | 85 | 86 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 87 | # 88 | # CocoaPods is smart about how it includes source code. For source files 89 | # giving a folder will include any swift, h, m, mm, c & cpp files. 90 | # For header files it will include any header in the folder. 91 | # Not including the public_header_files will make all headers public. 92 | # 93 | 94 | s.source_files = "SPCycleScrollView" 95 | s.exclude_files = "Classes/Exclude" 96 | 97 | # s.public_header_files = "Classes/**/*.h" 98 | 99 | 100 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 101 | # 102 | # A list of resources included with the Pod. These are copied into the 103 | # target bundle with a build phase script. Anything else will be cleaned. 104 | # You can preserve files from being cleaned, please don't preserve 105 | # non-essential files like tests, examples and documentation. 106 | # 107 | 108 | # s.resource = "icon.png" 109 | # s.resources = "Resources/*.png" 110 | 111 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 112 | 113 | 114 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 115 | # 116 | # Link your library with frameworks, or libraries. Libraries do not include 117 | # the lib prefix of their name. 118 | # 119 | 120 | # s.framework = "SomeFramework" 121 | # s.frameworks = "SomeFramework", "AnotherFramework" 122 | 123 | # s.library = "iconv" 124 | # s.libraries = "iconv", "xml2" 125 | 126 | 127 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 128 | # 129 | # If your library depends on compiler flags you can set them in the xcconfig hash 130 | # where they will only apply to your library. If you depend on other Podspecs 131 | # you can include multiple dependencies to ensure it works. 132 | 133 | s.requires_arc = true 134 | 135 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 136 | # s.dependency "SDWebImage" 137 | 138 | end 139 | -------------------------------------------------------------------------------- /SPCycleScrollView/SPCycleScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollView.h 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | // 本框架的github地址:https://github.com/SPStore/SPCycleScrollView 10 | 11 | #import 12 | #import "SPPageControl.h" 13 | 14 | @class SPCycleScrollView; 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | 19 | @protocol SPCycleScrollViewDelegate 20 | @optional 21 | // 轮播图的图片被点击时触发的代理方法,index为点击的图片下标 22 | - (void)cycleScrollView:(SPCycleScrollView *)cycleScrollView clickedImageAtIndex:(NSUInteger)index; 23 | 24 | @end 25 | 26 | typedef void(^ClickedImageBlock)(NSUInteger index); 27 | 28 | typedef NS_ENUM(NSInteger, SPPageContolPosition) { 29 | SPPageContolPositionBottomCenter, // 底部中心 30 | SPPageContolPositionBottomRight, // 底部右边 31 | SPPageContolPositionBottomLeft // 底部左边 32 | }; 33 | 34 | @interface SPCycleScrollView : UIView 35 | 36 | // 提供类方法创建轮播图 这种创建方式有个局限性,那就是必须在创建时就传入数组。 37 | /** 本地图片 */ 38 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame localImages:(nonnull NSArray *)localImages placeholderImage:(nullable UIImage *)image; 39 | 40 | /** 网络图片 */ 41 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame urlImages:(nonnull NSArray *)urlImages placeholderImage:(nullable UIImage *)image; 42 | 43 | 44 | // 为了消除类方法创建的局限性,提供下面两个属性,轮播图的图片数组。适用于创建时用alloc init,然后在以后的某个时刻传入数组。 45 | @property(strong, nonatomic) NSArray *localImages; // 本地图片 46 | @property(strong, nonatomic) NSArray *urlImages; // 网络图片 47 | 48 | @property (nonatomic, strong) NSArray *titles; // 图片对应的标题数组,如果标题个数小于图片个数,内部会用空字符串补足 49 | @property (nonatomic, strong) UIColor *titleLabelBackgroundColor; // 图片上label的背景色,默认是[UIColor colorWithWhite:0 alpha:0.5] 50 | @property (nonatomic, strong) UIColor *titleLabelTextColor; // 图片上label的文字颜色,默认是白色 51 | @property (nonatomic, strong) UIFont *titleLabelFont; // 图片上label的字体 52 | 53 | @property(weak, nonatomic) id delegate; // 代理 54 | @property (nonatomic, copy) ClickedImageBlock clickedImageBlock; // 轮播图的图片被点击时回调的block,与代理功能一致,开发者可二选其一.如果两种方式不小心同时实现了,则默认block方式 55 | 56 | - (void)adjustWhenControllerViewWillAppear; // 解决viewWillAppear时出现时轮播图卡在一半的问题,在控制器viewWillAppear时调用此方法 57 | 58 | @property(assign ,nonatomic) NSTimeInterval duration; // 图片自动切换间隔时间, 默认设置为 2s 59 | 60 | @property (assign ,nonatomic, getter=isAutoScroll) BOOL autoScroll; // 是否自动轮播,默认为YES 61 | 62 | @property (nonatomic, assign) UIViewContentMode imageMode; // 设置图片的内容模式,默认为UIViewContentModeScaleToFill 63 | 64 | @property (nonatomic, strong) UIImage *placeholderImage; // 占位图,默认nil,必须在设置图片数组之前设置才有效 65 | 66 | @property (nonatomic, assign) BOOL autoCache;// 是否开启图片缓存,默认为YES 67 | 68 | @property (nonatomic, strong, readonly) SPPageControl *pageControl; // 自定义的pageControl,可拿到此对象自行定义你所需要的样式,例如小圆点的颜色,图片,隐藏等 69 | @property (assign, nonatomic) SPPageContolPosition pageControlPosition; // pageControl的位置,分左,中,右 70 | 71 | + (void)clearDiskCache; 72 | @end 73 | 74 | NS_ASSUME_NONNULL_END 75 | -------------------------------------------------------------------------------- /SPCycleScrollView/SPCycleScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPCycleScrollView.m 3 | // SPCycleScrollView 4 | // 5 | // Created by 乐升平 on 2018/10/29. 6 | // Copyright © 2018 乐升平. All rights reserved. 7 | // 8 | 9 | #import "SPCycleScrollView.h" 10 | 11 | #define kWidth ceilf(self.bounds.size.width) // 加ceil取上整,是为了保证scrollView.contentOffset.x能够更加准确的和kwidth作比较,例如在scrollViewDidScroll:方法中就有这样的比较,当外界进行了下拉放大时,kWidth可能是一个很长的浮点数,而scrollView.contentOffset.x直接依赖于kWidth会导致比较不准确,所以kWidth也取上整 12 | #define kHeight self.bounds.size.height 13 | 14 | #define kPageControlMargin 10.0f 15 | 16 | typedef NS_ENUM(NSInteger, SPCycleImageViewLabelPosition) { 17 | SPCycleImageViewLabelPositionCenter, // 底部中心 18 | SPCycleImageViewLabelPositionRight, // 底部右边 19 | SPCycleImageViewLabelPositionLeft // 底部左边 20 | }; 21 | 22 | @interface SPCycleImageView : UIImageView 23 | @property (nonatomic, strong) UIView *labelContentView; 24 | @property (nonatomic, strong) UILabel *titleLabel; 25 | @property (nonatomic, copy) NSString *title; 26 | 27 | - (void)setLabelPosition:(SPCycleImageViewLabelPosition)labelPosition remainderSpacing:(CGFloat)remainderSpacing; 28 | @property (nonatomic, assign) CGFloat remainderSpacing; // label的剩余空间,比pageControl的宽度略大 29 | @property (nonatomic, assign) SPCycleImageViewLabelPosition labelPosition; // label的位置 30 | @end 31 | 32 | @implementation SPCycleImageView 33 | 34 | - (instancetype)initWithFrame:(CGRect)frame { 35 | if (self = [super initWithFrame:frame]) { 36 | [self setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 37 | [self addSubview:self.labelContentView]; 38 | [self.labelContentView addSubview:self.titleLabel]; 39 | } 40 | return self; 41 | } 42 | 43 | - (void)setTitle:(NSString *)title { 44 | _title = title; 45 | self.titleLabel.text = title; 46 | } 47 | 48 | - (void)setLabelPosition:(SPCycleImageViewLabelPosition)labelPosition remainderSpacing:(CGFloat)remainderSpacing { 49 | _labelPosition = labelPosition; 50 | _remainderSpacing = remainderSpacing; 51 | switch (_labelPosition) { 52 | case SPCycleImageViewLabelPositionCenter: 53 | self.titleLabel.textAlignment = NSTextAlignmentCenter; 54 | break; 55 | case SPCycleImageViewLabelPositionRight: 56 | self.titleLabel.textAlignment = NSTextAlignmentRight; 57 | break; 58 | case SPCycleImageViewLabelPositionLeft: 59 | self.titleLabel.textAlignment = NSTextAlignmentLeft; 60 | break; 61 | default: 62 | break; 63 | } 64 | [self setNeedsLayout]; 65 | [self layoutIfNeeded]; 66 | } 67 | 68 | - (void)layoutSubviews { 69 | [super layoutSubviews]; 70 | 71 | CGFloat width = self.bounds.size.width; 72 | CGFloat height = self.bounds.size.height; 73 | CGFloat labelH = 30; 74 | 75 | self.labelContentView.frame = CGRectMake(0, height-labelH, width, labelH); 76 | 77 | switch (_labelPosition) { 78 | case SPCycleImageViewLabelPositionCenter: 79 | self.titleLabel.frame = CGRectMake(10, 0, width-20, labelH); 80 | break; 81 | case SPCycleImageViewLabelPositionRight: 82 | self.titleLabel.frame = CGRectMake(_remainderSpacing+20, 0, width-(_remainderSpacing+20+10), labelH); 83 | break; 84 | case SPCycleImageViewLabelPositionLeft: 85 | self.titleLabel.frame = CGRectMake(10, 0, width-(_remainderSpacing+20+10), labelH); 86 | break; 87 | default: 88 | break; 89 | } 90 | } 91 | 92 | - (UIView *)labelContentView { 93 | if (!_labelContentView) { 94 | _labelContentView = [[UIView alloc] init]; 95 | _labelContentView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; 96 | _labelContentView.hidden = YES; 97 | } 98 | return _labelContentView; 99 | } 100 | 101 | - (UILabel *)titleLabel { 102 | if (!_titleLabel) { 103 | _titleLabel = [[UILabel alloc] init]; 104 | _titleLabel.textColor = [UIColor whiteColor]; 105 | } 106 | return _titleLabel; 107 | } 108 | 109 | @end 110 | 111 | typedef NS_ENUM(NSInteger, SPCycleScrollViewImagesDataType){ 112 | SPCycleScrollViewImagesDataTypeInLocal,// 本地图片标记 113 | SPCycleScrollViewImagesDataTypeInURL // URL图片标记 114 | }; 115 | 116 | @interface SPCycleScrollView () 117 | 118 | @property(strong, nonatomic) UIScrollView *scrollView; 119 | @property(strong, nonatomic) SPPageControl *pageControl; 120 | 121 | // 前一个视图,当前视图,下一个视图 122 | @property(strong, nonatomic) SPCycleImageView *lastImgView; 123 | @property(strong, nonatomic) SPCycleImageView *currentImgView; 124 | @property(strong, nonatomic) SPCycleImageView *nextImgView; 125 | 126 | // 图片来源(本地或URL) 127 | @property(nonatomic) SPCycleScrollViewImagesDataType carouseImagesType; 128 | 129 | @property(strong, nonatomic) NSTimer *timer; 130 | 131 | // kImageCount = array.count,图片数组个数 132 | @property(assign, nonatomic) NSInteger kImageCount; 133 | 134 | // 记录nextImageView的下标 默认从1开始 135 | @property(assign, nonatomic) NSInteger nextPhotoIndex; 136 | // 记录lastImageView的下标 默认从 _kImageCount - 1 开始 137 | @property(assign, nonatomic) NSInteger lastPhotoIndex; 138 | 139 | //pageControl图片大小 140 | @property (nonatomic, assign) CGSize pageImageSize; 141 | 142 | @property (nonatomic, strong) NSOperationQueue *queue; 143 | 144 | @end 145 | 146 | static NSString *cache; 147 | 148 | @implementation SPCycleScrollView 149 | 150 | #pragma mark - 初始化方法 151 | - (instancetype)initWithFrame:(CGRect)frame{ 152 | self = [super initWithFrame:frame]; 153 | if (self) { 154 | [self initialize]; 155 | } 156 | return self; 157 | } 158 | 159 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 160 | if (self = [super initWithCoder:aDecoder]) { 161 | [self initialize]; 162 | } 163 | return self; 164 | } 165 | 166 | - (void)initialize { 167 | _duration = 2.0; 168 | _autoScroll = YES; 169 | _autoCache = YES; 170 | _titleLabelBackgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; 171 | _titleLabelTextColor = [UIColor whiteColor]; 172 | _titleLabelFont = [UIFont systemFontOfSize:17]; 173 | 174 | cache = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"XLsn0wLoop"]; 175 | BOOL isDir = NO; 176 | BOOL isExists = [[NSFileManager defaultManager] fileExistsAtPath:cache isDirectory:&isDir]; 177 | if (!isExists || !isDir) { 178 | [[NSFileManager defaultManager] createDirectoryAtPath:cache withIntermediateDirectories:YES attributes:nil error:nil]; 179 | } 180 | } 181 | 182 | #pragma mark - Public Method 183 | // 如果是本地图片调用此方法 184 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame localImages:(NSArray *)localImages placeholderImage:(UIImage *)image { 185 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] initWithFrame:frame]; 186 | cycleScrollView.placeholderImage = image; 187 | // 调用set方法 188 | cycleScrollView.localImages = localImages; 189 | return cycleScrollView; 190 | } 191 | 192 | // 如果是网络图片调用此方法 193 | + (SPCycleScrollView *)cycleScrollViewWithFrame:(CGRect)frame urlImages:(NSArray *)urlImages placeholderImage:(UIImage *)image { 194 | SPCycleScrollView *cycleScrollView = [[SPCycleScrollView alloc] initWithFrame:frame]; 195 | cycleScrollView.placeholderImage = image; 196 | // 调用set方法 197 | cycleScrollView.urlImages = urlImages; 198 | return cycleScrollView; 199 | } 200 | 201 | - (void)adjustWhenControllerViewWillAppear { 202 | // 将轮播图的偏移量设回中间位置 203 | if (self.kImageCount > 1) { 204 | self.scrollView.contentOffset = CGPointMake(kWidth, 0); 205 | } 206 | } 207 | 208 | #pragma maek - Private Method 209 | // 开启定时器 210 | - (void)openTimer { 211 | // 开启之前一定要先将上一次开启的定时器关闭,否则会跟新的定时器重叠 212 | [self closeTimer]; 213 | if (_autoScroll && self.kImageCount > 1) { 214 | _timer = [NSTimer scheduledTimerWithTimeInterval:self.duration target:self selector:@selector(timerAction) userInfo:self repeats:YES]; 215 | // 当外界滑动其他scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer事件. 216 | // 因此,要保证timer事件不中断,就必须把_timer加入到NSRunLoopCommonModes模式下的 RunLoop中。也可以加入到UITrackingRunLoopMode 217 | [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; 218 | } 219 | } 220 | 221 | // 关闭定时器 222 | - (void)closeTimer { 223 | [_timer invalidate]; 224 | _timer = nil; 225 | } 226 | 227 | // timer事件 228 | - (void)timerAction{ 229 | // 定时器每次触发都让当前图片为轮播图的第三张ImageView的image 230 | [_scrollView setContentOffset:CGPointMake(kWidth*2, 0) animated:YES]; 231 | } 232 | 233 | - (void)configure{ 234 | [self addSubview:self.scrollView]; 235 | // 添加最初的三张imageView 236 | if (self.kImageCount > 1) { 237 | [self.scrollView addSubview:self.lastImgView]; 238 | [self.scrollView addSubview:self.currentImgView]; 239 | [self.scrollView addSubview:self.nextImgView]; 240 | 241 | // 将上一张图片设置为数组中最后一张图片 242 | [self setImageView:_lastImgView withSubscript:(_kImageCount-1)]; 243 | // 将当前图片设置为数组中第一张图片 244 | [self setImageView:_currentImgView withSubscript:0]; 245 | // 将下一张图片设置为数组中第二张图片,如果数组只有一张图片,则上、中、下图片全部是数组中的第一张图片 246 | [self setImageView:_nextImgView withSubscript:_kImageCount == 1 ? 0 : 1]; 247 | 248 | if (self.titles.count) { // 如果设置标题数组在设置图片数组之前,在这里需要再调一次titles的setter方法 249 | self.titles = self.titles; 250 | } 251 | self.titleLabelBackgroundColor = self.titleLabelBackgroundColor; 252 | self.titleLabelTextColor = self.titleLabelTextColor; 253 | self.titleLabelFont = self.titleLabelFont; 254 | 255 | } else { 256 | [self.scrollView addSubview:self.currentImgView]; 257 | [self setImageView:_currentImgView withSubscript:0]; 258 | } 259 | 260 | [self addSubview:self.pageControl]; 261 | _pageControl.numberOfPages = self.kImageCount; 262 | _pageControl.currentPage = 0; 263 | 264 | self.nextPhotoIndex = 1; 265 | self.lastPhotoIndex = _kImageCount - 1; 266 | 267 | [self layoutIfNeeded]; 268 | } 269 | 270 | // 根据下标设置imgView的image 271 | - (void)setImageView:(SPCycleImageView *)imgView withSubscript:(NSInteger)subcript{ 272 | if (_placeholderImage) { // 先给一张 273 | imgView.image = _placeholderImage; 274 | } 275 | if (self.carouseImagesType == SPCycleScrollViewImagesDataTypeInLocal) { 276 | NSString *localImgString = self.localImages[subcript]; 277 | if ([[localImgString.pathExtension lowercaseString] isEqualToString:@"gif"]) { 278 | imgView.image = gifImageNamed(localImgString); 279 | } else { 280 | imgView.image = [UIImage imageNamed:self.localImages[subcript]]; 281 | } 282 | } else{ 283 | // 网络图片设置 284 | [self sp_setImageWithImageView:imgView URL:self.urlImages[subcript]]; 285 | } 286 | if (self.titles.count) { 287 | imgView.title = self.titles[subcript]; 288 | } 289 | } 290 | 291 | #pragma mark - setter 292 | // 本地图片 293 | - (void)setLocalImages:(NSArray *)localImages { 294 | if (localImages.count == 0) return; 295 | if (![_localImages isEqualToArray:localImages]) { 296 | _localImages = nil; 297 | _localImages = [localImages copy]; 298 | // 标记图片来源 299 | self.carouseImagesType = SPCycleScrollViewImagesDataTypeInLocal; 300 | //获取数组个数 301 | self.kImageCount = _localImages.count; 302 | [self configure]; 303 | 304 | [self openTimer]; 305 | } 306 | } 307 | 308 | // 网络图片 309 | - (void)setUrlImages:(NSArray *)urlImages { 310 | if (urlImages.count == 0) return; 311 | if (![_urlImages isEqualToArray:urlImages]) { 312 | _urlImages = nil; 313 | _urlImages = [urlImages copy]; 314 | // 标记图片来源 315 | self.carouseImagesType = SPCycleScrollViewImagesDataTypeInURL; 316 | self.kImageCount = _urlImages.count; 317 | [self configure]; 318 | 319 | [self openTimer]; 320 | } 321 | } 322 | 323 | - (void)setTitles:(NSArray *)titles { 324 | if (titles.count == 0) { 325 | _lastImgView.labelContentView.hidden = YES; 326 | _currentImgView.labelContentView.hidden = YES; 327 | _nextImgView.labelContentView.hidden = YES; 328 | } else { 329 | 330 | _lastImgView.labelContentView.hidden = NO; 331 | _currentImgView.labelContentView.hidden = NO; 332 | _nextImgView.labelContentView.hidden = NO; 333 | 334 | NSArray *images = [NSArray array]; 335 | if (self.carouseImagesType == SPCycleScrollViewImagesDataTypeInURL) { 336 | images = self.urlImages; 337 | } else { 338 | images = self.localImages; 339 | } 340 | if (titles.count < images.count) { // 如果标题个数小于图片个数,则用空字符串补齐 341 | NSMutableArray *newTitles = [NSMutableArray arrayWithArray:titles]; 342 | for (NSInteger i = titles.count; i < images.count; i++) { 343 | [newTitles addObject:@""]; 344 | } 345 | _titles = newTitles; 346 | } else { // 标题个数大于或等于图片个数,如果是大于了,下标最大只会是图片最大个数-1,所以不用考虑截取到和图片个数一样大 347 | _titles = titles; 348 | } 349 | _lastImgView.title = _titles[(_titles.count-1)]; 350 | _currentImgView.title = _titles[0]; 351 | _nextImgView.title = _titles[_titles.count == 1 ? 0 : 1]; 352 | 353 | self.pageControlPosition = self.pageControlPosition; // 重新设置pageControl的位置 354 | } 355 | } 356 | 357 | - (void)setTitleLabelBackgroundColor:(UIColor *)titleLabelBackgroundColor { 358 | _titleLabelBackgroundColor = titleLabelBackgroundColor; 359 | if (_lastImgView && _currentImgView && _nextImgView) { 360 | _lastImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 361 | _currentImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 362 | _nextImgView.labelContentView.backgroundColor = _titleLabelBackgroundColor; 363 | } 364 | } 365 | 366 | - (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor { 367 | _titleLabelTextColor = titleLabelTextColor; 368 | if (_lastImgView && _currentImgView && _nextImgView) { 369 | _lastImgView.titleLabel.textColor = _titleLabelTextColor; 370 | _currentImgView.titleLabel.textColor = _titleLabelTextColor; 371 | _nextImgView.titleLabel.textColor = _titleLabelTextColor; 372 | } 373 | } 374 | 375 | - (void)setTitleLabelFont:(UIFont *)titleLabelFont { 376 | _titleLabelFont = titleLabelFont; 377 | if (_lastImgView && _currentImgView && _nextImgView) { 378 | _lastImgView.titleLabel.font = _titleLabelFont; 379 | _currentImgView.titleLabel.font = _titleLabelFont; 380 | _nextImgView.titleLabel.font = _titleLabelFont; 381 | } 382 | } 383 | 384 | // 是否自动轮播 385 | - (void)setAutoScroll:(BOOL)autoScroll { 386 | _autoScroll = autoScroll; 387 | 388 | if (autoScroll) { 389 | // 开启新的定时器 390 | [self openTimer]; 391 | } else { 392 | // 关闭定时器 393 | [self closeTimer]; 394 | } 395 | } 396 | 397 | // 重写duration的set方法,用户可以在外界设置轮播图间隔时间 398 | - (void)setDuration:(NSTimeInterval)duration{ 399 | _duration = duration; 400 | if (duration < 1.0f) { // 如果外界不小心设置的时间小于1秒,强制默认2秒。 401 | duration = 2.0f; 402 | } 403 | [self openTimer]; 404 | } 405 | 406 | // 设置pageControl的位置 407 | - (void)setPageControlPosition:(SPPageContolPosition)pageControlPosition { 408 | _pageControlPosition = pageControlPosition; 409 | 410 | if (_pageControl.hidden) return; 411 | 412 | CGSize size; 413 | if (!_pageImageSize.width) {// 没有设置图片,系统原有样式 414 | size = [_pageControl sizeForNumberOfPages:_pageControl.numberOfPages]; 415 | } else { // 设置图片了 416 | size = CGSizeMake(_pageImageSize.width * (_pageControl.numberOfPages * 2 - 1), _pageImageSize.height); 417 | } 418 | 419 | _pageControl.frame = CGRectMake(0, 0, size.width, size.height); 420 | 421 | CGFloat pointY = kHeight - size.height - kPageControlMargin; 422 | 423 | switch (pageControlPosition) { 424 | case SPPageContolPositionBottomCenter: 425 | // 底部中间 426 | if (self.titles.count) { 427 | _pageControl.frame = CGRectMake((kWidth-size.width)*0.5, pointY-30, size.width, size.height); 428 | } else { 429 | _pageControl.frame = CGRectMake((kWidth-size.width)*0.5, pointY, size.width, size.height); 430 | } 431 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 432 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 433 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionCenter remainderSpacing:0]; 434 | 435 | break; 436 | case SPPageContolPositionBottomRight: 437 | // 底部右边 438 | _pageControl.frame = CGRectMake(kWidth - size.width - kPageControlMargin, pointY, size.width, size.height); 439 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 440 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 441 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionLeft remainderSpacing:size.width]; 442 | break; 443 | case SPPageContolPositionBottomLeft: 444 | // 底部左边 445 | _pageControl.frame = CGRectMake(kPageControlMargin, pointY, size.width, size.height); 446 | [_lastImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 447 | [_currentImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 448 | [_nextImgView setLabelPosition:SPCycleImageViewLabelPositionRight remainderSpacing:size.width]; 449 | break; 450 | default: 451 | break; 452 | } 453 | } 454 | 455 | // 设置imageView的内容模式 456 | - (void)setImageMode:(UIViewContentMode)imageMode { 457 | _imageMode = imageMode; 458 | self.nextImgView.contentMode = self.currentImgView.contentMode = self.lastImgView.contentMode = imageMode; 459 | } 460 | 461 | #pragma mark - scrollView代理方法 462 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 463 | if (CGSizeEqualToSize(CGSizeZero, scrollView.contentSize)) return; 464 | 465 | CGFloat offsetX = scrollView.contentOffset.x; 466 | 467 | // 到第一张图片时 (一上来,当前图片的x值是kWidth) 468 | if (ceil(offsetX) <= 0) { // 右滑 469 | 470 | _nextImgView.image = _currentImgView.image; 471 | _currentImgView.image = _lastImgView.image; 472 | _nextImgView.title = _currentImgView.title; 473 | _currentImgView.title = _lastImgView.title; 474 | // 将轮播图的偏移量设回中间位置 475 | scrollView.contentOffset = CGPointMake(kWidth, 0); 476 | _lastImgView.image = nil; 477 | // 一定要小于等于,否则数组中只有一张图片时会出错 478 | if (_lastPhotoIndex <= 0) { 479 | _lastPhotoIndex = _kImageCount - 1; 480 | _nextPhotoIndex = _lastPhotoIndex - (_kImageCount - 2); 481 | } else { 482 | _lastPhotoIndex--; 483 | if (_nextPhotoIndex == 0) { 484 | _nextPhotoIndex = _kImageCount - 1; 485 | } else { 486 | _nextPhotoIndex--; 487 | } 488 | } 489 | [self setImageView:_lastImgView withSubscript:_lastPhotoIndex]; 490 | } 491 | // 到最后一张图片时(最后一张就是轮播图的第三张) 492 | if (ceil(offsetX) >= kWidth*2) { // 左滑 493 | _lastImgView.image = _currentImgView.image; 494 | _currentImgView.image = _nextImgView.image; 495 | _lastImgView.title = _currentImgView.title; 496 | _currentImgView.title = _nextImgView.title; 497 | scrollView.contentOffset = CGPointMake(kWidth, 0); 498 | _nextImgView.image = nil; 499 | // 一定要是大于等于,否则数组中只有一张图片时会出错 500 | if (_nextPhotoIndex >= _kImageCount - 1 ) { 501 | _nextPhotoIndex = 0; 502 | _lastPhotoIndex = _nextPhotoIndex + (_kImageCount - 2); 503 | } else{ 504 | _nextPhotoIndex++; 505 | if (_lastPhotoIndex == _kImageCount - 1) { 506 | _lastPhotoIndex = 0; 507 | } else { 508 | _lastPhotoIndex++; 509 | } 510 | } 511 | [self setImageView:_nextImgView withSubscript:_nextPhotoIndex]; 512 | } 513 | 514 | if (_nextPhotoIndex - 1 < 0) { 515 | self.pageControl.currentPage = _kImageCount - 1; 516 | } else { 517 | self.pageControl.currentPage = _nextPhotoIndex - 1; 518 | } 519 | 520 | } 521 | 522 | // 用户将要拖拽时将定时器关闭 523 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ 524 | // 关闭定时器 525 | [self closeTimer]; 526 | } 527 | 528 | // 用户结束拖拽时将定时器开启(在打开自动轮播的前提下) 529 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { 530 | if (self.autoScroll) { 531 | [self openTimer]; 532 | } 533 | } 534 | 535 | #pragma mark 下载网络图片 536 | - (void)sp_setImageWithImageView:(UIImageView *)imageView URL:(NSString *)urlString { 537 | NSString *subURLString = [urlString substringFromIndex:30]; 538 | NSString *imageName = [[subURLString stringByReplacingOccurrencesOfString:@"/" withString:@""] stringByReplacingOccurrencesOfString:@"_" withString:@""]; 539 | NSString *path = [cache stringByAppendingPathComponent:imageName]; 540 | if (_autoCache) { //如果开启了缓存功能,先从沙盒中取图片 541 | NSData *data = [NSData dataWithContentsOfFile:path]; 542 | if (data) { 543 | imageView.image = getImageWithData(data); 544 | return; 545 | } 546 | } 547 | //下载图片 548 | NSBlockOperation *download = [NSBlockOperation blockOperationWithBlock:^{ 549 | NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]]; 550 | if (!data) return; 551 | UIImage *image = getImageWithData(data); 552 | //取到的data有可能不是图片 553 | if (image) { 554 | dispatch_async(dispatch_get_main_queue(), ^{ 555 | imageView.image = image; 556 | }); 557 | if (self.autoCache) [data writeToFile:path atomically:YES]; 558 | } 559 | }]; 560 | [self.queue addOperation:download]; 561 | } 562 | 563 | #pragma mark 下载图片,如果是gif则计算动画时长 564 | UIImage *getImageWithData(NSData *data) { 565 | CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); 566 | size_t count = CGImageSourceGetCount(imageSource); 567 | if (count <= 1) { //非gif 568 | CFRelease(imageSource); 569 | return [[UIImage alloc] initWithData:data]; 570 | } else { //gif图片 571 | NSMutableArray *images = [NSMutableArray array]; 572 | NSTimeInterval duration = 0; 573 | for (size_t i = 0; i < count; i++) { 574 | CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); 575 | if (!image) continue; 576 | duration += durationWithSourceAtIndex(imageSource, i); 577 | [images addObject:[UIImage imageWithCGImage:image]]; 578 | CGImageRelease(image); 579 | } 580 | if (!duration) duration = 0.1 * count; 581 | CFRelease(imageSource); 582 | return [UIImage animatedImageWithImages:images duration:duration]; 583 | } 584 | } 585 | 586 | 587 | #pragma mark 获取每一帧图片的时长 588 | float durationWithSourceAtIndex(CGImageSourceRef source, NSUInteger index) { 589 | float duration = 0.1f; 590 | CFDictionaryRef propertiesRef = CGImageSourceCopyPropertiesAtIndex(source, index, nil); 591 | NSDictionary *properties = (__bridge NSDictionary *)propertiesRef; 592 | NSDictionary *gifProperties = properties[(NSString *)kCGImagePropertyGIFDictionary]; 593 | 594 | NSNumber *delayTime = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; 595 | if (delayTime) duration = delayTime.floatValue; 596 | else { 597 | delayTime = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; 598 | if (delayTime) duration = delayTime.floatValue; 599 | } 600 | CFRelease(propertiesRef); 601 | return duration; 602 | } 603 | 604 | UIImage *gifImageNamed(NSString *imageName) { 605 | 606 | if (![imageName hasSuffix:@".gif"]) { 607 | imageName = [imageName stringByAppendingString:@".gif"]; 608 | } 609 | 610 | NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:nil]; 611 | NSData *data = [NSData dataWithContentsOfFile:imagePath]; 612 | if (data) return getImageWithData(data); 613 | 614 | return [UIImage imageNamed:imageName]; 615 | } 616 | 617 | #pragma mark 清除沙盒中的图片缓存 618 | + (void)clearDiskCache { 619 | NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:cache error:NULL]; 620 | for (NSString *fileName in contents) { 621 | [[NSFileManager defaultManager] removeItemAtPath:[cache stringByAppendingPathComponent:fileName] error:nil]; 622 | } 623 | } 624 | 625 | #pragma mark - 手势点击事件 626 | -(void)handleTapActionInImageView:(UITapGestureRecognizer *)tap { 627 | if (self.clickedImageBlock) { 628 | // 如果_nextPhotoIndex == 0,那么中间那张图片一定是数组中最后一张,我们要传的就是中间那张图片在数组中的下标 629 | if (_nextPhotoIndex == 0) { 630 | self.clickedImageBlock(_kImageCount-1); 631 | }else{ 632 | self.clickedImageBlock(_nextPhotoIndex-1); 633 | } 634 | } else if (_delegate && [_delegate respondsToSelector:@selector(cycleScrollView:clickedImageAtIndex:)]) { 635 | // 如果_nextPhotoIndex == 0,那么中间那张图片一定是数组中最后一张,我们要传的就是中间那张图片在数组中的下标 636 | if (_nextPhotoIndex == 0) { 637 | [_delegate cycleScrollView:self clickedImageAtIndex:_kImageCount-1]; 638 | }else{ 639 | [_delegate cycleScrollView:self clickedImageAtIndex:_nextPhotoIndex-1]; 640 | } 641 | } 642 | } 643 | 644 | - (void)layoutSubviews { 645 | [super layoutSubviews]; 646 | 647 | self.scrollView.frame = self.bounds; 648 | //有导航控制器时,会默认在scrollview上方添加64的内边距,这里强制设置为0 649 | self.scrollView.contentInset = UIEdgeInsetsZero; 650 | 651 | if (self.kImageCount > 1) { 652 | // 重新设置contentOffset和contentSize对于轮播图下拉放大以及里面的图片跟随放大起着关键作用,因为scrollView放大了,如果不手动设置contentOffset和contentSize,则会导致scrollView的容量不够大,从而导致图片越出scrollview边界的问题 653 | self.scrollView.contentSize = CGSizeMake(kWidth * 3, 0); 654 | // 这里如果采用动画效果设置偏移量将不起任何作用 655 | self.scrollView.contentOffset = CGPointMake(kWidth, 0); 656 | 657 | self.lastImgView.frame = CGRectMake(0, 0, kWidth, kHeight); 658 | self.currentImgView.frame = CGRectMake(kWidth, 0, kWidth, kHeight); 659 | self.nextImgView.frame = CGRectMake(kWidth * 2, 0, kWidth, kHeight); 660 | 661 | } else { 662 | self.scrollView.contentSize = CGSizeZero; 663 | self.scrollView.contentOffset = CGPointMake(0, 0); 664 | self.currentImgView.frame = CGRectMake(0, 0, kWidth, kHeight); 665 | } 666 | 667 | // 等号左边是调setter方法,右边调用getter方法 668 | self.pageControlPosition = self.pageControlPosition; 669 | } 670 | 671 | #pragma mark - 懒加载 672 | -(UIScrollView *)scrollView{ 673 | if (_scrollView == nil) { 674 | _scrollView = [[UIScrollView alloc] init]; 675 | _scrollView.pagingEnabled = YES; 676 | _scrollView.bounces = NO; 677 | _scrollView.showsHorizontalScrollIndicator = NO; 678 | _scrollView.showsVerticalScrollIndicator = NO; 679 | _scrollView.delegate = self; 680 | _scrollView.clipsToBounds = YES; 681 | _scrollView.layer.masksToBounds = YES; 682 | } 683 | return _scrollView; 684 | } 685 | 686 | - (SPPageControl *)pageControl{ 687 | if (_pageControl == nil) { 688 | _pageControl = [[SPPageControl alloc] init]; 689 | _pageControl.userInteractionEnabled = NO; 690 | _pageControl.hidesForSinglePage = YES; 691 | _pageControl.pageIndicatorTintColor = [UIColor grayColor]; 692 | _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; 693 | _pageControl.currentPage = 0; 694 | } 695 | return _pageControl; 696 | } 697 | 698 | - (SPCycleImageView *)lastImgView{ 699 | if (_lastImgView == nil) { 700 | _lastImgView = [[SPCycleImageView alloc] init]; 701 | _lastImgView.layer.masksToBounds = YES; 702 | } 703 | return _lastImgView; 704 | } 705 | 706 | - (SPCycleImageView *)currentImgView{ 707 | if (_currentImgView == nil) { 708 | _currentImgView = [[SPCycleImageView alloc] init]; 709 | _currentImgView.layer.masksToBounds = YES; 710 | // 给当前图片添加手势 711 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleTapActionInImageView:)]; 712 | [_currentImgView addGestureRecognizer:tap]; 713 | _currentImgView.userInteractionEnabled = YES; 714 | } 715 | return _currentImgView; 716 | } 717 | 718 | - (SPCycleImageView *)nextImgView{ 719 | if (_nextImgView == nil) { 720 | _nextImgView = [[SPCycleImageView alloc] init]; 721 | _nextImgView.layer.masksToBounds = YES; 722 | } 723 | return _nextImgView; 724 | } 725 | 726 | - (NSOperationQueue *)queue { 727 | if (!_queue) { 728 | _queue = [[NSOperationQueue alloc] init]; 729 | } 730 | return _queue; 731 | } 732 | 733 | #pragma mark - 系统方法 734 | -(void)willMoveToSuperview:(UIView *)newSuperview { 735 | if (!newSuperview) { 736 | [self closeTimer]; 737 | } 738 | } 739 | 740 | -(void)dealloc { 741 | _scrollView.delegate = nil; 742 | } 743 | 744 | @end 745 | 746 | -------------------------------------------------------------------------------- /SPCycleScrollView/SPPageControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPPageControl.h 3 | // SPPageControl 4 | // 5 | // Created by Jerry Jones on 10/13/12. 6 | // Copyright (c) 2012 Spaceman Labs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSUInteger, SPPageControlAlignment) { 12 | SPPageControlAlignmentLeft = 1, 13 | SPPageControlAlignmentCenter, 14 | SPPageControlAlignmentRight 15 | }; 16 | 17 | typedef NS_ENUM(NSUInteger, SPPageControlVerticalAlignment) { 18 | SPPageControlVerticalAlignmentTop = 1, 19 | SPPageControlVerticalAlignmentMiddle, 20 | SPPageControlVerticalAlignmentBottom 21 | }; 22 | 23 | typedef NS_ENUM(NSUInteger, SPPageControlTapBehavior) { 24 | SPPageControlTapBehaviorStep = 1, 25 | SPPageControlTapBehaviorJump 26 | }; 27 | 28 | @interface SPPageControl : UIControl 29 | 30 | @property (nonatomic) NSInteger numberOfPages; 31 | @property (nonatomic) NSInteger currentPage; 32 | @property (nonatomic) CGFloat indicatorMargin UI_APPEARANCE_SELECTOR; // deafult is 10 33 | @property (nonatomic) CGFloat indicatorDiameter UI_APPEARANCE_SELECTOR; // deafult is 6 34 | @property (nonatomic) CGFloat minHeight UI_APPEARANCE_SELECTOR; // default is 36, cannot be less than indicatorDiameter 35 | @property (nonatomic) SPPageControlAlignment alignment UI_APPEARANCE_SELECTOR; // deafult is Center 36 | @property (nonatomic) SPPageControlVerticalAlignment verticalAlignment UI_APPEARANCE_SELECTOR; // deafult is Middle 37 | 38 | @property (nonatomic, strong) UIImage *pageIndicatorImage UI_APPEARANCE_SELECTOR; 39 | @property (nonatomic, strong) UIImage *pageIndicatorMaskImage UI_APPEARANCE_SELECTOR; // ignored if pageIndicatorImage is set 40 | @property (nonatomic, strong) UIColor *pageIndicatorTintColor UI_APPEARANCE_SELECTOR; // ignored if pageIndicatorImage is set 41 | @property (nonatomic, strong) UIImage *currentPageIndicatorImage UI_APPEARANCE_SELECTOR; 42 | @property (nonatomic, strong) UIColor *currentPageIndicatorTintColor UI_APPEARANCE_SELECTOR; // ignored if currentPageIndicatorImage is set 43 | 44 | @property (nonatomic) BOOL hidesForSinglePage; // hide the the indicator if there is only one page. default is NO 45 | @property (nonatomic) BOOL defersCurrentPageDisplay; // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO 46 | 47 | @property (nonatomic) SPPageControlTapBehavior tapBehavior; // SPPageControlTapBehaviorStep provides an increment/decrement behavior exactly like UIPageControl. SPPageControlTapBehaviorJump allows specific pages to be selected by tapping their respective indicator. Default is SPPageControlTapBehaviorStep 48 | 49 | - (void)updateCurrentPageDisplay; // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately 50 | 51 | - (CGRect)rectForPageIndicator:(NSInteger)pageIndex; 52 | - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount; 53 | 54 | - (void)setImage:(UIImage *)image forPage:(NSInteger)pageIndex; 55 | - (void)setCurrentImage:(UIImage *)image forPage:(NSInteger)pageIndex; 56 | - (void)setImageMask:(UIImage *)image forPage:(NSInteger)pageIndex; 57 | 58 | - (UIImage *)imageForPage:(NSInteger)pageIndex; 59 | - (UIImage *)currentImageForPage:(NSInteger)pageIndex; 60 | - (UIImage *)imageMaskForPage:(NSInteger)pageIndex; 61 | 62 | - (void)updatePageNumberForScrollView:(UIScrollView *)scrollView; 63 | - (void)setScrollViewContentOffsetForCurrentPage:(UIScrollView *)scrollView animated:(BOOL)animated; 64 | 65 | #pragma mark - UIAccessibility 66 | 67 | // SPPageControl mirrors UIPageControl's standard accessibility functionality by default. 68 | // Basically, the accessibility label is set to "[current page index + 1] of [page count]". 69 | 70 | // SPPageControl extends UIPageControl's functionality by allowing you to name specific pages. This is especially useful when using 71 | // the per-page indicator images, and allows you to provide more context to the user. 72 | 73 | - (void)setName:(NSString *)name forPage:(NSInteger)pageIndex; 74 | - (NSString *)nameForPage:(NSInteger)pageIndex; 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /SPCycleScrollView/SPageControl.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPPageControl.m 3 | // SPPageControl 4 | // 5 | // Created by Jerry Jones on 10/13/12. 6 | // Copyright (c) 2012 Spaceman Labs. All rights reserved. 7 | // 8 | 9 | #import "SPPageControl.h" 10 | 11 | #if ! __has_feature(objc_arc) 12 | #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). 13 | #endif 14 | 15 | 16 | #define DEFAULT_INDICATOR_WIDTH 6.0f 17 | #define DEFAULT_INDICATOR_MARGIN 10.0f 18 | #define DEFAULT_MIN_HEIGHT 36.0f 19 | 20 | #define DEFAULT_INDICATOR_WIDTH_LARGE 7.0f 21 | #define DEFAULT_INDICATOR_MARGIN_LARGE 9.0f 22 | #define DEFAULT_MIN_HEIGHT_LARGE 36.0f 23 | 24 | typedef NS_ENUM(NSUInteger, SPPageControlImageType) { 25 | SPPageControlImageTypeNormal = 1, 26 | SPPageControlImageTypeCurrent, 27 | SPPageControlImageTypeMask 28 | }; 29 | 30 | typedef NS_ENUM(NSUInteger, SPPageControlStyleDefaults) { 31 | SPPageControlDefaultStyleClassic = 0, 32 | SPPageControlDefaultStyleModern 33 | }; 34 | 35 | static SPPageControlStyleDefaults _defaultStyleForSystemVersion; 36 | 37 | @interface SPPageControl () 38 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageNames; 39 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageImages; 40 | @property (strong, readonly, nonatomic) NSMutableDictionary *currentPageImages; 41 | @property (strong, readonly, nonatomic) NSMutableDictionary *pageImageMasks; 42 | @property (strong, readonly, nonatomic) NSMutableDictionary *cgImageMasks; 43 | @property (strong, readwrite, nonatomic) NSArray *pageRects; 44 | 45 | // Page Control used for stealing page number localizations for accessibility labels 46 | // I'm not sure I love this technique, but it's the best way to get exact translations for all the languages 47 | // that Apple supports out of the box 48 | @property (nonatomic, strong) UIPageControl *accessibilityPageControl; 49 | @end 50 | 51 | @implementation SPPageControl 52 | { 53 | @private 54 | NSInteger _displayedPage; 55 | CGFloat _measuredIndicatorWidth; 56 | CGFloat _measuredIndicatorHeight; 57 | CGImageRef _pageImageMask; 58 | } 59 | 60 | @synthesize pageNames = _pageNames; 61 | @synthesize pageImages = _pageImages; 62 | @synthesize currentPageImages = _currentPageImages; 63 | @synthesize pageImageMasks = _pageImageMasks; 64 | @synthesize cgImageMasks = _cgImageMasks; 65 | 66 | + (void)initialize 67 | { 68 | NSString *reqSysVer = @"7.0"; 69 | NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; 70 | if ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending) { 71 | _defaultStyleForSystemVersion = SPPageControlDefaultStyleModern; 72 | } else { 73 | _defaultStyleForSystemVersion = SPPageControlDefaultStyleClassic; 74 | } 75 | } 76 | 77 | - (void)_initialize 78 | { 79 | _numberOfPages = 0; 80 | _tapBehavior = SPPageControlTapBehaviorStep; 81 | 82 | self.backgroundColor = [UIColor clearColor]; 83 | 84 | // If the app wasn't linked against iOS 7 or newer, always use the classic style 85 | // otherwise, use the style of the current OS. 86 | #ifdef __IPHONE_7_0 87 | [self setStyleWithDefaults:_defaultStyleForSystemVersion]; 88 | #else 89 | [self setStyleWithDefaults:SPPageControlDefaultStyleClassic]; 90 | #endif 91 | 92 | _alignment = SPPageControlAlignmentCenter; 93 | _verticalAlignment = SPPageControlVerticalAlignmentMiddle; 94 | 95 | self.isAccessibilityElement = YES; 96 | self.accessibilityTraits = UIAccessibilityTraitUpdatesFrequently; 97 | self.accessibilityPageControl = [[UIPageControl alloc] init]; 98 | self.contentMode = UIViewContentModeRedraw; 99 | } 100 | 101 | - (id)initWithFrame:(CGRect)frame 102 | { 103 | self = [super initWithFrame:frame]; 104 | if (nil == self) { 105 | return nil; 106 | } 107 | 108 | [self _initialize]; 109 | return self; 110 | } 111 | 112 | - (id)initWithCoder:(NSCoder *)aDecoder 113 | { 114 | self = [super initWithCoder:aDecoder]; 115 | if (nil == self) { 116 | return nil; 117 | } 118 | 119 | [self _initialize]; 120 | return self; 121 | } 122 | 123 | - (void)dealloc 124 | { 125 | if (_pageImageMask) { 126 | CGImageRelease(_pageImageMask); 127 | } 128 | } 129 | 130 | - (void)drawRect:(CGRect)rect 131 | { 132 | CGContextRef context = UIGraphicsGetCurrentContext(); 133 | [self _renderPages:context rect:rect]; 134 | } 135 | 136 | - (void)_renderPages:(CGContextRef)context rect:(CGRect)rect 137 | { 138 | NSMutableArray *pageRects = [NSMutableArray arrayWithCapacity:self.numberOfPages]; 139 | 140 | if (_numberOfPages < 2 && _hidesForSinglePage) { 141 | return; 142 | } 143 | 144 | CGFloat left = [self _leftOffset]; 145 | 146 | CGFloat xOffset = left; 147 | CGFloat yOffset = 0.0f; 148 | UIColor *fillColor = nil; 149 | UIImage *image = nil; 150 | CGImageRef maskingImage = nil; 151 | CGSize maskSize = CGSizeZero; 152 | 153 | for (NSInteger i = 0; i < _numberOfPages; i++) { 154 | NSNumber *indexNumber = @(i); 155 | 156 | if (i == _displayedPage) { 157 | fillColor = _currentPageIndicatorTintColor ? _currentPageIndicatorTintColor : [UIColor whiteColor]; 158 | image = _currentPageImages[indexNumber]; 159 | if (nil == image) { 160 | image = _currentPageIndicatorImage; 161 | } 162 | } else { 163 | fillColor = _pageIndicatorTintColor ? _pageIndicatorTintColor : [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; 164 | image = _pageImages[indexNumber]; 165 | if (nil == image) { 166 | image = _pageIndicatorImage; 167 | } 168 | } 169 | 170 | // If no finished images have been set, try a masking image 171 | if (nil == image) { 172 | maskingImage = (__bridge CGImageRef)_cgImageMasks[indexNumber]; 173 | UIImage *originalImage = _pageImageMasks[indexNumber]; 174 | maskSize = originalImage.size; 175 | 176 | // If no per page mask is set, try for a global page mask! 177 | if (nil == maskingImage) { 178 | maskingImage = _pageImageMask; 179 | maskSize = _pageIndicatorMaskImage.size; 180 | } 181 | } 182 | 183 | [fillColor set]; 184 | CGRect indicatorRect; 185 | if (image) { 186 | yOffset = [self _topOffsetForHeight:image.size.height rect:rect]; 187 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - image.size.width) / 2.0f); 188 | [image drawAtPoint:CGPointMake(centeredXOffset, yOffset)]; 189 | indicatorRect = CGRectMake(centeredXOffset, yOffset, image.size.width, image.size.height); 190 | } else if (maskingImage) { 191 | yOffset = [self _topOffsetForHeight:maskSize.height rect:rect]; 192 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - maskSize.width) / 2.0f); 193 | indicatorRect = CGRectMake(centeredXOffset, yOffset, maskSize.width, maskSize.height); 194 | CGContextDrawImage(context, indicatorRect, maskingImage); 195 | } else { 196 | yOffset = [self _topOffsetForHeight:_indicatorDiameter rect:rect]; 197 | CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - _indicatorDiameter) / 2.0f); 198 | indicatorRect = CGRectMake(centeredXOffset, yOffset, _indicatorDiameter, _indicatorDiameter); 199 | CGContextFillEllipseInRect(context, indicatorRect); 200 | } 201 | 202 | [pageRects addObject:[NSValue valueWithCGRect:indicatorRect]]; 203 | maskingImage = NULL; 204 | xOffset += _measuredIndicatorWidth + _indicatorMargin; 205 | } 206 | 207 | self.pageRects = pageRects; 208 | 209 | } 210 | 211 | - (CGFloat)_leftOffset 212 | { 213 | CGRect rect = self.bounds; 214 | CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; 215 | CGFloat left = 0.0f; 216 | switch (_alignment) { 217 | case SPPageControlAlignmentCenter: 218 | left = ceilf(CGRectGetMidX(rect) - (size.width / 2.0f)); 219 | break; 220 | case SPPageControlAlignmentRight: 221 | left = CGRectGetMaxX(rect) - size.width; 222 | break; 223 | default: 224 | break; 225 | } 226 | 227 | return left; 228 | } 229 | 230 | - (CGFloat)_topOffsetForHeight:(CGFloat)height rect:(CGRect)rect 231 | { 232 | CGFloat top = 0.0f; 233 | switch (_verticalAlignment) { 234 | case SPPageControlVerticalAlignmentMiddle: 235 | top = CGRectGetMidY(rect) - (height / 2.0f); 236 | break; 237 | case SPPageControlVerticalAlignmentBottom: 238 | top = CGRectGetMaxY(rect) - height; 239 | break; 240 | default: 241 | break; 242 | } 243 | 244 | return top; 245 | } 246 | 247 | - (void)updateCurrentPageDisplay 248 | { 249 | _displayedPage = _currentPage; 250 | [self setNeedsDisplay]; 251 | } 252 | 253 | - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount 254 | { 255 | CGFloat marginSpace = MAX(0, pageCount - 1) * _indicatorMargin; 256 | CGFloat indicatorSpace = pageCount * _measuredIndicatorWidth; 257 | CGSize size = CGSizeMake(marginSpace + indicatorSpace, _measuredIndicatorHeight); 258 | return size; 259 | } 260 | 261 | - (CGRect)rectForPageIndicator:(NSInteger)pageIndex 262 | { 263 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 264 | return CGRectZero; 265 | } 266 | 267 | CGFloat left = [self _leftOffset]; 268 | CGSize size = [self sizeForNumberOfPages:pageIndex + 1]; 269 | CGRect rect = CGRectMake(left + size.width - _measuredIndicatorWidth, 0, _measuredIndicatorWidth, _measuredIndicatorWidth); 270 | return rect; 271 | } 272 | 273 | - (void)_setImage:(UIImage *)image forPage:(NSInteger)pageIndex type:(SPPageControlImageType)type 274 | { 275 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 276 | return; 277 | } 278 | 279 | NSMutableDictionary *dictionary = nil; 280 | switch (type) { 281 | case SPPageControlImageTypeCurrent: 282 | dictionary = self.currentPageImages; 283 | break; 284 | case SPPageControlImageTypeNormal: 285 | dictionary = self.pageImages; 286 | break; 287 | case SPPageControlImageTypeMask: 288 | dictionary = self.pageImageMasks; 289 | break; 290 | default: 291 | break; 292 | } 293 | 294 | if (image) { 295 | dictionary[@(pageIndex)] = image; 296 | } else { 297 | [dictionary removeObjectForKey:@(pageIndex)]; 298 | } 299 | } 300 | 301 | - (void)setImage:(UIImage *)image forPage:(NSInteger)pageIndex 302 | { 303 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeNormal]; 304 | [self _updateMeasuredIndicatorSizes]; 305 | } 306 | 307 | - (void)setCurrentImage:(UIImage *)image forPage:(NSInteger)pageIndex 308 | { 309 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeCurrent];; 310 | [self _updateMeasuredIndicatorSizes]; 311 | } 312 | 313 | - (void)setImageMask:(UIImage *)image forPage:(NSInteger)pageIndex 314 | { 315 | [self _setImage:image forPage:pageIndex type:SPPageControlImageTypeMask]; 316 | 317 | if (nil == image) { 318 | [self.cgImageMasks removeObjectForKey:@(pageIndex)]; 319 | return; 320 | } 321 | 322 | CGImageRef maskImage = [self createMaskForImage:image]; 323 | 324 | if (maskImage) { 325 | self.cgImageMasks[@(pageIndex)] = (__bridge id)maskImage; 326 | CGImageRelease(maskImage); 327 | [self _updateMeasuredIndicatorSizeWithSize:image.size]; 328 | [self setNeedsDisplay]; 329 | } 330 | } 331 | 332 | - (id)_imageForPage:(NSInteger)pageIndex type:(SPPageControlImageType)type 333 | { 334 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 335 | return nil; 336 | } 337 | 338 | NSDictionary *dictionary = nil; 339 | switch (type) { 340 | case SPPageControlImageTypeCurrent: 341 | dictionary = _currentPageImages; 342 | break; 343 | case SPPageControlImageTypeNormal: 344 | dictionary = _pageImages; 345 | break; 346 | case SPPageControlImageTypeMask: 347 | dictionary = _pageImageMasks; 348 | break; 349 | default: 350 | break; 351 | } 352 | 353 | return dictionary[@(pageIndex)]; 354 | } 355 | 356 | - (UIImage *)imageForPage:(NSInteger)pageIndex 357 | { 358 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeNormal]; 359 | } 360 | 361 | - (UIImage *)currentImageForPage:(NSInteger)pageIndex 362 | { 363 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeCurrent]; 364 | } 365 | 366 | - (UIImage *)imageMaskForPage:(NSInteger)pageIndex 367 | { 368 | return [self _imageForPage:pageIndex type:SPPageControlImageTypeMask]; 369 | } 370 | 371 | - (CGSize)sizeThatFits:(CGSize)size 372 | { 373 | CGSize sizeThatFits = [self sizeForNumberOfPages:self.numberOfPages]; 374 | sizeThatFits.height = MAX(sizeThatFits.height, _minHeight); 375 | return sizeThatFits; 376 | } 377 | 378 | - (CGSize)intrinsicContentSize 379 | { 380 | if (_numberOfPages < 1 || (_numberOfPages < 2 && _hidesForSinglePage)) { 381 | return CGSizeMake(UIViewNoIntrinsicMetric, 0.0f); 382 | } 383 | CGSize intrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, MAX(_measuredIndicatorHeight, _minHeight)); 384 | return intrinsicContentSize; 385 | } 386 | 387 | - (void)updatePageNumberForScrollView:(UIScrollView *)scrollView 388 | { 389 | NSInteger page = (int)floorf(scrollView.contentOffset.x / scrollView.bounds.size.width); 390 | self.currentPage = page; 391 | } 392 | 393 | - (void)setScrollViewContentOffsetForCurrentPage:(UIScrollView *)scrollView animated:(BOOL)animated 394 | { 395 | CGPoint offset = scrollView.contentOffset; 396 | offset.x = scrollView.bounds.size.width * self.currentPage; 397 | [scrollView setContentOffset:offset animated:animated]; 398 | } 399 | 400 | - (void)setStyleWithDefaults:(SPPageControlStyleDefaults)defaultStyle 401 | { 402 | switch (defaultStyle) { 403 | case SPPageControlDefaultStyleModern: 404 | self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH_LARGE; 405 | self.indicatorMargin = DEFAULT_INDICATOR_MARGIN_LARGE; 406 | self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2f]; 407 | self.minHeight = DEFAULT_MIN_HEIGHT_LARGE; 408 | break; 409 | case SPPageControlDefaultStyleClassic: 410 | default: 411 | self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH; 412 | self.indicatorMargin = DEFAULT_INDICATOR_MARGIN; 413 | self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; 414 | self.minHeight = DEFAULT_MIN_HEIGHT; 415 | break; 416 | } 417 | } 418 | 419 | #pragma mark - 420 | 421 | - (CGImageRef)createMaskForImage:(UIImage *)image CF_RETURNS_RETAINED 422 | { 423 | size_t pixelsWide = image.size.width * image.scale; 424 | size_t pixelsHigh = image.size.height * image.scale; 425 | size_t bitmapBytesPerRow = (pixelsWide * 1); 426 | CGContextRef context = CGBitmapContextCreate(NULL, pixelsWide, pixelsHigh, CGImageGetBitsPerComponent(image.CGImage), bitmapBytesPerRow, NULL, (CGBitmapInfo)kCGImageAlphaOnly); 427 | CGContextTranslateCTM(context, 0.f, pixelsHigh); 428 | CGContextScaleCTM(context, 1.0f, -1.0f); 429 | 430 | CGContextDrawImage(context, CGRectMake(0, 0, pixelsWide, pixelsHigh), image.CGImage); 431 | CGImageRef maskImage = CGBitmapContextCreateImage(context); 432 | CGContextRelease(context); 433 | 434 | return maskImage; 435 | } 436 | 437 | - (void)_updateMeasuredIndicatorSizeWithSize:(CGSize)size 438 | { 439 | _measuredIndicatorWidth = MAX(_measuredIndicatorWidth, size.width); 440 | _measuredIndicatorHeight = MAX(_measuredIndicatorHeight, size.height); 441 | } 442 | 443 | - (void)_updateMeasuredIndicatorSizes 444 | { 445 | _measuredIndicatorWidth = _indicatorDiameter; 446 | _measuredIndicatorHeight = _indicatorDiameter; 447 | 448 | // If we're only using images, ignore the _indicatorDiameter 449 | if ( (self.pageIndicatorImage || self.pageIndicatorMaskImage) && self.currentPageIndicatorImage ) 450 | { 451 | _measuredIndicatorWidth = 0; 452 | _measuredIndicatorHeight = 0; 453 | } 454 | 455 | if (self.pageIndicatorImage) { 456 | [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorImage.size]; 457 | } 458 | 459 | if (self.currentPageIndicatorImage) { 460 | [self _updateMeasuredIndicatorSizeWithSize:self.currentPageIndicatorImage.size]; 461 | } 462 | 463 | if (self.pageIndicatorMaskImage) { 464 | [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorMaskImage.size]; 465 | } 466 | 467 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 468 | [self invalidateIntrinsicContentSize]; 469 | } 470 | } 471 | 472 | 473 | #pragma mark - Tap Gesture 474 | 475 | // We're using touchesEnded: because we want to mimick UIPageControl as close as possible 476 | // As of iOS 6, UIPageControl still (as far as we know) does not use a tap gesture recognizer. This means that actions like 477 | // touching down, sliding around, and releasing, still results in the page incrementing or decrementing. 478 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 479 | { 480 | UITouch *touch = [touches anyObject]; 481 | CGPoint point = [touch locationInView:self]; 482 | 483 | if (SPPageControlTapBehaviorJump == self.tapBehavior) { 484 | 485 | __block NSInteger tappedIndicatorIndex = NSNotFound; 486 | 487 | [self.pageRects enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger index, BOOL *stop) { 488 | CGRect indicatorRect = [value CGRectValue]; 489 | 490 | if (CGRectContainsPoint(indicatorRect, point)) { 491 | tappedIndicatorIndex = index; 492 | *stop = YES; 493 | } 494 | }]; 495 | 496 | if (NSNotFound != tappedIndicatorIndex) { 497 | [self setCurrentPage:tappedIndicatorIndex sendEvent:YES canDefer:YES]; 498 | return; 499 | } 500 | } 501 | 502 | CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; 503 | CGFloat left = [self _leftOffset]; 504 | CGFloat middle = left + (size.width / 2.0f); 505 | if (point.x < middle) { 506 | [self setCurrentPage:self.currentPage - 1 sendEvent:YES canDefer:YES]; 507 | } else { 508 | [self setCurrentPage:self.currentPage + 1 sendEvent:YES canDefer:YES]; 509 | } 510 | 511 | } 512 | 513 | #pragma mark - Accessors 514 | 515 | - (void)setFrame:(CGRect)frame 516 | { 517 | [super setFrame:frame]; 518 | [self setNeedsDisplay]; 519 | } 520 | 521 | - (void)setIndicatorDiameter:(CGFloat)indicatorDiameter 522 | { 523 | if (indicatorDiameter == _indicatorDiameter) { 524 | return; 525 | } 526 | 527 | _indicatorDiameter = indicatorDiameter; 528 | 529 | // Absolute minimum height of the control is the indicator diameter 530 | if (_minHeight < indicatorDiameter) { 531 | self.minHeight = indicatorDiameter; 532 | } 533 | 534 | [self _updateMeasuredIndicatorSizes]; 535 | [self setNeedsDisplay]; 536 | } 537 | 538 | - (void)setIndicatorMargin:(CGFloat)indicatorMargin 539 | { 540 | if (indicatorMargin == _indicatorMargin) { 541 | return; 542 | } 543 | 544 | _indicatorMargin = indicatorMargin; 545 | [self setNeedsDisplay]; 546 | } 547 | 548 | - (void)setMinHeight:(CGFloat)minHeight 549 | { 550 | if (minHeight == _minHeight) { 551 | return; 552 | } 553 | 554 | // Absolute minimum height of the control is the indicator diameter 555 | if (minHeight < _indicatorDiameter) { 556 | minHeight = _indicatorDiameter; 557 | } 558 | 559 | _minHeight = minHeight; 560 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 561 | [self invalidateIntrinsicContentSize]; 562 | } 563 | [self setNeedsLayout]; 564 | } 565 | 566 | - (void)setNumberOfPages:(NSInteger)numberOfPages 567 | { 568 | if (numberOfPages == _numberOfPages) { 569 | return; 570 | } 571 | 572 | self.accessibilityPageControl.numberOfPages = numberOfPages; 573 | 574 | _numberOfPages = MAX(0, numberOfPages); 575 | if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { 576 | [self invalidateIntrinsicContentSize]; 577 | } 578 | [self updateAccessibilityValue]; 579 | [self setNeedsDisplay]; 580 | } 581 | 582 | - (void)setCurrentPage:(NSInteger)currentPage 583 | { 584 | [self setCurrentPage:currentPage sendEvent:NO canDefer:NO]; 585 | } 586 | 587 | - (void)setCurrentPage:(NSInteger)currentPage sendEvent:(BOOL)sendEvent canDefer:(BOOL)defer 588 | { 589 | _currentPage = MIN(MAX(0, currentPage), _numberOfPages - 1); 590 | self.accessibilityPageControl.currentPage = self.currentPage; 591 | 592 | [self updateAccessibilityValue]; 593 | 594 | if (NO == self.defersCurrentPageDisplay || NO == defer) { 595 | _displayedPage = _currentPage; 596 | [self setNeedsDisplay]; 597 | } 598 | 599 | if (sendEvent) { 600 | [self sendActionsForControlEvents:UIControlEventValueChanged]; 601 | } 602 | } 603 | 604 | - (void)setCurrentPageIndicatorImage:(UIImage *)currentPageIndicatorImage 605 | { 606 | if ([currentPageIndicatorImage isEqual:_currentPageIndicatorImage]) { 607 | return; 608 | } 609 | 610 | _currentPageIndicatorImage = currentPageIndicatorImage; 611 | [self _updateMeasuredIndicatorSizes]; 612 | [self setNeedsDisplay]; 613 | } 614 | 615 | - (void)setPageIndicatorImage:(UIImage *)pageIndicatorImage 616 | { 617 | if ([pageIndicatorImage isEqual:_pageIndicatorImage]) { 618 | return; 619 | } 620 | 621 | _pageIndicatorImage = pageIndicatorImage; 622 | [self _updateMeasuredIndicatorSizes]; 623 | [self setNeedsDisplay]; 624 | } 625 | 626 | - (void)setPageIndicatorMaskImage:(UIImage *)pageIndicatorMaskImage 627 | { 628 | if ([pageIndicatorMaskImage isEqual:_pageIndicatorMaskImage]) { 629 | return; 630 | } 631 | 632 | _pageIndicatorMaskImage = pageIndicatorMaskImage; 633 | 634 | if (_pageImageMask) { 635 | CGImageRelease(_pageImageMask); 636 | } 637 | 638 | _pageImageMask = [self createMaskForImage:_pageIndicatorMaskImage]; 639 | 640 | [self _updateMeasuredIndicatorSizes]; 641 | [self setNeedsDisplay]; 642 | } 643 | 644 | - (NSMutableDictionary *)pageNames 645 | { 646 | if (nil != _pageNames) { 647 | return _pageNames; 648 | } 649 | 650 | _pageNames = [[NSMutableDictionary alloc] init]; 651 | return _pageNames; 652 | } 653 | 654 | - (NSMutableDictionary *)pageImages 655 | { 656 | if (nil != _pageImages) { 657 | return _pageImages; 658 | } 659 | 660 | _pageImages = [[NSMutableDictionary alloc] init]; 661 | return _pageImages; 662 | } 663 | 664 | - (NSMutableDictionary *)currentPageImages 665 | { 666 | if (nil != _currentPageImages) { 667 | return _currentPageImages; 668 | } 669 | 670 | _currentPageImages = [[NSMutableDictionary alloc] init]; 671 | return _currentPageImages; 672 | } 673 | 674 | - (NSMutableDictionary *)pageImageMasks 675 | { 676 | if (nil != _pageImageMasks) { 677 | return _pageImageMasks; 678 | } 679 | 680 | _pageImageMasks = [[NSMutableDictionary alloc] init]; 681 | return _pageImageMasks; 682 | } 683 | 684 | - (NSMutableDictionary *)cgImageMasks 685 | { 686 | if (nil != _cgImageMasks) { 687 | return _cgImageMasks; 688 | } 689 | 690 | _cgImageMasks = [[NSMutableDictionary alloc] init]; 691 | return _cgImageMasks; 692 | } 693 | 694 | #pragma mark - UIAccessibility 695 | 696 | - (void)setName:(NSString *)name forPage:(NSInteger)pageIndex 697 | { 698 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 699 | return; 700 | } 701 | 702 | self.pageNames[@(pageIndex)] = name; 703 | 704 | } 705 | 706 | - (NSString *)nameForPage:(NSInteger)pageIndex 707 | { 708 | if (pageIndex < 0 || pageIndex >= _numberOfPages) { 709 | return nil; 710 | } 711 | 712 | return self.pageNames[@(pageIndex)]; 713 | } 714 | 715 | - (void)updateAccessibilityValue 716 | { 717 | NSString *pageName = [self nameForPage:self.currentPage]; 718 | NSString *accessibilityValue = self.accessibilityPageControl.accessibilityValue; 719 | 720 | if (pageName) { 721 | self.accessibilityValue = [NSString stringWithFormat:@"%@ - %@", pageName, accessibilityValue]; 722 | } else { 723 | self.accessibilityValue = accessibilityValue; 724 | } 725 | } 726 | 727 | @end 728 | --------------------------------------------------------------------------------