├── CWCarouselDemo
├── oc_demo.png
├── CWCarousel
│ ├── Sources
│ │ ├── 01.jpg
│ │ ├── 02.jpg
│ │ ├── 03.jpg
│ │ ├── 04.jpg
│ │ ├── 05.jpg
│ │ ├── main.m
│ │ ├── Info.plist
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ └── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Swift
│ │ ├── CWCarousel-Bridging-Header.h
│ │ └── CWProxy.swift
│ ├── ViewController.h
│ ├── AppDelegate.h
│ ├── code
│ │ ├── CWPageControl.h
│ │ └── CWPageControl.m
│ ├── AppDelegate.m
│ ├── SViewController.swift
│ ├── TestController.swift
│ ├── ShowViewController.swift
│ └── ViewController.m
└── CWCarousel.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── OC_CWCarousel
├── CWCarouselHeader.h
├── CWFlowLayout.h
├── CWCarouselProtocol.h
├── CWCarousel.h
├── CWFlowLayout.m
└── CWCarousel.m
├── OC_CWCarousel.podspec
├── LICENSE
├── .gitignore
├── README.md
└── Swift_CWCarousel
├── CWSwiftFlowLayout.swift
└── CWBanner.swift
/CWCarouselDemo/oc_demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/oc_demo.png
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/01.jpg
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/02.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/02.jpg
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/03.jpg
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/04.jpg
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baozoudiudiu/CWCarousel/HEAD/CWCarouselDemo/CWCarousel/Sources/05.jpg
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Swift/CWCarousel-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/ViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface ViewController : UIViewController
12 |
13 |
14 | @end
15 |
16 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWCarouselHeader.h:
--------------------------------------------------------------------------------
1 | //
2 | // CWCarouselHeader.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #ifndef CWCarouselHeader_h
10 | #define CWCarouselHeader_h
11 |
12 | #import "CWCarousel.h"
13 | #import "CWCarouselProtocol.h"
14 |
15 | #endif /* CWCarouselHeader_h */
16 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. 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 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/code/CWPageControl.h:
--------------------------------------------------------------------------------
1 | //
2 | // CWPageControl.h
3 | // CWCarousel
4 | //
5 | // Created by chenwang on 2018/7/16.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "CWCarouselProtocol.h"
11 | @interface CWPageControl : UIView
12 | + (CGFloat)widthFromNumber:(NSInteger)num;
13 | @end
14 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. 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 |
--------------------------------------------------------------------------------
/OC_CWCarousel.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "OC_CWCarousel"
3 | s.version = "1.1.9"
4 | s.summary = "banner. 无限滚动轮播图"
5 | s.homepage = "https://github.com/baozoudiudiu/CWCarousel"
6 | s.license = { :type => 'MIT', :file => 'LICENSE' }
7 | s.author = { "baozoudiudiu" => "diudiu_WangChen@163.com" }
8 | s.source = { :git => "https://github.com/baozoudiudiu/CWCarousel.git",
9 | :tag => "#{s.version}" }
10 | s.platform = :ios, '9.0'
11 | s.requires_arc = true
12 | s.source_files = 'OC_CWCarousel/*'
13 | s.frameworks = 'Foundation', 'UIKit'
14 | end
15 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Swift/CWProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CWProxy.swift
3 | // CWCarousel
4 | //
5 | // Created by 陈旺 on 2021/10/28.
6 | // Copyright © 2021 ChenWang. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class CWProxy {
12 |
13 | typealias TimerHandler = (T?) -> Void
14 |
15 | private(set) weak var target: T?
16 |
17 | fileprivate var handler: TimerHandler?
18 |
19 | init(_ target: T?) {
20 | self.target = target
21 | }
22 |
23 | func createTimer(timeInterval: TimeInterval, _ action: TimerHandler?) -> Timer? {
24 | self.handler = action
25 | return Timer.scheduledTimer(timeInterval: timeInterval,
26 | target: self,
27 | selector: #selector(timerAction),
28 | userInfo: nil,
29 | repeats: true)
30 | }
31 |
32 | @objc fileprivate func timerAction() {
33 | self.handler?(self.target)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 baozoudiudiu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/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 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 | *.DS_Store
31 |
32 | # CocoaPods
33 | #
34 | # We recommend against adding the Pods directory to your .gitignore. However
35 | # you should judge for yourself, the pros and cons are mentioned at:
36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
37 | #
38 | # Pods/
39 |
40 | # Carthage
41 | #
42 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
43 | # Carthage/Checkouts
44 |
45 | Carthage/Build
46 |
47 | # fastlane
48 | #
49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
50 | # screenshots whenever they are needed.
51 | # For more information about the recommended setup visit:
52 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
53 |
54 | fastlane/report.xml
55 | fastlane/Preview.html
56 | fastlane/screenshots
57 | fastlane/test_output
58 |
59 | # Code Injection
60 | #
61 | # After new code Injection tools there's a generated folder /iOSInjectionProject
62 | # https://github.com/johnno1962/injectionforxcode
63 |
64 | iOSInjectionProject/
65 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/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 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWFlowLayout.h:
--------------------------------------------------------------------------------
1 | //
2 | // CWFlowLayout.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | typedef NS_ENUM(NSUInteger, CWCarouselStyle) {
12 | CWCarouselStyle_Unknow = 0, ///<未知样式
13 | CWCarouselStyle_Normal, ///<普通样式,一张图占用整个屏幕宽度
14 | CWCarouselStyle_H_1, ///<自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到
15 | CWCarouselStyle_H_2, ///<自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放
16 | CWCarouselStyle_H_3, ///<自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小
17 | };
18 |
19 | @interface CWFlowLayout : UICollectionViewFlowLayout
20 | /**
21 | 影响轮播图风格
22 | */
23 | @property (nonatomic, assign) CWCarouselStyle style;
24 |
25 | /**
26 | * 横向滚动时,每张轮播图之间的间距
27 | * CWCarouselStyle_H_3 样式时请设置负值
28 | */
29 | @property (nonatomic, assign) CGFloat itemSpace_H;
30 |
31 | /**
32 | * 横向滚动时,每张轮播图的宽度
33 | * style = CWCarouselStyle_Normal 时设置无效
34 | */
35 | @property (nonatomic, assign) CGFloat itemWidth;
36 |
37 | /**
38 | * style = CWCarouselStyle_H_2, CWCarouselStyle_H_3 有效
39 | * 前后2张图的缩小比例 (0.0 ~ 1.0)
40 | * 默认: 0.8
41 | */
42 | @property (nonatomic, assign) CGFloat minScale;
43 |
44 | /**
45 | * (注: 1.1.7 版本后该属性已经废弃, 设置该属性将不生效. 请使用 minScale代替.)
46 | * style = CWCarouselStyle_H_3 有效
47 | * 中间一张图放大比例
48 | * 默认: 1.2
49 | * 1.1.0版本后,无论设置多少,中间一张的cell的比例始终是原始size, 这个比例是相对两边cell的size的相对比例
50 | 也就是说,该值越大,那么两边的cell就会相对越小.反之越大.
51 | */
52 | @property (nonatomic, assign) CGFloat maxScale;
53 |
54 | /**
55 | 纵向滚动时,每张轮播图之间的间距(暂未实现)
56 | */
57 | @property (nonatomic, assign) CGFloat itemSpace_V;
58 |
59 |
60 | /**
61 | 构造方法
62 |
63 | @param style 轮播图风格
64 | @return 实例对象
65 | */
66 | - (instancetype)initWithStyle:(CWCarouselStyle)style;
67 | @end
68 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/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 | }
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. 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 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/SViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SViewController.swift
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SViewController : UIViewController {
12 | override func viewDidLoad() {
13 | self.configureUI()
14 | }
15 |
16 | private func configureUI () {
17 | self.view.backgroundColor = UIColor.white
18 | self.automaticallyAdjustsScrollViewInsets = false;
19 | self.listView.tableFooterView = UIView.init()
20 | }
21 |
22 | override func didReceiveMemoryWarning() {
23 |
24 | }
25 |
26 | //MARK: - Property
27 | @IBOutlet weak var listView: UITableView!
28 | /// 选项数据源
29 | let titles = ["默认样式",
30 | "可以看到前后两张(正常样式)",
31 | "可以看到前后两张(两边缩放)",
32 | "可以看到前后两张(中间一张放大)"]
33 |
34 | let types = [CWBannerStyle.normal,
35 | CWBannerStyle.preview_normal,
36 | CWBannerStyle.preview_zoom,
37 | CWBannerStyle.preview_big]
38 | }
39 |
40 | extension SViewController: UITableViewDelegate, UITableViewDataSource {
41 | // cell selected
42 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
43 | tableView.deselectRow(at: indexPath, animated: true)
44 | let type = self.types[indexPath.row]
45 | let showVC = ShowViewController.init(style: type)
46 | showVC.hidesBottomBarWhenPushed = true
47 | self.navigationController?.pushViewController(showVC, animated: true)
48 | }
49 |
50 | // cell numbers
51 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
52 | return self.titles.count
53 | }
54 |
55 | // cells
56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
57 | let idStr = "cellId"
58 | var cell = tableView.dequeueReusableCell(withIdentifier: idStr)
59 | if cell == nil {
60 | cell = UITableViewCell.init(style: .default, reuseIdentifier: idStr)
61 | cell?.accessoryType = .disclosureIndicator
62 | }
63 | cell?.textLabel?.text = self.titles[indexPath.row]
64 | return cell!
65 | }
66 |
67 | // cell height
68 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
69 | return 44
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWCarouselProtocol.h:
--------------------------------------------------------------------------------
1 | //
2 | // CWCarouselProtocol.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #ifndef CWCarouselProtocol_h
10 | #define CWCarouselProtocol_h
11 |
12 | @class CWCarousel;
13 | @protocol CWCarouselDelegate
14 | /**
15 | 轮播图点击代理
16 |
17 | @param carousel 轮播图实例对象
18 | @param index 被点击的下标
19 | */
20 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index;
21 |
22 | @optional
23 |
24 | /**
25 | 将要开始滑动时,会触发该代理
26 |
27 | @param carousel 轮播图控件
28 | @param index 开始滑动时,处于中心点图片的下标
29 | @param indexPathRow 开始滑动时,处于中心点图片的在控件内部的实际下标
30 | */
31 | - (void)CWCarousel:(CWCarousel *)carousel didStartScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow;
32 |
33 |
34 | /**
35 | 滑动结束后,会触发该代理
36 |
37 | @param carousel 轮播图控件
38 | @param index 结束滑动时,处于中心点图片的下标
39 | @param indexPathRow 结束滑动时,处于中心点图片在控件内部的实际下标
40 | */
41 | - (void)CWCarousel:(CWCarousel *)carousel didEndScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow;
42 |
43 |
44 | /// 开始布局pageControl时会触发该回调
45 | /// @param carousel 轮播组件对象
46 | /// @param pageControl pageControl对象
47 | /// @param isDefault YES: 组件内部默认的UIPageControl, NO: 自定义的customPageControl
48 | ///
49 | /// 可以在这个回调中自定义pageControl的布局, 如果没有实现该回调, 将默认在底部居中.
50 | ///
51 | /// 注意:
52 | /// 1. 先设置customPageControl, 后将CWCarousel添加到父视图中, 此回调只会被调用一次.
53 | /// 2. 先将CWCarousel添加到父视图中, 后设置customPageControl, 此回调会被调用两次, 第一次isDefault = YES, 第二次isDefault = NO. 因为将CWCarousel添加到父视图时检测到没有customPageControl, 组件内部并不清楚外部调用者是否会设置customPageControl, 会先创建并添加默认的pageControl.
54 | /// 3. 当自定义的customPageControl宽度布局不是自己撑开的时候, 请在该回调中自己布局. 否则采用默认的布局将不可见.
55 | - (void)CWCarousel:(CWCarousel *)carousel addPageControl:(UIView *)pageControl isDefault:(BOOL)isDefault;
56 |
57 | @end
58 |
59 | @protocol CWCarouselDatasource
60 | /**
61 | 轮播图数量
62 |
63 | @return 轮播图展示个数
64 | */
65 | - (NSInteger)numbersForCarousel;
66 | /**
67 | 自定义每个轮播图视图
68 |
69 | @param carousel 轮播图控件
70 | @param indexPath 轮播图cell实际下标
71 | @param index 业务逻辑需要的下标
72 | @return 自定义视图
73 | */
74 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index;
75 | @end
76 |
77 |
78 | @protocol CWCarouselPageControlProtocol
79 | @required
80 | /**
81 | 总页数
82 | */
83 | @property (nonatomic, assign, readonly) NSInteger pageNumbers;
84 | /**
85 | 当前页
86 | */
87 | @property (nonatomic, assign, readonly) NSInteger currentPage;
88 |
89 | - (void)setCurrentPage:(NSInteger)currentPage;
90 | - (void)setPageNumbers:(NSInteger)pageNumbers;
91 | @end
92 | #endif /* CWCarouselProtocol_h */
93 |
94 |
95 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/code/CWPageControl.m:
--------------------------------------------------------------------------------
1 | //
2 | // CWPageControl.m
3 | // CWCarousel
4 | //
5 | // Created by chenwang on 2018/7/16.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import "CWPageControl.h"
10 | @interface CWPageControl () {
11 |
12 | }
13 | @property (nonatomic, assign) NSInteger myPageNumbers;
14 | @property (nonatomic, assign) NSInteger myCurrentPage;
15 |
16 | @property (nonatomic, strong) NSArray *indicatorArr;
17 | @property (nonatomic, strong) UIView *currentIndicator;
18 | @end
19 | @implementation CWPageControl
20 | @synthesize currentPage;
21 | @synthesize pageNumbers;
22 |
23 | #pragma mark - INITIAL
24 | - (instancetype)initWithFrame:(CGRect)frame {
25 | if(self = [super initWithFrame:frame]) {
26 | [self configureView];
27 | }
28 | return self;
29 | }
30 |
31 | #pragma mark - PROPERTY
32 | - (void)setCurrentPage:(NSInteger)currentPage {
33 | if (currentPage >= self.indicatorArr.count) {
34 | return;
35 | }
36 | self.myCurrentPage = currentPage;
37 | UIView *indicator = self.indicatorArr[currentPage];
38 | [UIView animateWithDuration:0.25 animations:^{
39 | self.currentIndicator.frame = indicator.frame;
40 | }];
41 |
42 | }
43 |
44 | - (void)setPageNumbers:(NSInteger)pageNumbers {
45 | if (self.pageNumbers == pageNumbers) {
46 | return;
47 | }
48 | self.myPageNumbers = pageNumbers;
49 | [self createIndicator];
50 | }
51 |
52 | - (NSInteger)currentPage {
53 | return self.currentPage;
54 | }
55 |
56 | - (NSInteger)pageNumbers {
57 | return self.myPageNumbers;
58 | }
59 |
60 | #pragma mark - LOGIC HELPER
61 | - (void)configureView {
62 |
63 | }
64 |
65 | + (CGFloat)widthFromNumber:(NSInteger)num {
66 | return 25 * num + 2 * (num - 1);
67 | }
68 |
69 | - (void)createIndicator {
70 | [self.indicatorArr makeObjectsPerformSelector:@selector(removeFromSuperview)];
71 | CGFloat width = 25;
72 | CGFloat height = 7;
73 | CGFloat space = 2;
74 | CGFloat containerWidth = (width + space) * self.pageNumbers - space;
75 | UIView *container = [[UIView alloc] initWithFrame:CGRectMake((CGRectGetWidth(self.frame) - containerWidth) * 0.5, 0, containerWidth, CGRectGetHeight(self.frame))];
76 | [self addSubview:container];
77 | NSMutableArray *arr = [NSMutableArray arrayWithCapacity:self.pageNumbers];
78 | for(int i = 0; i < self.pageNumbers; i ++) {
79 | CGFloat y = (CGRectGetHeight(self.frame) - height) * 0.5;
80 | CGFloat x = (width + space) * i;
81 | UIView *indictor = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)];
82 | [container addSubview:indictor];
83 | indictor.backgroundColor = [UIColor groupTableViewBackgroundColor];
84 | [arr addObject:indictor];
85 | if(i == 0 && self.currentIndicator == nil) {
86 | UIView *indictor = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)];
87 | [container addSubview:indictor];
88 | self.currentIndicator = indictor;
89 | self.currentIndicator.backgroundColor = [UIColor redColor];
90 | self.currentIndicator.layer.zPosition = 999;
91 | }
92 | }
93 | self.indicatorArr = [NSArray arrayWithArray:arr];
94 | }
95 | @end
96 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWCarousel.h:
--------------------------------------------------------------------------------
1 | //
2 | // CWCarousel.h
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "CWCarouselProtocol.h"
11 | #import "CWFlowLayout.h"
12 |
13 | @class CWCarouselCollectionView;
14 | @interface CWCarousel : UIView
15 | #pragma mark - < 相关属性 >
16 |
17 | /**
18 | 控件版本号
19 | */
20 | @property (nonatomic, readonly, copy) NSString * _Nullable version;
21 |
22 |
23 | /**
24 | 相关代理
25 | */
26 | @property (nonatomic, assign) id _Nullable delegate;
27 |
28 |
29 | /**
30 | 相关数据源
31 | */
32 | @property (nonatomic, assign) id _Nullable datasource;
33 |
34 |
35 | /**
36 | 布局自定义layout
37 | */
38 | @property (nonatomic, strong, readonly) CWFlowLayout * _Nonnull flowLayout;
39 |
40 |
41 | /**
42 | 样式风格
43 | */
44 | @property (nonatomic, assign, readonly) CWCarouselStyle style;
45 |
46 |
47 | /**
48 | style = CWCarouselStyle_H_3时的扩展高度 (1.1.0版本后该属性废弃,请不要使用了)
49 | */
50 | @property (nonatomic, assign, readonly) CGFloat addHeight;
51 |
52 |
53 | /**
54 | 实际的示轮播图内容的视图(其实就是基于collectionView实现的)
55 | */
56 | @property (nonatomic, strong, readonly) CWCarouselCollectionView * _Nonnull carouselView;
57 |
58 |
59 | /**
60 | 是否自动轮播, 默认为NO
61 | */
62 | @property (nonatomic, assign) BOOL isAuto;
63 |
64 |
65 | /**
66 | 自动轮播时间间隔, 默认 3s
67 | */
68 | @property (nonatomic, assign) NSTimeInterval autoTimInterval;
69 |
70 |
71 | /**
72 | 默认的pageControl, 当设置了customPageControl时, 该属性为nil
73 | */
74 | @property (nonatomic, strong) UIPageControl * _Nullable pageControl;
75 |
76 |
77 | /**
78 | 自定义的pageControl
79 | */
80 | @property (nonatomic, strong) UIView * _Nullable customPageControl;
81 |
82 |
83 | /**
84 | 是否开始无限轮播
85 | YES: 可以无限衔接
86 | NO: 滑动到第一张或者最后一张就不能滑动了
87 | */
88 | @property (nonatomic, assign) BOOL endless;
89 | #pragma mark - < 相关方法 >
90 | /**
91 | 创建实例构造方法
92 |
93 | @param frame 尺寸大小
94 | @param delegate 代理
95 | @param datasource 数据源
96 | @param flowLayout 自定义flowlayout
97 | @return 实例对象
98 | */
99 | - (instancetype _Nullable )initWithFrame:(CGRect)frame
100 | delegate:(id _Nullable)delegate
101 | datasource:(id _Nullable)datasource
102 | flowLayout:(nonnull CWFlowLayout *)flowLayout;
103 |
104 | /**
105 | 注册自定视图
106 |
107 | @param viewClass 自定义视图类名
108 | @param identifier 重用唯一标识符
109 | */
110 | - (void)registerViewClass:(Class _Nullable )viewClass identifier:(NSString *_Nullable)identifier;
111 |
112 | /**
113 | 注册自定义视图
114 |
115 | @param nibName 自定义视图xib相关文件名
116 | @param identifier 重用唯一标识符
117 | */
118 | - (void)registerNibView:(NSString *_Nullable)nibName identifier:(NSString *_Nullable)identifier;
119 |
120 |
121 | /**
122 | 刷新轮播图
123 | */
124 | - (void)freshCarousel;
125 |
126 | /**
127 | 暂停轮播图后,可以调用改方法继续播放
128 | */
129 | - (void)resumePlay;
130 |
131 | /**
132 | 轮播图暂停自动播放
133 | */
134 | - (void)pause;
135 |
136 | /**
137 | 如果开启自动轮播,销毁前需要调用该方法,释放定时器.否则可能内存泄漏
138 | 注: 1.1.9版本之后无需再手动调用该方法, 控件内部会自动释放.
139 | */
140 | - (void)releaseTimer;
141 |
142 | /**
143 | 轮播图所处控制器WillAppear方法里调用
144 | */
145 | - (void)controllerWillAppear;
146 |
147 | /**
148 | 轮播图所处控制器WillDisAppear方法里调用
149 | */
150 | - (void)controllerWillDisAppear;
151 |
152 |
153 | /// 滚动到指定下标
154 | /// @param index 指定下标
155 | /// @param animation 是否开启滚动动画
156 | - (void)scrollTo:(NSInteger)index animation:(BOOL)animation;
157 | @end
158 |
159 |
160 |
161 |
162 |
163 |
164 | @interface CWCarouselCollectionView: UICollectionView
165 |
166 | @end
167 |
168 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/TestController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestController.swift
3 | // CWCarousel
4 | //
5 | // Created by 陈旺 on 2021/10/8.
6 | // Copyright © 2021 ChenWang. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TestController: UIViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 | self.view.backgroundColor = .cyan
16 |
17 | let width = self.view.frame.size.width
18 | let height: CGFloat = 400
19 | let scrollView = UIScrollView()
20 | scrollView.delegate = self
21 | scrollView.frame = CGRect(x: 0, y: 100, width: width, height: height)
22 | scrollView.backgroundColor = UIColor.white
23 | self.view.addSubview(scrollView)
24 |
25 | var view = UIView()
26 | view.frame = CGRect(x: 0, y: 0, width: width, height: height)
27 | view.backgroundColor = .red
28 | scrollView.addSubview(view)
29 |
30 | view = UIView()
31 | view.frame = CGRect(x: width, y: 0, width: width, height: height)
32 | view.backgroundColor = .orange
33 | scrollView.addSubview(view)
34 |
35 | scrollView.contentSize = CGSize(width: width * 2, height: height)
36 | if #available(iOS 11.0, *) {
37 | scrollView.contentInsetAdjustmentBehavior = .never
38 | } else {
39 | // Fallback on earlier versions
40 | }
41 |
42 |
43 | // let layout = UICollectionViewFlowLayout()
44 | // layout.itemSize = CGSize(width: self.view.frame.size.width, height: 400)
45 | // layout.minimumLineSpacing = 10
46 | // layout.scrollDirection = .horizontal
47 | // let collection = UICollectionView(frame: CGRect(x: 0, y: 100, width: self.view.frame.size.width, height: 400),
48 | // collectionViewLayout: layout)
49 | // collection.backgroundColor = .orange
50 | // self.view.addSubview(collection)
51 | //
52 | // collection.delegate = self
53 | // collection.dataSource = self
54 | // collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellId")
55 | }
56 | }
57 |
58 | extension TestController: UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource {
59 |
60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
61 | return 5
62 | }
63 |
64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
65 | let colors: [UIColor] = [.red, .orange, .yellow, .green, .blue]
66 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
67 | cell.contentView.backgroundColor = colors[indexPath.row]
68 | return cell
69 | }
70 |
71 |
72 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
73 |
74 | }
75 |
76 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
77 | print("will begin dragging")
78 | }
79 |
80 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
81 | print("will end dragging")
82 | }
83 |
84 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
85 | print("did end dragging")
86 | }
87 |
88 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
89 | print("will begin decelerating")
90 | }
91 |
92 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
93 | print("did end decelerating")
94 | }
95 |
96 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
97 | print("did end scrolling animation")
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/ShowViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShowViewController.swift
3 | // CWCarousel
4 | //
5 | // Created by chenwang on 2018/7/20.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | let cellReuseId = "cellReuseId"
12 | class ShowViewController: UIViewController {
13 | //MARK: - INITILAL
14 | init(style: CWBannerStyle) {
15 | self.bannerStyle = style
16 | super.init(nibName: nil, bundle: nil)
17 | }
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | self.bannerStyle = .unknown
21 | super.init(coder: aDecoder)
22 | }
23 |
24 | deinit {
25 | NSLog("[%@ -- %@]", NSStringFromClass(self.classForCoder), #function)
26 | }
27 |
28 | //MARK: - LIFE CYCLE
29 | override func viewDidLoad() {
30 | super.viewDidLoad()
31 | self.configureUI()
32 | }
33 |
34 | override func viewWillAppear(_ animated: Bool) {
35 | super.viewWillAppear(animated)
36 | self.bannerView.bannerWillAppear()
37 | }
38 |
39 | override func viewWillDisappear(_ animated: Bool) {
40 | super.viewWillDisappear(animated)
41 | self.bannerView.bannerWillDisAppear()
42 | }
43 |
44 | override func didReceiveMemoryWarning() {
45 | super.didReceiveMemoryWarning()
46 | }
47 | //MARK: - PROPERTY
48 | let bannerStyle: CWBannerStyle
49 |
50 | let imgNames = ["01.jpg",
51 | "02.jpg",
52 | "03.jpg",
53 | "04.jpg",
54 | "05.jpg"]
55 |
56 | lazy var bannerView: CWBanner = {
57 | let layout = CWSwiftFlowLayout.init(style: self.bannerStyle)
58 | layout.itemSpace = 10
59 | if self.bannerStyle == .preview_big {
60 | layout.itemSpace = -20
61 | }
62 | let banner = CWBanner.init(frame: CGRect.init(x: 0, y: 100, width: UIScreen.main.bounds.width, height: 240), flowLayout: layout, delegate: self)
63 | self.view.addSubview(banner)
64 |
65 | banner.backgroundColor = self.view.backgroundColor
66 | banner.banner.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellReuseId)
67 |
68 | banner.autoPlay = false
69 | banner.endless = true
70 | banner.timeInterval = 2
71 |
72 | return banner
73 | }()
74 |
75 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
76 | self.bannerView.scroll(to: 3, animated: false)
77 | }
78 | }
79 |
80 | //MARK: - CWBannerDelegate
81 | extension ShowViewController: CWBannerDelegate {
82 | func bannerNumbers() -> Int {
83 | return self.imgNames.count
84 | }
85 |
86 | func bannerView(banner: CWBanner, index: Int, indexPath: IndexPath) -> UICollectionViewCell {
87 | let cell = banner.banner.dequeueReusableCell(withReuseIdentifier: cellReuseId, for: indexPath)
88 | var imgView = cell.contentView.viewWithTag(999)
89 | var label = cell.contentView.viewWithTag(888)
90 | if imgView == nil {
91 | imgView = UIImageView.init(frame: cell.contentView.bounds)
92 | imgView?.tag = 999
93 | cell.contentView.addSubview(imgView!)
94 | imgView?.layer.cornerRadius = 4.0
95 | imgView?.layer.masksToBounds = true
96 |
97 | label = UILabel.init(frame: CGRect.init(x: 30, y: 0, width: 60, height: 30))
98 | (label as! UILabel).textColor = UIColor.white
99 | (label as! UILabel).font = UIFont.systemFont(ofSize: 21)
100 | label?.tag = 888
101 | cell.contentView.addSubview(label!)
102 | }
103 | (imgView as! UIImageView).image = UIImage.init(named: self.imgNames[index])
104 | (label as! UILabel).text = "\(index)"
105 | return cell
106 | }
107 |
108 | func didSelected(banner: CWBanner, index: Int, indexPath: IndexPath) {
109 | print("点击 \(index) click...")
110 | }
111 |
112 | func didStartScroll(banner: CWBanner, index: Int, indexPath: IndexPath) {
113 | print("开始滑动: \(index) ...")
114 | }
115 |
116 | func didEndScroll(banner: CWBanner, index: Int, indexPath: IndexPath) {
117 | print("结束滑动: \(index) ...")
118 | }
119 | }
120 |
121 | // MARK: - CONFIGURE UI
122 | extension ShowViewController {
123 | fileprivate func configureUI() {
124 | self.view.backgroundColor = UIColor.white
125 | self.navigationItem.title = "show"
126 | self.bannerView.freshBanner()
127 | self.automaticallyAdjustsScrollViewInsets = false
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CWCarousel
2 |
3 | ## 基于collectionView实现的通用无限轮播图封装.
4 |
5 | > 1. issues里面的可以重现的问题已经修复.
6 | > 2. 部分不能重现的问题暂未处理.
7 | > 3. 请大家提issue尽量描述详细些,最好能提供代码或者截图.请附上是OC版本还是Swift版本的问题.
8 | > 4. 谢谢配合,大家一起进步.
9 | > 5. 代码示例为oc代码, 项目中已有swift版本,请自行下载.
10 | > 6. oc版本支持cocoaPods安装 `pod search OC_CWCarousel`
11 |
12 | oc版和swift是两份完全不同的代码,所以代码实现不尽相同。有些可能在其中一个版本实现了,但是另一个版本不一定来得及实现。请参照下面的版本记录自行了解:
13 |
14 | #### OC:版本记录
15 | 
16 |
17 | | 版本号 | 更新内容 |
18 | | ------ | ------ |
19 | | 1.1.9 | 1、采用 NSTimer + NSProxy方式实现自动轮播,外部可以无需再手动调用releaseTimer方法来释放内存了。
2、优化了scrollToIndex内部实现
3、修改pageControl布局代理的触发逻辑 |
20 | | 1.1.8 | 1. 代码重构。
2. 计算间距算法修改,更好更简单的设置间距。
3. 新增代理回调:`- (void)CWCarousel:(CWCarousel*)carousel addPageControl:(UIView* )pageControl;`可以在该回调中自定义布局pageControl。
4. 新增滚动到指定位置方法:`- (void*scrollTo:(NSInteger)index animation:(BOOL)animation`;。
5. 废弃了`maxScale`属性。
6.优化了自定义pageControl的实现。
7.修改了演示demo首页样式和逻辑。 |
21 | | 1.1.7 | 适配iOS14 感谢[milletZZ](https://github.com/milletZZ) |
22 | | 1.1.5 | 1.新增了2个代理回调
2.点击事件代理回调逻辑优化 |
23 | | 1.1.4 | 修复样式3在320尺寸屏幕上可能出现的问题 |
24 | | 1.1.3 | 提升了一个版本号 |
25 | | 1.1.2 | 控件优化 |
26 | | 1.1.1 | 添加了无限轮播功能开关 endless |
27 | | 1.1.0 | 修复了之前的一些bug |
28 |
29 | #### swift:版本记录
30 | | 版本号 | 更新内容 |
31 | | ------ | :----- |
32 | | 1.1.9| 1、代码重构 2、timer实现方式优化,外部无需手动调用releaseTimer释放内存 3、计算间距算法修改,更好更简单的设置间距 4、新增滚动到指定位置方法|
33 | | 1.1.8| 1.fix bug,2. 自定义pageControl优化, 3.只有一张图时不可无限滑动, 4.无数据时可以设置占位视图|
34 | | 1.1.7 | 适配iOS14 感谢[milletZZ](https://github.com/milletZZ) |
35 | | 1.1.4 | 修复通过layout创建视图,偶现不自动轮播问题. |
36 | | 1.1.3 | 适配swift5.0 |
37 | | 1.1.2 | 修复样式3在320尺寸屏幕上可能出现的问题 |
38 | | 1.1.1 | 添加了无限轮播功能开关 endless |
39 | | 1.1.0 | 功能同OC版 1.1.0 |
40 |
41 |
42 | 
43 |
44 | * 目前支持4种样式
45 | ```
46 | typedef NS_ENUM(NSUInteger, CWCarouselStyle) {
47 | CWCarouselStyle_Unknow = 0, ///<未知样式
48 | CWCarouselStyle_Normal, ///<普通样式,一张图占用整个屏幕宽度
49 | CWCarouselStyle_H_1, ///<自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到
50 | CWCarouselStyle_H_2, ///<自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放
51 | CWCarouselStyle_H_3, ///<自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小
52 | };
53 | ```
54 | > CWCarouselStyle_Normal
55 |
56 |
57 | 
58 |
59 | > CWCarouselStyle_H_1
60 |
61 |
62 | 
63 | > CWCarouselStyle_H_2
64 |
65 |
66 | 
67 |
68 | > CWCarouselStyle_H_3
69 |
70 |
71 | 
72 |
73 | * 控件实例对象创建
74 | 1. 创建flowLayout对象,设置轮播图风格
75 | ```
76 | /**
77 | 构造方法
78 |
79 | @param style 轮播图风格
80 | @return 实例对象
81 | */
82 | - (instancetype)initWithStyle:(CWCarouselStyle)style;
83 |
84 | // egg:
85 | CWFlowLayout *flowLayout = [[CWFlowLayout alloc] initWithStyle:[self styleFromTag:tag]];
86 | ```
87 | 2. 创建容器对象
88 | ```
89 | /**
90 | 创建实例构造方法
91 |
92 | @param frame 尺寸大小
93 | @param delegate 代理
94 | @param datasource 数据源
95 | @param flowLayout 自定义flowlayout
96 | @return 实例对象
97 | */
98 | - (instancetype _Nullable )initWithFrame:(CGRect)frame
99 | delegate:(id _Nullable)delegate
100 | datasource:(id _Nullable)datasource
101 | flowLayout:(nonnull CWFlowLayout *)flowLayout;
102 |
103 | // egg:
104 | CWCarousel *carousel = [[CWCarousel alloc] initWithFrame:self.animationView.bounds
105 | delegate:self
106 | datasource:self
107 | flowLayout:flowLayout];
108 | ```
109 | 3. 注册自定义cell,并实现代理方法,刷新视图
110 | ```
111 | [carousel registerViewClass:[UICollectionViewCell class] identifier:@"cellId"];
112 | [carousel freshCarousel];
113 |
114 | #pragma mark - Delegate
115 | // 每个轮播图cell样式
116 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index{
117 | UICollectionViewCell *cell = [carousel.carouselView dequeueReusableCellWithReuseIdentifier:@"cellId" forIndexPath:indexPath];
118 | /*
119 | your code
120 | */
121 | return cell;
122 | }
123 |
124 | // 点击代理回调
125 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index {
126 | NSLog(@"...%ld...", index);
127 | }
128 |
129 | // 轮播图个数
130 | - (NSInteger)numbersForCarousel {
131 | return kCount;
132 | }
133 | ```
134 |
135 | 4. 为了流畅性和避免概率图片位置错乱问题,当开启自动滚动时,在banner所处的控制器生命周期中需要调用以下对应方法
136 | ```
137 | /**
138 | 轮播图所处控制器WillAppear方法里调用
139 | */
140 | - (void)controllerWillAppear;
141 |
142 | /**
143 | 轮播图所处控制器WillDisAppear方法里调用
144 | */
145 | - (void)controllerWillDisAppear;
146 | ```
147 | * 具体UI样式修改都有具体的属性,详情请查看对应类的.h文件即可.
148 | * 如有问题和bug,欢迎指正.
149 |
150 |
--------------------------------------------------------------------------------
/Swift_CWCarousel/CWSwiftFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CWSwiftFlowLayout.swift
3 | // CWCarousel
4 | //
5 | // Created by chenwang on 2018/7/18.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// banner风格枚举
12 | enum CWBannerStyle {
13 | /// 未知样式
14 | case unknown
15 | /// 默认样式
16 | case normal
17 | /// 自定义样式一, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到
18 | case preview_normal
19 | /// 自定义样式二, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,并且中间一张图正常大小,前后2张图会缩放
20 | case preview_zoom
21 | /// 自定义样式三, 中间一张居中,前后2张图有部分内容在屏幕内可以预览到,中间一张有放大效果,前后2张正常大小
22 | case preview_big
23 | }
24 |
25 | class CWSwiftFlowLayout: UICollectionViewFlowLayout {
26 | //MARK: - 构造方法
27 | init(style: CWBannerStyle) {
28 | self.style = style
29 | super.init()
30 | }
31 |
32 | deinit {
33 | NSLog("[%@ -- %@]",NSStringFromClass(self.classForCoder) ,#function);
34 | }
35 |
36 | required init?(coder aDecoder: NSCoder) {
37 | self.style = .unknown
38 | super.init(coder: aDecoder)
39 | }
40 |
41 | //MARK: - Override
42 | override func prepare() {
43 | super.prepare()
44 | guard self.collectionView != nil else {
45 | assert(self.collectionView != nil, "error")
46 | return
47 | }
48 | switch self.style {
49 | case .normal:
50 | self.initialNormalStyle()
51 | case .preview_normal:
52 | self.initialPreview_normalStyle()
53 | case .preview_zoom:
54 | self.initialPreview_zoomStyle()
55 | case .preview_big:
56 | self.initialPreview_bigStyle()
57 | default:()
58 | }
59 | }
60 |
61 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
62 | return true
63 | }
64 |
65 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
66 | var arr: [UICollectionViewLayoutAttributes]? = nil;
67 | switch self.style {
68 | case .unknown: fallthrough
69 | case .normal: fallthrough
70 | case .preview_normal: arr = super.layoutAttributesForElements(in: rect)
71 | case .preview_zoom: fallthrough
72 | case .preview_big:
73 | arr = self.caculateScale(rect: rect, block: { (width, space) -> CGFloat in
74 | return (self.minScale - 1) / (self.itemSize.width) * space + 1;
75 | })
76 | }
77 | return arr
78 | }
79 |
80 | //MARK: - Property
81 | /// banner风格
82 | let style: CWBannerStyle
83 |
84 | /// 每张图之间的间距, 默认为0
85 | var itemSpace: CGFloat = 0
86 |
87 | /*
88 | * cell的宽度占总宽度的比例
89 | * 默认: 0.75
90 | * normal 样式下无效
91 | */
92 | var itemWidthScale: CGFloat = 0.75
93 |
94 | /**
95 | * style = preview_zoom 有效
96 | * 前后2张图的缩小比例 (0.0 ~ 1.0)
97 | * 默认: 0.8
98 | */
99 | var minScale: CGFloat = 0.8
100 | }
101 |
102 |
103 | // MARK: - Logic Helper
104 | extension CWSwiftFlowLayout {
105 |
106 | /// 计算cell缩放公式
107 | ///
108 | /// - Parameters:
109 | /// - rect: rect
110 | /// - block: 计算方法闭包
111 | /// - Returns:
112 | fileprivate func caculateScale(rect: CGRect, block: (CGFloat, CGFloat) -> CGFloat) -> [UICollectionViewLayoutAttributes]? {
113 | let arr = super.layoutAttributesForElements(in: rect)?.map({$0.copy()}) as! [UICollectionViewLayoutAttributes]?
114 | let centerX = self.collectionView!.contentOffset.x + self.collectionView!.frame.width * 0.5
115 | let width = self.collectionView!.frame.width * self.itemWidthScale
116 | var maxScale: CGFloat = 0
117 | var attri: UICollectionViewLayoutAttributes? = nil
118 | arr?.forEach({ (element) in
119 | let space = CGFloat(abs(element.center.x - centerX))
120 | var scale: CGFloat = 1.0
121 | scale = block(width, space)
122 | element.transform = CGAffineTransform.init(scaleX: scale, y: scale)
123 | if maxScale < scale {
124 | maxScale = scale
125 | attri = element
126 | }
127 | element.zIndex = 0
128 | })
129 | if attri != nil {
130 | attri?.zIndex = 1
131 | }
132 | return arr
133 | }
134 |
135 | /// 设置默认样式
136 | fileprivate func initialNormalStyle() {
137 | self.scrollDirection = .horizontal
138 | let width = self.collectionView!.frame.width
139 | let height = self.collectionView!.frame.height
140 | self.itemSize = CGSize.init(width: width, height: height)
141 | self.minimumLineSpacing = self.itemSpace;
142 | }
143 |
144 | /// 设置preview_默认样式
145 | fileprivate func initialPreview_normalStyle() {
146 | self.scrollDirection = .horizontal
147 | let height = self.collectionView!.frame.height
148 | let width = self.collectionView!.frame.width * self.itemWidthScale
149 | self.itemSize = CGSize.init(width: width, height: height)
150 | self.minimumLineSpacing = self.itemSpace;
151 | }
152 |
153 | /// 设置preiview_zoom样式
154 | fileprivate func initialPreview_zoomStyle() {
155 | self.scrollDirection = .horizontal
156 | let height = self.collectionView!.frame.height
157 | let width = self.collectionView!.frame.width * self.itemWidthScale
158 | self.itemSize = CGSize.init(width: width, height: height)
159 | let padding = width * (1 - self.minScale) * 0.5;
160 | self.minimumLineSpacing = self.itemSpace - padding;
161 | }
162 |
163 | /// 设置preiview_big样式
164 | fileprivate func initialPreview_bigStyle() {
165 | self.scrollDirection = .horizontal
166 | let height = self.collectionView!.frame.height
167 | let width = self.collectionView!.frame.width * self.itemWidthScale
168 | self.itemSize = CGSize.init(width: width, height: height)
169 | let padding = width * (1 - self.minScale) * 0.5;
170 | self.minimumLineSpacing = self.itemSpace - padding;
171 | }
172 | }
173 |
174 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWFlowLayout.m:
--------------------------------------------------------------------------------
1 | //
2 | // CWFlowLayout.m
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import "CWFlowLayout.h"
10 |
11 | @interface CWFlowLayout () {
12 |
13 | }
14 | /**
15 | 默认轮播图宽度
16 | */
17 | @property (nonatomic, assign) CGFloat defaultItemWidth;
18 | @property (nonatomic, assign) CGFloat factItemSpace;
19 | @end
20 |
21 | @implementation CWFlowLayout
22 |
23 | - (instancetype)initWithStyle:(CWCarouselStyle)style {
24 | if(self = [super init]) {
25 | self.style = style;
26 | [self initial];
27 | }
28 | return self;
29 | }
30 |
31 | - (void)dealloc {
32 | NSLog(@"%s", __func__);
33 | }
34 |
35 | - (void)initial {
36 | self.itemSpace_H = 0;
37 | self.itemSpace_V = 0;
38 | self.minScale = 0.8;
39 | self.maxScale = 1.2;
40 | }
41 |
42 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
43 |
44 | if (self.style == CWCarouselStyle_Unknow) {
45 | return NO;
46 | }
47 |
48 | if (self.style == CWCarouselStyle_Normal) {
49 | return NO;
50 | }
51 |
52 | if (self.style == CWCarouselStyle_H_1) {
53 | return NO;
54 | }
55 |
56 | return YES;
57 | }
58 |
59 | - (void)prepareLayout {
60 | [super prepareLayout];
61 | switch (self.style) {
62 | case CWCarouselStyle_Normal: {
63 | CGFloat width = CGRectGetWidth(self.collectionView.frame);
64 | CGFloat height = CGRectGetHeight(self.collectionView.frame);
65 | self.itemWidth = width;
66 | self.itemSize = CGSizeMake(width, height);
67 | self.minimumLineSpacing = self.itemSpace_H;
68 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
69 | }
70 | break;
71 | case CWCarouselStyle_H_1: {
72 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth;
73 | self.itemWidth = width;
74 | CGFloat height = CGRectGetHeight(self.collectionView.frame);
75 | self.itemSize = CGSizeMake(width, height);
76 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
77 | self.minimumLineSpacing = self.itemSpace_H;
78 | break;
79 | }
80 | case CWCarouselStyle_H_2: {
81 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth;
82 | self.itemWidth = width;
83 | CGFloat height = CGRectGetHeight(self.collectionView.frame);
84 | self.itemSize = CGSizeMake(width, height);
85 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
86 | CGFloat padding = width * (1 - self.minScale) * 0.5;
87 | self.factItemSpace = 0;
88 | self.minimumLineSpacing = self.itemSpace_H - padding;
89 | }
90 | break;
91 | case CWCarouselStyle_H_3: {
92 | CGFloat width = self.itemWidth <= 0 ? self.defaultItemWidth : self.itemWidth;
93 | self.itemWidth = width;
94 | CGFloat height = CGRectGetHeight(self.collectionView.frame);
95 | self.itemSize = CGSizeMake(width, height);
96 | self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
97 | CGFloat padding = width * (1 - self.minScale) * 0.5;
98 | self.factItemSpace = 0;
99 | // if(width * (1 - self.minScale) * 0.5 < self.itemSpace_H) {
100 | // self.factItemSpace = self.itemSpace_H - width * (1 - self.minScale) * 0.5;
101 | // }
102 | // self.minimumLineSpacing = self.factItemSpace;
103 | self.minimumLineSpacing = self.itemSpace_H - padding;
104 | }
105 | break;
106 |
107 | default:
108 | break;
109 | }
110 |
111 | }
112 |
113 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
114 |
115 | if (self.style == CWCarouselStyle_Unknow ||
116 | self.style == CWCarouselStyle_Normal ||
117 | self.style == CWCarouselStyle_H_1) {
118 |
119 | return [super layoutAttributesForElementsInRect:rect];
120 | }
121 |
122 | NSArray *arr = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES];
123 |
124 | CGFloat centerX = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.frame) * 0.5;
125 | __block CGFloat maxScale = 0;
126 | __block UICollectionViewLayoutAttributes *attri = nil;
127 |
128 | [arr enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
129 | CGFloat space = ABS(obj.center.x - centerX);
130 | space = MIN(space, self.itemWidth + self.factItemSpace);
131 | obj.zIndex = 0;
132 | if(space >= 0) {
133 | CGFloat scale = 1;
134 | if (self.style == CWCarouselStyle_H_2 ||
135 | self.style == CWCarouselStyle_H_3) {
136 | /**
137 | 公式: scale = k * space + a
138 | 其中: k = (minScale - 1) / (itemWidth + factItemSpace)
139 | 其中: a = 1
140 | 综上所述:
141 | scale = (minScale - 1) / (itemWitdh + factItemSpace) * space + 1
142 | */
143 | scale = (self.minScale - 1) / (self.itemWidth + self.factItemSpace) * space + 1;
144 | }else {
145 | // scale = -((self.maxScale - 1) / width) * space + self.maxScale;
146 | }
147 | obj.transform = CGAffineTransformMakeScale(scale, scale);
148 | if(maxScale < scale) {
149 | maxScale = scale;
150 | attri = obj;
151 | }
152 | }
153 | }];
154 |
155 | if (attri) {
156 | attri.zIndex = 1;
157 | }
158 |
159 | return arr;
160 | }
161 |
162 | - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
163 | return proposedContentOffset;
164 | }
165 | #pragma mark - Property
166 | - (CGFloat)defaultItemWidth {
167 | switch (self.style) {
168 | case CWCarouselStyle_Unknow:
169 | case CWCarouselStyle_Normal:
170 | return self.collectionView.frame.size.width;
171 | break;
172 | case CWCarouselStyle_H_1:
173 | case CWCarouselStyle_H_2:
174 | case CWCarouselStyle_H_3:
175 | return self.collectionView.frame.size.width * 0.75;
176 | break;
177 | default:
178 | break;
179 | }
180 | }
181 |
182 | - (void)setMaxScale:(CGFloat)maxScale {
183 | _maxScale = maxScale;
184 | if(maxScale < 1) {
185 | _maxScale = 1;
186 | }
187 | }
188 |
189 | - (void)setMinScale:(CGFloat)minScale {
190 | _minScale = minScale;
191 | if(minScale < 0) {
192 | _minScale = 0.1;
193 | }
194 | if (minScale >= 1) {
195 | _minScale = 1;
196 | }
197 | }
198 | @end
199 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/ViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.m
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import "ViewController.h"
10 | #import "CWCarousel.h"
11 | #import "CWPageControl.h"
12 |
13 | #define kViewTag 666
14 | #define kCount 0
15 |
16 | @interface ViewController ()
17 |
18 | @property (nonatomic, strong) CWCarousel *carousel;
19 | @property (nonatomic, assign) BOOL openCustomPageControl;
20 | @property (weak, nonatomic) IBOutlet UIView *contentView;
21 | @property (nonatomic, strong) NSArray *dataArr;
22 | @property (weak, nonatomic) IBOutlet UISegmentedControl *styleSegment;
23 | @property (weak, nonatomic) IBOutlet UISwitch *endlessSwitch;
24 | @property (weak, nonatomic) IBOutlet UISwitch *autoSwitch;
25 | @property (weak, nonatomic) IBOutlet UILabel *spaceLab;
26 | @property (weak, nonatomic) IBOutlet UIStepper *spaceSteper;
27 | @property (weak, nonatomic) IBOutlet UISwitch *cusPageControlSwitch;
28 | @end
29 |
30 | @implementation ViewController
31 | - (void)dealloc {
32 | NSLog(@"%s", __func__);
33 | }
34 |
35 | - (void)viewDidLoad {
36 | [super viewDidLoad];
37 | self.navigationItem.title = @"demo";
38 | [self configureUI:0];
39 | }
40 |
41 | - (void)configureUI:(NSInteger)tag {
42 | self.dataArr = nil;
43 | CATransition *tr = [CATransition animation];
44 | tr.type = @"cube";
45 | tr.subtype = kCATransitionFromRight;
46 | tr.duration = 0.25;
47 | [self.contentView.layer addAnimation:tr forKey:nil];
48 |
49 | self.view.backgroundColor = [UIColor whiteColor];
50 | if(self.carousel) {
51 | [self.carousel removeFromSuperview];
52 | self.carousel = nil;
53 | }
54 |
55 | CWFlowLayout *flowLayout = [[CWFlowLayout alloc] initWithStyle:[self styleFromTag:tag]];
56 | flowLayout.itemSpace_H = 0;
57 | self.spaceLab.text = @"0";
58 | self.spaceSteper.value = 0;
59 | if (flowLayout.style == CWCarouselStyle_H_3) {
60 | flowLayout.itemSpace_H = -10;
61 | self.spaceLab.text = @"-10";
62 | self.spaceSteper.value = -10;
63 | }
64 | // 使用layout创建视图(使用masonry 或者 系统api)
65 | CWCarousel *carousel = [[CWCarousel alloc] initWithFrame:CGRectZero
66 | delegate:self
67 | datasource:self
68 | flowLayout:flowLayout];
69 | self.carousel = carousel;
70 | if (self.cusPageControlSwitch.isOn) {
71 | [self setPageControl];
72 | }
73 | carousel.translatesAutoresizingMaskIntoConstraints = NO;
74 | [self.contentView addSubview:carousel];
75 | NSDictionary *dic = @{@"view" : carousel};
76 | [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|"
77 | options:kNilOptions
78 | metrics:nil
79 | views:dic]];
80 | [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view]-0-|"
81 | options:kNilOptions
82 | metrics:nil
83 | views:dic]];
84 | carousel.isAuto = self.autoSwitch.isOn;
85 | carousel.autoTimInterval = 2;
86 | carousel.endless = self.endlessSwitch.isOn;
87 | carousel.backgroundColor = [UIColor whiteColor];
88 | [carousel registerViewClass:[UICollectionViewCell class] identifier:@"cellId"];
89 | [self requestNetworkData];
90 | }
91 |
92 | - (void)viewWillAppear:(BOOL)animated {
93 | [super viewWillAppear:animated];
94 | [self.carousel controllerWillAppear];
95 | }
96 |
97 | - (void)viewWillDisappear:(BOOL)animated {
98 | [super viewWillDisappear:animated];
99 | [self.carousel controllerWillDisAppear];
100 | }
101 |
102 |
103 | #pragma mark - < 事件响应 >
104 | - (CWCarouselStyle)styleFromTag:(NSInteger)tag {
105 | switch (tag) {
106 | case 0:
107 | return CWCarouselStyle_Normal;
108 | break;
109 | case 1:
110 | return CWCarouselStyle_H_1;
111 | break;
112 | case 2:
113 | return CWCarouselStyle_H_2;
114 | break;
115 | case 3:
116 | return CWCarouselStyle_H_3;
117 | break;
118 | default:
119 | return CWCarouselStyle_Unknow;
120 | break;
121 | }
122 | }
123 |
124 | - (IBAction)styleChanged:(UISegmentedControl *)sender {
125 | [self configureUI:sender.selectedSegmentIndex];
126 | }
127 |
128 | - (IBAction)endlessChanged:(UISwitch *)sender {
129 | self.carousel.endless = sender.isOn;
130 | [self.carousel freshCarousel];
131 | }
132 |
133 | - (IBAction)autoChanged:(UISwitch *)sender {
134 | self.carousel.isAuto = sender.isOn;
135 | [self.carousel freshCarousel];
136 | }
137 |
138 | - (IBAction)spaceChanged:(UIStepper *)sender {
139 | self.carousel.flowLayout.itemSpace_H = sender.value;
140 | self.spaceLab.text = [NSString stringWithFormat:@"%.0f", sender.value];
141 | [self.carousel freshCarousel];
142 | }
143 |
144 | - (IBAction)customPageControlChanged:(UISwitch *)sender {
145 | if (sender.isOn) {
146 | [self setPageControl];
147 | }else {
148 | self.carousel.customPageControl = nil;
149 | }
150 | [self.carousel freshCarousel];
151 | }
152 |
153 | - (void)setPageControl {
154 | CGFloat width = [CWPageControl widthFromNumber:self.dataArr.count];
155 | CWPageControl *pageC = [[CWPageControl alloc] initWithFrame:CGRectMake(0, 0, width, 20)];
156 | pageC.translatesAutoresizingMaskIntoConstraints = NO;
157 | [[pageC.widthAnchor constraintEqualToConstant:width] setActive:YES];
158 | /**
159 | 这里我自己给了宽度, 所以就不再自定义布局代理:
160 | - (void)CWCarousel:(CWCarousel *)carousel addPageControl:(UIView *)pageControl;
161 | */
162 | self.carousel.customPageControl = pageC;
163 | }
164 |
165 | - (IBAction)buttonClick {
166 | [self.carousel scrollTo:2 animation:YES];
167 | }
168 |
169 | #pragma mark - 网络层
170 | - (void)requestNetworkData {
171 | // 模拟网络请求
172 | dispatch_async(dispatch_get_global_queue(0, 0), ^{
173 | NSMutableArray *arr = [NSMutableArray array];
174 | NSString *imgName = @"";
175 | for (int i = 0; i < 5; i++) {
176 | imgName = [NSString stringWithFormat:@"%02d.jpg", i + 1];
177 | [arr addObject:imgName];
178 | }
179 | self.dataArr = arr;
180 | dispatch_async(dispatch_get_main_queue(), ^{
181 | [self.carousel freshCarousel];
182 | });
183 | });
184 | }
185 |
186 | #pragma mark - Delegate
187 | - (NSInteger)numbersForCarousel {
188 | return self.dataArr.count;
189 | }
190 |
191 | - (UICollectionViewCell *)viewForCarousel:(CWCarousel *)carousel indexPath:(NSIndexPath *)indexPath index:(NSInteger)index{
192 | UICollectionViewCell *cell = [carousel.carouselView dequeueReusableCellWithReuseIdentifier:@"cellId" forIndexPath:indexPath];
193 | cell.contentView.backgroundColor = [UIColor cyanColor];
194 | UIImageView *imgView = [cell.contentView viewWithTag:kViewTag];
195 | if(!imgView) {
196 | imgView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
197 | imgView.tag = kViewTag;
198 | imgView.backgroundColor = [UIColor redColor];
199 | imgView.contentMode = UIViewContentModeScaleAspectFill;
200 | [cell.contentView addSubview:imgView];
201 | cell.layer.masksToBounds = YES;
202 | cell.layer.cornerRadius = 8;
203 | }
204 | // https://www.google.com/url?sa=i&rct=j&q=&esrc=s&source=images&cd=&cad=rja&uact=8&ved=2ahUKEwio8MyTp-DdAhWKM94KHUmEDcAQjRx6BAgBEAU&url=http%3A%2F%2F699pic.com%2Ftupian%2Fchuan.html&psig=AOvVaw20gpsPpW4JcNm0mJi9dYrb&ust=1538313533814128
205 |
206 | NSString *name = self.dataArr[index];
207 | UIImage *img = [UIImage imageNamed:name];
208 | if(!img) {
209 | NSLog(@"%@", name);
210 | }
211 | [imgView setImage:img];
212 | return cell;
213 | }
214 |
215 | - (void)CWCarousel:(CWCarousel *)carousel didSelectedAtIndex:(NSInteger)index {
216 | NSLog(@"did selected at index %ld", index);
217 | }
218 |
219 |
220 | - (void)CWCarousel:(CWCarousel *)carousel didStartScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow {
221 |
222 | }
223 |
224 |
225 | - (void)CWCarousel:(CWCarousel *)carousel didEndScrollAtIndex:(NSInteger)index indexPathRow:(NSInteger)indexPathRow {
226 |
227 | }
228 |
229 |
230 | #pragma mark - Setter && Getter
231 | - (void)didReceiveMemoryWarning {
232 | [super didReceiveMemoryWarning];
233 | }
234 | @end
235 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 48;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2826F4472101C3EE00BC32CB /* ShowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2826F4462101C3EE00BC32CB /* ShowViewController.swift */; };
11 | 2882BCDF20FC7B97006E8063 /* CWPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 2882BCDE20FC7B97006E8063 /* CWPageControl.m */; };
12 | 28D8861920F44EAE00BDED3D /* 05.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861420F44EAD00BDED3D /* 05.jpg */; };
13 | 28D8861A20F44EAE00BDED3D /* 03.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861520F44EAD00BDED3D /* 03.jpg */; };
14 | 28D8861B20F44EAE00BDED3D /* 04.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861620F44EAE00BDED3D /* 04.jpg */; };
15 | 28D8861C20F44EAE00BDED3D /* 02.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861720F44EAE00BDED3D /* 02.jpg */; };
16 | 28D8861D20F44EAE00BDED3D /* 01.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 28D8861820F44EAE00BDED3D /* 01.jpg */; };
17 | 6647FD98258E079A005A9547 /* CWBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6647FD96258E079A005A9547 /* CWBanner.swift */; };
18 | 6647FD99258E079A005A9547 /* CWSwiftFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */; };
19 | 6648C317272A719B00DD47AF /* CWProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6648C316272A719B00DD47AF /* CWProxy.swift */; };
20 | 665B49FF271028C8006872EA /* TestController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665B49FE271028C8006872EA /* TestController.swift */; };
21 | 666851A22073BBF900F91985 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851A12073BBF900F91985 /* AppDelegate.m */; };
22 | 666851A52073BBF900F91985 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851A42073BBF900F91985 /* ViewController.m */; };
23 | 666851A82073BBF900F91985 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666851A62073BBF900F91985 /* Main.storyboard */; };
24 | 666851AA2073BBF900F91985 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666851A92073BBF900F91985 /* Assets.xcassets */; };
25 | 666851AD2073BBF900F91985 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */; };
26 | 666851B02073BBF900F91985 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 666851AF2073BBF900F91985 /* main.m */; };
27 | 666851BA2073BC8B00F91985 /* SViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666851B92073BC8B00F91985 /* SViewController.swift */; };
28 | 6680CED625A3368000707014 /* CWFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 6680CED425A3368000707014 /* CWFlowLayout.m */; };
29 | 6680CED725A3368000707014 /* CWCarousel.m in Sources */ = {isa = PBXBuildFile; fileRef = 6680CED525A3368000707014 /* CWCarousel.m */; };
30 | /* End PBXBuildFile section */
31 |
32 | /* Begin PBXFileReference section */
33 | 2826F4462101C3EE00BC32CB /* ShowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowViewController.swift; sourceTree = ""; };
34 | 2882BCDD20FC7B97006E8063 /* CWPageControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CWPageControl.h; sourceTree = ""; };
35 | 2882BCDE20FC7B97006E8063 /* CWPageControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CWPageControl.m; sourceTree = ""; };
36 | 28D8861420F44EAD00BDED3D /* 05.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 05.jpg; sourceTree = ""; };
37 | 28D8861520F44EAD00BDED3D /* 03.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 03.jpg; sourceTree = ""; };
38 | 28D8861620F44EAE00BDED3D /* 04.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 04.jpg; sourceTree = ""; };
39 | 28D8861720F44EAE00BDED3D /* 02.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 02.jpg; sourceTree = ""; };
40 | 28D8861820F44EAE00BDED3D /* 01.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 01.jpg; sourceTree = ""; };
41 | 6647FD96258E079A005A9547 /* CWBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CWBanner.swift; path = ../../../Swift_CWCarousel/CWBanner.swift; sourceTree = ""; };
42 | 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CWSwiftFlowLayout.swift; path = ../../../Swift_CWCarousel/CWSwiftFlowLayout.swift; sourceTree = ""; };
43 | 6648C316272A719B00DD47AF /* CWProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CWProxy.swift; sourceTree = ""; };
44 | 665B49FE271028C8006872EA /* TestController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestController.swift; sourceTree = ""; };
45 | 6668519D2073BBF800F91985 /* CWCarousel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CWCarousel.app; sourceTree = BUILT_PRODUCTS_DIR; };
46 | 666851A02073BBF900F91985 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
47 | 666851A12073BBF900F91985 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
48 | 666851A32073BBF900F91985 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; };
49 | 666851A42073BBF900F91985 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; };
50 | 666851A72073BBF900F91985 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
51 | 666851A92073BBF900F91985 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
52 | 666851AC2073BBF900F91985 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
53 | 666851AE2073BBF900F91985 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
54 | 666851AF2073BBF900F91985 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
55 | 666851B82073BC8A00F91985 /* CWCarousel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CWCarousel-Bridging-Header.h"; sourceTree = ""; };
56 | 666851B92073BC8B00F91985 /* SViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SViewController.swift; sourceTree = ""; };
57 | 6680CED025A3368000707014 /* CWCarouselHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarouselHeader.h; path = ../../../OC_CWCarousel/CWCarouselHeader.h; sourceTree = ""; };
58 | 6680CED125A3368000707014 /* CWCarousel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarousel.h; path = ../../../OC_CWCarousel/CWCarousel.h; sourceTree = ""; };
59 | 6680CED225A3368000707014 /* CWCarouselProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWCarouselProtocol.h; path = ../../../OC_CWCarousel/CWCarouselProtocol.h; sourceTree = ""; };
60 | 6680CED325A3368000707014 /* CWFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CWFlowLayout.h; path = ../../../OC_CWCarousel/CWFlowLayout.h; sourceTree = ""; };
61 | 6680CED425A3368000707014 /* CWFlowLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CWFlowLayout.m; path = ../../../OC_CWCarousel/CWFlowLayout.m; sourceTree = ""; };
62 | 6680CED525A3368000707014 /* CWCarousel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CWCarousel.m; path = ../../../OC_CWCarousel/CWCarousel.m; sourceTree = ""; };
63 | /* End PBXFileReference section */
64 |
65 | /* Begin PBXFrameworksBuildPhase section */
66 | 6668519A2073BBF800F91985 /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | );
71 | runOnlyForDeploymentPostprocessing = 0;
72 | };
73 | /* End PBXFrameworksBuildPhase section */
74 |
75 | /* Begin PBXGroup section */
76 | 281EFA0D20FF39AD001859F3 /* Viewcontrollers */ = {
77 | isa = PBXGroup;
78 | children = (
79 | 666851A32073BBF900F91985 /* ViewController.h */,
80 | 666851A42073BBF900F91985 /* ViewController.m */,
81 | 666851B92073BC8B00F91985 /* SViewController.swift */,
82 | 2826F4462101C3EE00BC32CB /* ShowViewController.swift */,
83 | 665B49FE271028C8006872EA /* TestController.swift */,
84 | );
85 | name = Viewcontrollers;
86 | sourceTree = "";
87 | };
88 | 2882BCDC20FC7B74006E8063 /* code */ = {
89 | isa = PBXGroup;
90 | children = (
91 | 2882BCDD20FC7B97006E8063 /* CWPageControl.h */,
92 | 2882BCDE20FC7B97006E8063 /* CWPageControl.m */,
93 | );
94 | path = code;
95 | sourceTree = "";
96 | };
97 | 666851942073BBF800F91985 = {
98 | isa = PBXGroup;
99 | children = (
100 | 6668519F2073BBF800F91985 /* CWCarousel */,
101 | 6668519E2073BBF800F91985 /* Products */,
102 | );
103 | sourceTree = "";
104 | };
105 | 6668519E2073BBF800F91985 /* Products */ = {
106 | isa = PBXGroup;
107 | children = (
108 | 6668519D2073BBF800F91985 /* CWCarousel.app */,
109 | );
110 | name = Products;
111 | sourceTree = "";
112 | };
113 | 6668519F2073BBF800F91985 /* CWCarousel */ = {
114 | isa = PBXGroup;
115 | children = (
116 | 666851A02073BBF900F91985 /* AppDelegate.h */,
117 | 666851A12073BBF900F91985 /* AppDelegate.m */,
118 | 281EFA0D20FF39AD001859F3 /* Viewcontrollers */,
119 | 2882BCDC20FC7B74006E8063 /* code */,
120 | 66704EA7209B427300286A9B /* Sources */,
121 | 666851B72073BC0C00F91985 /* Swift */,
122 | 666851B62073BBFE00F91985 /* OC */,
123 | );
124 | path = CWCarousel;
125 | sourceTree = "";
126 | };
127 | 666851B62073BBFE00F91985 /* OC */ = {
128 | isa = PBXGroup;
129 | children = (
130 | 6680CED125A3368000707014 /* CWCarousel.h */,
131 | 6680CED525A3368000707014 /* CWCarousel.m */,
132 | 6680CED025A3368000707014 /* CWCarouselHeader.h */,
133 | 6680CED225A3368000707014 /* CWCarouselProtocol.h */,
134 | 6680CED325A3368000707014 /* CWFlowLayout.h */,
135 | 6680CED425A3368000707014 /* CWFlowLayout.m */,
136 | );
137 | path = OC;
138 | sourceTree = "";
139 | };
140 | 666851B72073BC0C00F91985 /* Swift */ = {
141 | isa = PBXGroup;
142 | children = (
143 | 6648C316272A719B00DD47AF /* CWProxy.swift */,
144 | 6647FD96258E079A005A9547 /* CWBanner.swift */,
145 | 6647FD97258E079A005A9547 /* CWSwiftFlowLayout.swift */,
146 | 666851B82073BC8A00F91985 /* CWCarousel-Bridging-Header.h */,
147 | );
148 | path = Swift;
149 | sourceTree = "";
150 | };
151 | 66704EA7209B427300286A9B /* Sources */ = {
152 | isa = PBXGroup;
153 | children = (
154 | 666851A62073BBF900F91985 /* Main.storyboard */,
155 | 666851A92073BBF900F91985 /* Assets.xcassets */,
156 | 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */,
157 | 666851AE2073BBF900F91985 /* Info.plist */,
158 | 666851AF2073BBF900F91985 /* main.m */,
159 | 28D8861820F44EAE00BDED3D /* 01.jpg */,
160 | 28D8861720F44EAE00BDED3D /* 02.jpg */,
161 | 28D8861520F44EAD00BDED3D /* 03.jpg */,
162 | 28D8861620F44EAE00BDED3D /* 04.jpg */,
163 | 28D8861420F44EAD00BDED3D /* 05.jpg */,
164 | );
165 | path = Sources;
166 | sourceTree = "";
167 | };
168 | /* End PBXGroup section */
169 |
170 | /* Begin PBXNativeTarget section */
171 | 6668519C2073BBF800F91985 /* CWCarousel */ = {
172 | isa = PBXNativeTarget;
173 | buildConfigurationList = 666851B32073BBF900F91985 /* Build configuration list for PBXNativeTarget "CWCarousel" */;
174 | buildPhases = (
175 | 666851992073BBF800F91985 /* Sources */,
176 | 6668519A2073BBF800F91985 /* Frameworks */,
177 | 6668519B2073BBF800F91985 /* Resources */,
178 | );
179 | buildRules = (
180 | );
181 | dependencies = (
182 | );
183 | name = CWCarousel;
184 | productName = CWCarousel;
185 | productReference = 6668519D2073BBF800F91985 /* CWCarousel.app */;
186 | productType = "com.apple.product-type.application";
187 | };
188 | /* End PBXNativeTarget section */
189 |
190 | /* Begin PBXProject section */
191 | 666851952073BBF800F91985 /* Project object */ = {
192 | isa = PBXProject;
193 | attributes = {
194 | LastUpgradeCheck = 0940;
195 | ORGANIZATIONNAME = ChenWang;
196 | TargetAttributes = {
197 | 6668519C2073BBF800F91985 = {
198 | CreatedOnToolsVersion = 9.0;
199 | LastSwiftMigration = 0900;
200 | ProvisioningStyle = Automatic;
201 | };
202 | };
203 | };
204 | buildConfigurationList = 666851982073BBF800F91985 /* Build configuration list for PBXProject "CWCarousel" */;
205 | compatibilityVersion = "Xcode 8.0";
206 | developmentRegion = en;
207 | hasScannedForEncodings = 0;
208 | knownRegions = (
209 | en,
210 | Base,
211 | );
212 | mainGroup = 666851942073BBF800F91985;
213 | productRefGroup = 6668519E2073BBF800F91985 /* Products */;
214 | projectDirPath = "";
215 | projectRoot = "";
216 | targets = (
217 | 6668519C2073BBF800F91985 /* CWCarousel */,
218 | );
219 | };
220 | /* End PBXProject section */
221 |
222 | /* Begin PBXResourcesBuildPhase section */
223 | 6668519B2073BBF800F91985 /* Resources */ = {
224 | isa = PBXResourcesBuildPhase;
225 | buildActionMask = 2147483647;
226 | files = (
227 | 28D8861A20F44EAE00BDED3D /* 03.jpg in Resources */,
228 | 666851AD2073BBF900F91985 /* LaunchScreen.storyboard in Resources */,
229 | 666851AA2073BBF900F91985 /* Assets.xcassets in Resources */,
230 | 28D8861C20F44EAE00BDED3D /* 02.jpg in Resources */,
231 | 28D8861920F44EAE00BDED3D /* 05.jpg in Resources */,
232 | 666851A82073BBF900F91985 /* Main.storyboard in Resources */,
233 | 28D8861D20F44EAE00BDED3D /* 01.jpg in Resources */,
234 | 28D8861B20F44EAE00BDED3D /* 04.jpg in Resources */,
235 | );
236 | runOnlyForDeploymentPostprocessing = 0;
237 | };
238 | /* End PBXResourcesBuildPhase section */
239 |
240 | /* Begin PBXSourcesBuildPhase section */
241 | 666851992073BBF800F91985 /* Sources */ = {
242 | isa = PBXSourcesBuildPhase;
243 | buildActionMask = 2147483647;
244 | files = (
245 | 6648C317272A719B00DD47AF /* CWProxy.swift in Sources */,
246 | 666851A52073BBF900F91985 /* ViewController.m in Sources */,
247 | 6680CED625A3368000707014 /* CWFlowLayout.m in Sources */,
248 | 666851B02073BBF900F91985 /* main.m in Sources */,
249 | 665B49FF271028C8006872EA /* TestController.swift in Sources */,
250 | 6647FD99258E079A005A9547 /* CWSwiftFlowLayout.swift in Sources */,
251 | 666851BA2073BC8B00F91985 /* SViewController.swift in Sources */,
252 | 2826F4472101C3EE00BC32CB /* ShowViewController.swift in Sources */,
253 | 2882BCDF20FC7B97006E8063 /* CWPageControl.m in Sources */,
254 | 666851A22073BBF900F91985 /* AppDelegate.m in Sources */,
255 | 6647FD98258E079A005A9547 /* CWBanner.swift in Sources */,
256 | 6680CED725A3368000707014 /* CWCarousel.m in Sources */,
257 | );
258 | runOnlyForDeploymentPostprocessing = 0;
259 | };
260 | /* End PBXSourcesBuildPhase section */
261 |
262 | /* Begin PBXVariantGroup section */
263 | 666851A62073BBF900F91985 /* Main.storyboard */ = {
264 | isa = PBXVariantGroup;
265 | children = (
266 | 666851A72073BBF900F91985 /* Base */,
267 | );
268 | name = Main.storyboard;
269 | sourceTree = "";
270 | };
271 | 666851AB2073BBF900F91985 /* LaunchScreen.storyboard */ = {
272 | isa = PBXVariantGroup;
273 | children = (
274 | 666851AC2073BBF900F91985 /* Base */,
275 | );
276 | name = LaunchScreen.storyboard;
277 | sourceTree = "";
278 | };
279 | /* End PBXVariantGroup section */
280 |
281 | /* Begin XCBuildConfiguration section */
282 | 666851B12073BBF900F91985 /* Debug */ = {
283 | isa = XCBuildConfiguration;
284 | buildSettings = {
285 | ALWAYS_SEARCH_USER_PATHS = NO;
286 | CLANG_ANALYZER_NONNULL = YES;
287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
289 | CLANG_CXX_LIBRARY = "libc++";
290 | CLANG_ENABLE_MODULES = YES;
291 | CLANG_ENABLE_OBJC_ARC = YES;
292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
293 | CLANG_WARN_BOOL_CONVERSION = YES;
294 | CLANG_WARN_COMMA = YES;
295 | CLANG_WARN_CONSTANT_CONVERSION = YES;
296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
299 | CLANG_WARN_EMPTY_BODY = YES;
300 | CLANG_WARN_ENUM_CONVERSION = YES;
301 | CLANG_WARN_INFINITE_RECURSION = YES;
302 | CLANG_WARN_INT_CONVERSION = YES;
303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
308 | CLANG_WARN_STRICT_PROTOTYPES = YES;
309 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
311 | CLANG_WARN_UNREACHABLE_CODE = YES;
312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
313 | CODE_SIGN_IDENTITY = "iPhone Developer";
314 | COPY_PHASE_STRIP = NO;
315 | DEBUG_INFORMATION_FORMAT = dwarf;
316 | ENABLE_STRICT_OBJC_MSGSEND = YES;
317 | ENABLE_TESTABILITY = YES;
318 | GCC_C_LANGUAGE_STANDARD = gnu11;
319 | GCC_DYNAMIC_NO_PIC = NO;
320 | GCC_NO_COMMON_BLOCKS = YES;
321 | GCC_OPTIMIZATION_LEVEL = 0;
322 | GCC_PREPROCESSOR_DEFINITIONS = (
323 | "DEBUG=1",
324 | "$(inherited)",
325 | );
326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
328 | GCC_WARN_UNDECLARED_SELECTOR = YES;
329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
330 | GCC_WARN_UNUSED_FUNCTION = YES;
331 | GCC_WARN_UNUSED_VARIABLE = YES;
332 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
333 | MTL_ENABLE_DEBUG_INFO = YES;
334 | ONLY_ACTIVE_ARCH = YES;
335 | SDKROOT = iphoneos;
336 | };
337 | name = Debug;
338 | };
339 | 666851B22073BBF900F91985 /* Release */ = {
340 | isa = XCBuildConfiguration;
341 | buildSettings = {
342 | ALWAYS_SEARCH_USER_PATHS = NO;
343 | CLANG_ANALYZER_NONNULL = YES;
344 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
345 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
346 | CLANG_CXX_LIBRARY = "libc++";
347 | CLANG_ENABLE_MODULES = YES;
348 | CLANG_ENABLE_OBJC_ARC = YES;
349 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
350 | CLANG_WARN_BOOL_CONVERSION = YES;
351 | CLANG_WARN_COMMA = YES;
352 | CLANG_WARN_CONSTANT_CONVERSION = YES;
353 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
354 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
355 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
356 | CLANG_WARN_EMPTY_BODY = YES;
357 | CLANG_WARN_ENUM_CONVERSION = YES;
358 | CLANG_WARN_INFINITE_RECURSION = YES;
359 | CLANG_WARN_INT_CONVERSION = YES;
360 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
361 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
362 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
363 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
364 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
365 | CLANG_WARN_STRICT_PROTOTYPES = YES;
366 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
367 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
368 | CLANG_WARN_UNREACHABLE_CODE = YES;
369 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
370 | CODE_SIGN_IDENTITY = "iPhone Developer";
371 | COPY_PHASE_STRIP = NO;
372 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
373 | ENABLE_NS_ASSERTIONS = NO;
374 | ENABLE_STRICT_OBJC_MSGSEND = YES;
375 | GCC_C_LANGUAGE_STANDARD = gnu11;
376 | GCC_NO_COMMON_BLOCKS = YES;
377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
379 | GCC_WARN_UNDECLARED_SELECTOR = YES;
380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
381 | GCC_WARN_UNUSED_FUNCTION = YES;
382 | GCC_WARN_UNUSED_VARIABLE = YES;
383 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
384 | MTL_ENABLE_DEBUG_INFO = NO;
385 | SDKROOT = iphoneos;
386 | SWIFT_COMPILATION_MODE = wholemodule;
387 | VALIDATE_PRODUCT = YES;
388 | };
389 | name = Release;
390 | };
391 | 666851B42073BBF900F91985 /* Debug */ = {
392 | isa = XCBuildConfiguration;
393 | buildSettings = {
394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
395 | CLANG_ENABLE_MODULES = YES;
396 | CODE_SIGN_STYLE = Automatic;
397 | CURRENT_PROJECT_VERSION = 1.1.8;
398 | DEVELOPMENT_TEAM = V3DM7W7CZT;
399 | INFOPLIST_FILE = "$(SRCROOT)/CWCarousel/Sources/Info.plist";
400 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
401 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
402 | MARKETING_VERSION = 1.1.8;
403 | PRODUCT_BUNDLE_IDENTIFIER = "com.chenwang.CWCarousel-01";
404 | PRODUCT_NAME = "$(TARGET_NAME)";
405 | SWIFT_OBJC_BRIDGING_HEADER = "CWCarousel/Swift/CWCarousel-Bridging-Header.h";
406 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
407 | SWIFT_VERSION = 5.0;
408 | TARGETED_DEVICE_FAMILY = "1,2";
409 | };
410 | name = Debug;
411 | };
412 | 666851B52073BBF900F91985 /* Release */ = {
413 | isa = XCBuildConfiguration;
414 | buildSettings = {
415 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
416 | CLANG_ENABLE_MODULES = YES;
417 | CODE_SIGN_STYLE = Automatic;
418 | CURRENT_PROJECT_VERSION = 1.1.8;
419 | DEVELOPMENT_TEAM = V3DM7W7CZT;
420 | INFOPLIST_FILE = "$(SRCROOT)/CWCarousel/Sources/Info.plist";
421 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
423 | MARKETING_VERSION = 1.1.8;
424 | PRODUCT_BUNDLE_IDENTIFIER = "com.chenwang.CWCarousel-01";
425 | PRODUCT_NAME = "$(TARGET_NAME)";
426 | SWIFT_OBJC_BRIDGING_HEADER = "CWCarousel/Swift/CWCarousel-Bridging-Header.h";
427 | SWIFT_VERSION = 5.0;
428 | TARGETED_DEVICE_FAMILY = "1,2";
429 | };
430 | name = Release;
431 | };
432 | /* End XCBuildConfiguration section */
433 |
434 | /* Begin XCConfigurationList section */
435 | 666851982073BBF800F91985 /* Build configuration list for PBXProject "CWCarousel" */ = {
436 | isa = XCConfigurationList;
437 | buildConfigurations = (
438 | 666851B12073BBF900F91985 /* Debug */,
439 | 666851B22073BBF900F91985 /* Release */,
440 | );
441 | defaultConfigurationIsVisible = 0;
442 | defaultConfigurationName = Release;
443 | };
444 | 666851B32073BBF900F91985 /* Build configuration list for PBXNativeTarget "CWCarousel" */ = {
445 | isa = XCConfigurationList;
446 | buildConfigurations = (
447 | 666851B42073BBF900F91985 /* Debug */,
448 | 666851B52073BBF900F91985 /* Release */,
449 | );
450 | defaultConfigurationIsVisible = 0;
451 | defaultConfigurationName = Release;
452 | };
453 | /* End XCConfigurationList section */
454 | };
455 | rootObject = 666851952073BBF800F91985 /* Project object */;
456 | }
457 |
--------------------------------------------------------------------------------
/Swift_CWCarousel/CWBanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CWBanner.swift
3 | // CWCarousel
4 | //
5 | // Created by chenwang on 2018/7/18.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CWCollectionView: UICollectionView, UIGestureRecognizerDelegate {
12 |
13 | override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
14 | super.init(frame: frame, collectionViewLayout: layout)
15 | self.panGestureRecognizer.delegate = self
16 | }
17 |
18 | required init?(coder: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
23 | return true
24 | }
25 | }
26 |
27 | protocol CWBannerDelegate: AnyObject {
28 | /// 视图数量
29 | func bannerNumbers() -> Int
30 |
31 | /// 加载cell
32 | /// - Parameters:
33 | /// - banner: 控件对象
34 | /// - index: 当前需要加载index
35 | /// - indexPath: 当前需要加载indexPath
36 | func bannerView(banner: CWBanner, index: Int, indexPath: IndexPath) -> UICollectionViewCell
37 |
38 | /// 点击
39 | /// - Parameters:
40 | /// - banner: 控件对象
41 | /// - index: 触发点击事件的index
42 | /// - indexPath: 触发点击事件的indexPath
43 | func didSelected(banner: CWBanner, index: Int, indexPath: IndexPath)
44 |
45 | /// 开始滚动
46 | /// - Parameters:
47 | /// - banner: 控件对象
48 | /// - index: 开始时的index
49 | /// - indexPath: 开始时的indexPath
50 | func didStartScroll(banner: CWBanner, index: Int, indexPath: IndexPath)
51 |
52 | /// 结束滚动
53 | /// - Parameters:
54 | /// - banner: 控件对象
55 | /// - index: 结束后居中的index
56 | /// - indexPath: 居中后结束的indexPath
57 | func didEndScroll(banner: CWBanner, index: Int, indexPath: IndexPath)
58 | }
59 |
60 | protocol CWBannerPageControl where Self: UIView {
61 | /// 当前下标
62 | var currentPage: Int? {set get}
63 | /// 总数
64 | var numberOfPages: Int? {get set}
65 | /// 自定义pageControl被添加到控件上后会调此方法, 在这里可以设置pageControl的布局
66 | func setupLayoutWhenMoveToBannerView(_ banner: CWBanner) -> Void
67 | }
68 |
69 | class CWBanner: UIView {
70 | //MARK: - 构造方法
71 | init(frame: CGRect, flowLayout: CWSwiftFlowLayout, delegate: CWBannerDelegate) {
72 | self.flowLayout = flowLayout
73 | self.delegate = delegate
74 | super.init(frame: frame)
75 | self.configureBanner()
76 | }
77 |
78 | override func layoutSubviews() {
79 | super.layoutSubviews()
80 | self.resumePlay()
81 | }
82 |
83 | deinit {
84 | NSLog("[%@ -- %@]",NSStringFromClass(self.classForCoder), #function);
85 | self.releaseTimer()
86 | NotificationCenter.default.removeObserver(self)
87 | }
88 |
89 | required init?(coder aDecoder: NSCoder) {
90 | self.flowLayout = CWSwiftFlowLayout.init(style: .unknown)
91 | super.init(coder: aDecoder)
92 | }
93 |
94 | //MARK: - Property
95 | fileprivate lazy var proxy: CWProxy = {
96 | let p = CWProxy(self)
97 | return p
98 | }()
99 | /// 是否展示PageControl 默认:true
100 | var showPageControl = true
101 | /// 没有数据时的占位图
102 | let emptyImgView = UIImageView.init(frame: CGRect.zero)
103 | /// 自定义layout
104 | let flowLayout: CWSwiftFlowLayout
105 | /// collectionView
106 | lazy var banner: UICollectionView = {
107 | let b = CWCollectionView.init(frame: CGRect.zero, collectionViewLayout: self.flowLayout)
108 | b.translatesAutoresizingMaskIntoConstraints = false
109 | self.addSubview(b)
110 | self.sendSubviewToBack(b)
111 | b.delegate = self
112 | b.dataSource = self
113 | b.showsHorizontalScrollIndicator = false
114 | b.decelerationRate = UIScrollView.DecelerationRate(rawValue: 0)
115 | b.backgroundColor = self.backgroundColor
116 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]-0-|",
117 | options: [],
118 | metrics: nil,
119 | views: ["view" : b]))
120 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]-0-|",
121 | options: [],
122 | metrics: nil,
123 | views: ["view" : b]))
124 | b.register(UICollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "tempCell")
125 | self.addSubview(self.emptyImgView)
126 | self.emptyImgView.translatesAutoresizingMaskIntoConstraints = false
127 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[empty]|", options: [], metrics: nil, views: ["empty":emptyImgView]))
128 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[empty]|", options: [], metrics: nil, views: ["empty":emptyImgView]))
129 | return b
130 | }()
131 | /// 外部代理委托
132 | weak var delegate: CWBannerDelegate?
133 | /// 当前居中展示的cell的下标
134 | var currentIndexPath: IndexPath = IndexPath.init(row: 0, section: 0) {
135 | didSet {
136 | let current = self.caculateIndex(indexPath: self.currentIndexPath)
137 | self.currentIndex = current
138 | if self.customPageControl == nil {
139 | self.pageControl.currentPage = current
140 | return
141 | }
142 | self.customPageControl?.currentPage = current
143 | }
144 | }
145 | fileprivate var currentIndex: Int = 0
146 | /// 是否开启自动滚动 (默认是关闭的)
147 | var autoPlay = false
148 | /// 定时器
149 | var timer: Timer?
150 | /// 自动滚动时间间隔,默认3s
151 | var timeInterval: TimeInterval = 3.0
152 | /// 默认的pageControl (默认位置在中下方,需要调整位置请自行设置frame)
153 | lazy var pageControl: UIPageControl = {
154 | let count = self.delegate?.bannerNumbers()
155 | let width = CGFloat(5) * CGFloat((count ?? 0))
156 | let height: CGFloat = 10
157 | let pageControl = UIPageControl.init(frame: CGRect.init(x: 0, y: 0, width: width, height: height))
158 | pageControl.center = CGPoint.init(x: self.bounds.width * 0.5, y: self.bounds.height - height * 0.5 - 8)
159 | pageControl.currentPage = 0
160 | pageControl.numberOfPages = self.delegate?.bannerNumbers() ?? 0
161 | pageControl.pageIndicatorTintColor = UIColor.white
162 | pageControl.isUserInteractionEnabled = false;
163 | pageControl.currentPageIndicatorTintColor = UIColor.black
164 | pageControl.translatesAutoresizingMaskIntoConstraints = false;
165 | return pageControl
166 | }()
167 | /// 自定义的pageControl
168 | var customPageControl: CWBannerPageControl? {
169 | willSet
170 | {
171 | guard let custom = newValue else {return}
172 | guard custom.superview == nil else {return}
173 | self.addSubview(custom);
174 | self.bringSubviewToFront(custom);
175 | self.pageControl.removeFromSuperview();
176 | newValue?.setupLayoutWhenMoveToBannerView(self)
177 | newValue?.numberOfPages = self.numbers
178 | newValue?.currentPage = self.currentIndex
179 | }
180 | }
181 |
182 | /// 控件版本号
183 | var version: String {
184 | get{
185 | return "1.1.9";
186 | }
187 | }
188 |
189 | /// 是否无限轮播 true:无限衔接下去; false: 到最后一张后就没有了
190 | var endless: Bool = true
191 | }
192 |
193 | // MARK: - OPEN METHOD
194 | extension CWBanner {
195 | /// 刷新数据
196 | func freshBanner() {
197 | self.stop()
198 | self.banner.reloadData()
199 | self.banner.layoutIfNeeded()
200 | self.scrollToIndexPathNoAnimated(self.originIndexPath())
201 | self.play()
202 | if self.customPageControl != nil {
203 | self.customPageControl?.numberOfPages = self.delegate?.bannerNumbers() ?? 0
204 | self.customPageControl?.currentPage = 0
205 | return
206 | }
207 | self.pageControl.numberOfPages = self.delegate?.bannerNumbers() ?? 0
208 | self.pageControl.currentPage = 0
209 | }
210 |
211 | fileprivate func play() {
212 | guard self.numbers > 1 && self.autoPlay else {
213 | self.releaseTimer()
214 | return
215 | }
216 |
217 | guard self.timer == nil else {
218 | return
219 | }
220 |
221 | defer { self.timer?.fireDate = Date.init(timeIntervalSinceNow: self.timeInterval) }
222 |
223 | if #available(iOS 10.0, *) {
224 | self.timer = Timer.scheduledTimer(withTimeInterval: self.timeInterval, repeats: true, block: { [weak self] (timer) in
225 | self?.nextCell()
226 | })
227 | return
228 | }
229 |
230 | self.timer = self.proxy.createTimer(timeInterval: self.timeInterval, { t in
231 | t?.nextCell()
232 | })
233 | }
234 |
235 | @objc fileprivate func nextCell() {
236 |
237 | defer {
238 | self.scrollViewWillBeginDecelerating(self.banner)
239 | }
240 |
241 | if self.endless {
242 | self.currentIndexPath = self.currentIndexPath + 1;
243 | return
244 | }
245 |
246 | let lastIndex = self.flowLayout.style == .normal ? self.numbers - 1 : self.factNumbers - 2
247 |
248 | if self.currentIndexPath.row == lastIndex {
249 | let row = self.flowLayout.style == .normal ? 0 : 1
250 | self.currentIndexPath = IndexPath.init(row: row, section: 0)
251 | return
252 | }
253 | self.currentIndexPath = self.currentIndexPath + 1
254 | }
255 |
256 | /// 继续滚动轮播图
257 | func resumePlay() {
258 | self.play()
259 | }
260 |
261 | /// 暂停自动滚动
262 | func pause() {
263 | if let timer = self.timer {
264 | timer.fireDate = Date.distantFuture
265 | }
266 | }
267 |
268 | /// 停止滚动
269 | func stop() {
270 | self.pause()
271 | }
272 |
273 | /// 释放timer资源,防止内存泄漏
274 | fileprivate func releaseTimer() {
275 | guard let timer = self.timer else {
276 | return
277 | }
278 | timer.fireDate = Date.distantFuture
279 | timer.invalidate()
280 | self.timer = nil
281 | }
282 |
283 | /// banner所处控制器WillAppear方法中调用
284 | func bannerWillAppear() {
285 | self.play()
286 | self.adjustErrorCell(isScroll: true, animation: false)
287 | }
288 |
289 | /// banner所处控制器WillDisAppear方法中调用
290 | func bannerWillDisAppear() {
291 | self.pause()
292 | self.adjustErrorCell(isScroll: true, animation: false)
293 | }
294 |
295 | func scroll(to index: Int, animated: Bool = true) {
296 | let numbers = self.numbers
297 |
298 | guard numbers >= 0 && index < numbers else {
299 | return
300 | }
301 |
302 | guard index != self.currentIndex else {
303 | return
304 | }
305 |
306 | self.currentIndexPath = self.currentIndexPath + (index - self.currentIndex)
307 | if (animated) {
308 | self.scrollToIndexPathAnimated(self.currentIndexPath)
309 | return
310 | }
311 | self.scrollToIndexPathNoAnimated(self.currentIndexPath)
312 | }
313 | }
314 |
315 | // MARK: - LOGIC HELPER
316 | extension CWBanner {
317 | /// 代码层下标换算成业务层下标
318 | ///
319 | /// - Parameter IndexPath: 代码层cell对应的下标
320 | /// - Returns: 业务层对应的下标
321 | fileprivate func caculateIndex(indexPath: IndexPath) -> Int {
322 | guard self.numbers > 0 else {
323 | return 0
324 | }
325 | var row = indexPath.row % self.numbers
326 | if self.endless == false, self.flowLayout.style != .normal {
327 | row = indexPath.row % self.factNumbers - 1
328 | }
329 | return row
330 | }
331 |
332 | /// 第一次加载时,会从中间开始展示
333 | ///
334 | /// - Returns: 返回对应的indexPath
335 | fileprivate func originIndexPath() -> IndexPath {
336 |
337 | let numbers = self.numbers
338 |
339 | guard numbers > 0 else {
340 | return IndexPath.init(index: 0)
341 | }
342 |
343 | guard endless else {
344 | let row = self.flowLayout.style == .normal ? 0 : 1
345 | self.currentIndexPath = IndexPath.init(row: row, section: 0)
346 | return self.currentIndexPath
347 | }
348 |
349 | let centerIndex = self.factNumbers / numbers
350 | self.currentIndexPath = centerIndex == 1 ? IndexPath.init(row: 0, section: 0) : IndexPath.init(row: centerIndex / 2 * numbers, section: 0)
351 |
352 | return self.currentIndexPath
353 | }
354 |
355 | /// 边缘检测, 如果将要滑到边缘,调整位置
356 | fileprivate func checkOutOfBounds() {
357 | let row = self.currentIndexPath.row
358 | if row == self.factNumbers - 2
359 | || row == 1 {
360 | let originIndexPath = self.originIndexPath()
361 | var index = self.caculateIndex(indexPath: self.currentIndexPath)
362 | index = row == 1 ? index + 1 : index - 2
363 | self.currentIndexPath = originIndexPath + index
364 | self.scrollToIndexPathNoAnimated(self.currentIndexPath)
365 | }
366 | }
367 |
368 | fileprivate func scrollToIndexPathAnimated(_ indexPath: IndexPath) {
369 | guard self.numbers > 0 else {
370 | return
371 | }
372 | self.banner.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
373 | }
374 |
375 | fileprivate func scrollToIndexPathNoAnimated(_ indexPath: IndexPath) {
376 | guard self.numbers > 0 else {
377 | return
378 | }
379 | self.banner.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
380 | self.delegate?.didEndScroll(banner: self, index: self.currentIndex, indexPath: self.currentIndexPath)
381 | }
382 |
383 | /// cell错位检测和调整
384 | func adjustErrorCell(isScroll: Bool, animation: Bool = true) {
385 | let indexPaths = self.banner.indexPathsForVisibleItems
386 | let centerX: CGFloat = self.banner.contentOffset.x + self.banner.frame.width * 0.5
387 | var minSpace = CGFloat(MAXFLOAT)
388 | var newIndexPath: IndexPath = self.currentIndexPath
389 | indexPaths.forEach({ (idx) in
390 | if let atr = self.banner.layoutAttributesForItem(at: idx) {
391 | atr.zIndex = 0
392 | let diff = abs(atr.center.x - centerX)
393 | if abs(minSpace) > diff {
394 | minSpace = diff
395 | newIndexPath = atr.indexPath
396 | }
397 | }
398 | })
399 | /// 正常情况下 newIndexPath 和 currentIndexPath 不会相差超过 2, 如果超过4明显不正常,就不做处理
400 | if abs(newIndexPath.row - self.currentIndexPath.row) < 4 {
401 | self.currentIndexPath = newIndexPath
402 | }
403 |
404 | if isScroll {
405 | self.cw_scrollViewWillBeginDecelerating(self.banner, animation: animation)
406 | }
407 | }
408 |
409 | @objc fileprivate func appActive(_ notify: Notification) {
410 | self.adjustErrorCell(isScroll: true, animation: false)
411 | self.play()
412 | }
413 |
414 | @objc fileprivate func appInactive(_ notify: Notification) {
415 | self.pause()
416 | self.adjustErrorCell(isScroll: true, animation: false)
417 | }
418 | }
419 |
420 | // MARK: - UI
421 | extension CWBanner {
422 | fileprivate func configureBanner() {
423 |
424 | if self.customPageControl == nil {
425 | self.addSubview(self.pageControl)
426 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[control]-0-|",
427 | options: [],
428 | metrics: nil,
429 | views: ["control" : self.pageControl]))
430 | self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[control(30)]-0-|",
431 | options: [],
432 | metrics: nil,
433 | views: ["control" : self.pageControl]))
434 | }else {
435 | self.addSubview(self.customPageControl!)
436 |
437 | }
438 | NotificationCenter.default.addObserver(self,
439 | selector: #selector(appActive(_:)), name: UIApplication.didBecomeActiveNotification ,
440 | object:nil)
441 |
442 | NotificationCenter.default.addObserver(self,
443 | selector: #selector(appInactive(_:)),
444 | name: UIApplication.willResignActiveNotification,
445 | object: nil)
446 | }
447 | }
448 |
449 | // MARK: - UIScrollViewDelegate
450 | extension CWBanner {
451 |
452 | /// 开始拖拽
453 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
454 |
455 | if #available(iOS 14.0, *) {
456 | self.banner.isPagingEnabled = false
457 | } else {
458 | self.banner.isPagingEnabled = true
459 | }
460 |
461 | self.pause()
462 | self.delegate?.didStartScroll(banner: self,
463 | index: self.currentIndex,
464 | indexPath: self.currentIndexPath)
465 | }
466 |
467 | /// 将要结束拖拽
468 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
469 |
470 | if (self.endless == false) {
471 |
472 | var maxIndex = self.numbers - 1
473 | var minIndex = 0
474 |
475 | if self.flowLayout.style != .normal {
476 | maxIndex = self.factNumbers - 2
477 | minIndex = 1
478 | }
479 |
480 | if velocity.x >= 0 && self.currentIndexPath.row == maxIndex {
481 | self.adjustErrorCell(isScroll: true)
482 | return
483 | }
484 |
485 | if velocity.x <= 0 && self.currentIndexPath.row == minIndex {
486 | self.adjustErrorCell(isScroll: true)
487 | return
488 | }
489 | }
490 |
491 | // 这里不用考虑越界问题,其他地方做了保护
492 | if velocity.x > 0 {
493 | //左滑,下一张
494 | self.currentIndexPath = self.currentIndexPath + 1
495 | }else if velocity.x < 0 {
496 | //右滑, 上一张
497 | self.currentIndexPath = self.currentIndexPath - 1
498 | }else if velocity.x == 0 {
499 | self.adjustErrorCell(isScroll: false)
500 | if #available(iOS 14.0, *) {
501 | self.scrollViewWillBeginDecelerating(self.banner);
502 | }
503 | }
504 | }
505 |
506 | /// 将要开始减速
507 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
508 | self.cw_scrollViewWillBeginDecelerating(scrollView, animation: true)
509 | }
510 |
511 | func cw_scrollViewWillBeginDecelerating(_ scrollView: UIScrollView, animation: Bool = true) {
512 |
513 | let factNumbers = self.factNumbers
514 |
515 | guard self.currentIndexPath.row >= 0,
516 | self.currentIndexPath.row < factNumbers else {
517 | // 越界保护
518 | return
519 | }
520 |
521 | if self.endless == false {
522 | if self.currentIndexPath.row == 0 && self.flowLayout.style != .normal {
523 | self.currentIndexPath = IndexPath.init(row: 1, section: 0)
524 | } else if self.currentIndexPath.row == factNumbers - 1 && self.flowLayout.style != .normal {
525 | self.currentIndexPath = IndexPath.init(row: factNumbers - 2, section: 0)
526 | }
527 | }
528 |
529 | // 在这里将需要显示的cell置为居中
530 | if animation {
531 | self.scrollToIndexPathAnimated(self.currentIndexPath)
532 | return
533 | }
534 |
535 | self.scrollToIndexPathNoAnimated(self.currentIndexPath)
536 | }
537 |
538 | /// 结束减速
539 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
540 | self.banner.isPagingEnabled = false
541 | }
542 |
543 | /// 滚动动画完成
544 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
545 | self.banner.isPagingEnabled = false
546 | if self.endless {
547 | self.checkOutOfBounds()
548 | }
549 | self.resumePlay()
550 | self.delegate?.didEndScroll(banner: self,
551 | index: self.caculateIndex(indexPath: self.currentIndexPath),
552 | indexPath: self.currentIndexPath)
553 | }
554 |
555 | /// 滚动中
556 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
557 |
558 | }
559 |
560 | }
561 |
562 | // MARK: - UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
563 | extension CWBanner: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
564 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
565 |
566 | func dequeueCell(_ idxPath: IndexPath) -> UICollectionViewCell {
567 | return self.delegate?.bannerView(banner: self,
568 | index: self.caculateIndex(indexPath: idxPath),
569 | indexPath: idxPath) ?? UICollectionViewCell()
570 | }
571 |
572 | if self.endless == false,
573 | self.flowLayout.style != .normal,
574 | indexPath.row == 0 || indexPath.row == self.factNumbers - 1 {
575 |
576 | return collectionView.dequeueReusableCell(withReuseIdentifier: "tempCell", for: indexPath)
577 | }
578 |
579 | return dequeueCell(indexPath)
580 | }
581 |
582 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
583 | return self.factNumbers
584 | }
585 |
586 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
587 |
588 | if self.endless == false,
589 | self.flowLayout.style != .normal,
590 | indexPath.row == 0 || indexPath.row == self.factNumbers - 1 {
591 | // 非无限轮播情况下, 第一个和最后一个是占位cell, 不参与外部的业务逻辑
592 | return
593 | }
594 |
595 | self.delegate?.didSelected(banner: self,
596 | index: self.caculateIndex(indexPath: indexPath),
597 | indexPath: indexPath)
598 | // 处于动画中时,点击cell,可能会出现cell不居中问题.这里处理下
599 | // 将里中心点最近的那个cell居
600 | self.adjustErrorCell(isScroll: true)
601 | }
602 | }
603 |
604 | // MARK: - Category
605 | extension CWBanner {
606 | /// 背地里实际返回的cell个数
607 | fileprivate var factNumbers: Int {
608 |
609 | let numbers = self.numbers
610 |
611 | guard numbers > 0 else {
612 | return 0;
613 | }
614 |
615 | func setPagecontrol(isHidden: Bool) {
616 | self.pageControl.isHidden = isHidden
617 | self.customPageControl?.isHidden = isHidden
618 | }
619 |
620 | self.banner.isScrollEnabled = numbers > 1
621 | setPagecontrol(isHidden: !self.showPageControl || numbers == 1)
622 |
623 | if endless {
624 | return numbers == 1 ? numbers : 100
625 | }
626 |
627 | return self.flowLayout.style == .normal ? numbers : numbers + 2
628 | }
629 |
630 | /// 业务层实际需要展示的cell个数
631 | fileprivate var numbers: Int {
632 | let count = self.delegate?.bannerNumbers() ?? 0
633 | self.emptyImgView.isHidden = count <= 0
634 | return count > 0 ? count : 0
635 | }
636 | }
637 |
638 | extension IndexPath {
639 | /// 重载 + 号运算符
640 | static func + (left: IndexPath, right: Int) -> IndexPath {
641 | return IndexPath.init(row: left.row + right, section: left.section)
642 | }
643 |
644 | /// 重载 - 号运算符
645 | static func - (left: IndexPath, right: Int) -> IndexPath {
646 | return IndexPath.init(row: left.row - right, section: left.section)
647 | }
648 | }
649 |
--------------------------------------------------------------------------------
/OC_CWCarousel/CWCarousel.m:
--------------------------------------------------------------------------------
1 | //
2 | // CWCarousel.m
3 | // CWCarousel
4 | //
5 | // Created by WangChen on 2018/4/3.
6 | // Copyright © 2018年 ChenWang. All rights reserved.
7 | //
8 |
9 | #import "CWCarousel.h"
10 |
11 | @interface CWCarouselCollectionView()
12 | @property (nonatomic, copy) void (^ _Nullable tapCallback) (void);
13 | @end
14 |
15 |
16 | @implementation CWCarouselCollectionView
17 | - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {
18 | if(self = [super initWithFrame:frame collectionViewLayout:layout]) {
19 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
20 | [self addGestureRecognizer:tap];
21 | tap.delegate = self;
22 | }
23 | return self;
24 | }
25 |
26 | - (void)tapAction:(UITapGestureRecognizer *)tap {
27 | if (!self.tapCallback) {
28 | return;
29 | }
30 | self.tapCallback();
31 | }
32 |
33 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
34 |
35 | if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] == NO) {
36 | return YES;
37 | }
38 |
39 | if ([[touch view] isKindOfClass:[UICollectionView class]]) {
40 | return YES;
41 | }
42 |
43 | return NO;
44 | }
45 |
46 | - (void)dealloc {
47 | NSLog(@"[CWCarouselCollectionView dealloc]");
48 | }
49 | @end
50 |
51 |
52 | @interface CWTempleteCell: UICollectionViewCell
53 | @end
54 |
55 | @implementation CWTempleteCell
56 | @end
57 |
58 |
59 | @interface MyProxy : NSProxy
60 | @property (nonatomic, weak) id _Nullable target;
61 | - (instancetype)init:(id)target;
62 | @end
63 |
64 | @implementation MyProxy
65 | - (instancetype)init:(id)target {
66 | self.target = target;
67 | return self;
68 | }
69 |
70 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
71 | return [self.target methodSignatureForSelector:sel];
72 | }
73 |
74 | - (void)forwardInvocation:(NSInvocation *)invocation {
75 | [invocation invokeWithTarget:self.target];
76 | }
77 |
78 | @end
79 |
80 |
81 | @interface CWCarousel () {
82 |
83 | }
84 | /// collectionView
85 | @property (nonatomic, strong) CWCarouselCollectionView *carouselView;
86 | /// 轮播总数
87 | @property (nonatomic, assign) NSInteger numbers;
88 | /// 当前居中的业务逻辑下标
89 | @property (nonatomic, assign) NSInteger currentIndex;
90 | /// 当前居中的实际下标
91 | @property (nonatomic, assign) NSInteger infactIndex;
92 | /// (已经废弃)
93 | @property (nonatomic, assign) CGFloat addHeight;
94 | /**
95 | 自动播放是否暂停
96 | */
97 | @property (nonatomic, assign) BOOL isPause;
98 |
99 | /**
100 | 当前展示在中间的cell下标
101 | */
102 | @property (nonatomic, strong) NSIndexPath *currentIndexPath;
103 |
104 | @property (nonatomic, strong) MyProxy *timerProxy;
105 | @property (nonatomic, strong) NSTimer *timer;
106 |
107 | @end
108 | @implementation CWCarousel
109 | @synthesize carouselView = _carouselView;
110 |
111 |
112 | - (instancetype)initWithFrame:(CGRect)frame delegate:(id)delegate datasource:(id)datasource flowLayout:(CWFlowLayout *)flowLayout {
113 | CGFloat addHeight = 0;
114 | frame.size.height += addHeight;
115 | self.addHeight = addHeight;
116 | if(self = [super initWithFrame:frame]) {
117 | _flowLayout = flowLayout;
118 | self.delegate = delegate;
119 | self.datasource = datasource;
120 | self.isAuto = NO;
121 | self.autoTimInterval = 3;
122 | self.endless = YES;
123 | [self configureView];
124 | [self addNotify];
125 | }
126 | NSAssert(self != nil, @"CWCarousel 初始化失败!");
127 | return self;
128 | }
129 |
130 | - (void)appBecomeInactive:(NSNotification *)notification {
131 | [self adjustErrorCell:YES];
132 | }
133 |
134 | - (void)appBecomeActive:(NSNotification *)notification {
135 | [self adjustErrorCell:YES];
136 | }
137 |
138 | - (void)controllerWillAppear {
139 | if(self.isAuto) {
140 | [self resumePlay];
141 | }
142 | [self addNotify];
143 | [self adjustErrorCell:YES];
144 | }
145 |
146 | - (void)controllerWillDisAppear {
147 | if(self.isAuto) {
148 | [self pause];
149 | }
150 | [self removeNotify];
151 | [self adjustErrorCell:YES];
152 | }
153 |
154 | - (void)addNotify {
155 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil];
156 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
157 | }
158 |
159 | - (void)removeNotify {
160 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
161 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
162 | }
163 |
164 | - (void)dealloc {
165 | NSLog(@"[CWCarousel dealloc]");
166 | [self removeNotify];
167 | [self releaseTimer];
168 | }
169 |
170 | - (void)willMoveToSuperview:(UIView *)newSuperview {
171 | [super willMoveToSuperview:newSuperview];
172 | if (nil == self.customPageControl && nil == self.pageControl.superview) {
173 | [self configurePageControl];
174 | }
175 | }
176 |
177 | - (void)willMoveToWindow:(UIWindow *)newWindow {
178 | [super willMoveToWindow:newWindow];
179 | if (nil == self.customPageControl && nil == self.pageControl.superview) {
180 | [self configurePageControl];
181 | }
182 | }
183 |
184 | - (void)registerViewClass:(Class)viewClass identifier:(NSString *)identifier {
185 | [self.carouselView registerClass:viewClass forCellWithReuseIdentifier:identifier];
186 | }
187 |
188 | - (void)registerNibView:(NSString *)nibName identifier:(NSString *)identifier {
189 | [self.carouselView registerNib:[UINib nibWithNibName:nibName bundle:[NSBundle mainBundle]]
190 | forCellWithReuseIdentifier:identifier];
191 | }
192 |
193 | - (void)freshCarousel {
194 |
195 | if([self numbers] < 0) {
196 | return;
197 | }
198 |
199 | [self.carouselView reloadData];
200 | [self layoutIfNeeded];
201 |
202 | if (self.endless)
203 | [self.carouselView scrollToItemAtIndexPath:[self originIndexPath] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
204 | else {
205 | if(self.flowLayout.style == CWCarouselStyle_Normal) {
206 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
207 | }
208 | else {
209 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
210 | }
211 | }
212 |
213 | self.carouselView.userInteractionEnabled = YES;
214 | if (self.isAuto) {
215 | [self play];
216 | }
217 | }
218 | #pragma mark - < Scroll Delegate >
219 | /// 开始拖拽
220 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
221 | // 防止拖动加速度太大,一次跳过多张图片,这里设置一下
222 | if (@available(iOS 14.0, *)) {
223 | scrollView.pagingEnabled = NO;
224 | } else {
225 | scrollView.pagingEnabled = YES;
226 | }
227 | if (self.isAuto) {
228 | [self stop];
229 | }
230 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didStartScrollAtIndex:indexPathRow:)]) {
231 | [self.delegate CWCarousel:self didStartScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row];
232 | }
233 | }
234 |
235 | /// 将要结束拖拽
236 | - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
237 |
238 | NSInteger num = [self numbers];
239 |
240 | if (num <= 0) {
241 | return;
242 | }
243 |
244 | if (self.endless == NO)
245 | {
246 | // 非无限轮播
247 | NSInteger maxIndex = num - 1;
248 | NSInteger minIndex = 0;
249 | if(self.flowLayout.style != CWCarouselStyle_Normal)
250 | {
251 | // 后面有一个占位cell, 所以减2, 不是减1
252 | maxIndex = [self infactNumbers] - 2;
253 | // 前面有一个占位cell, 所以下标是从1开始
254 | minIndex = 1;
255 | }
256 | if (velocity.x == 0) {
257 | [self velocityZero];
258 | return;
259 | }
260 |
261 | if (velocity.x >= 0 && self.currentIndexPath.row == maxIndex) {
262 | // 已经是最后一张了
263 | return;
264 | }
265 |
266 | if (velocity.x <= 0 && self.currentIndexPath.row == minIndex) {
267 | // 已经是第一张了
268 | return;
269 | }
270 | }
271 |
272 | if(velocity.x > 0) {
273 | //左滑,下一张
274 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section];
275 | }else if (velocity.x < 0) {
276 | //右滑,上一张
277 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row - 1 inSection:self.currentIndexPath.section];
278 | }else if (velocity.x == 0) {
279 | [self velocityZero];
280 | }
281 | }
282 |
283 | /// 开始减速
284 | - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
285 | [self cusScrollViewWillBeginDecelerating:YES scroll:scrollView];
286 | }
287 |
288 | - (void)cusScrollViewWillBeginDecelerating:(BOOL)animation scroll:(UIScrollView *)scrollView{
289 |
290 | if (self.currentIndexPath == nil) {
291 | return;
292 | }
293 |
294 | if (self.currentIndexPath.row >= [self infactNumbers]) {
295 | return;
296 | }
297 |
298 | if (self.currentIndexPath.row < 0) {
299 | return;
300 | }
301 |
302 | // 中间一张轮播,居中显示
303 | if (self.endless == NO)
304 | {
305 | // 非无限轮播, 非CWCarouselStyle_Normal样式下, 前后有两张占位cell, 这里需要处理一下.
306 | if (self.currentIndexPath.row == 0 && self.style != CWCarouselStyle_Normal) {
307 |
308 | self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:self.currentIndexPath.section];
309 |
310 | }else if (self.currentIndexPath.row == [self infactNumbers] - 1 && self.style != CWCarouselStyle_Normal) {
311 |
312 | self.currentIndexPath = [NSIndexPath indexPathForRow:[self infactNumbers] - 2 inSection:self.currentIndexPath.section];
313 |
314 | }
315 | }
316 |
317 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animation];
318 |
319 | if (animation == NO) {
320 | [self cusScrollAnimationEnd:scrollView];
321 | }
322 | }
323 |
324 | /// 减速完成
325 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
326 |
327 | // 打开交互
328 | scrollView.pagingEnabled = NO;
329 | if(self.isAuto) {
330 | [self play];
331 | }
332 | }
333 |
334 | /// 滚动动画完成
335 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
336 |
337 | [self cusScrollAnimationEnd:scrollView];
338 | }
339 |
340 | - (void)cusScrollAnimationEnd:(UIScrollView *)scrollView {
341 | // 滚动完成,打开交互,关掉pagingEnabled
342 | // 为什么要关掉pagingEnabled呢,因为切换控制器的时候会有系统级bug,不信你试试.
343 | scrollView.userInteractionEnabled = YES;
344 | scrollView.pagingEnabled = NO;
345 |
346 | if(self.isAuto) {
347 | [self play];
348 | }
349 |
350 | if (self.endless) {
351 | [self checkOutofBounds];
352 | }
353 |
354 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didEndScrollAtIndex:indexPathRow:)]) {
355 | [self.delegate CWCarousel:self didEndScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row];
356 | }
357 | }
358 |
359 | // 滚动中
360 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
361 |
362 | }
363 |
364 | #pragma mark - < Logic Helper >
365 | - (NSIndexPath *)originIndexPath {
366 |
367 | NSInteger num = [self numbers];
368 | if (num <= 0) {
369 | return [[NSIndexPath alloc] initWithIndex:0];
370 | }
371 |
372 | if (self.endless == NO) {
373 | NSInteger row = self.flowLayout.style == CWCarouselStyle_Normal ? 0 : 1;
374 | self.currentIndexPath = [NSIndexPath indexPathForRow:row inSection:0];
375 | return self.currentIndexPath;
376 | }
377 |
378 | NSInteger centerIndex = [self infactNumbers] / num; //一共有多少组
379 |
380 | if (centerIndex == 0) {
381 | // 异常, 一组都没有
382 | NSAssert(true, @"计算起始下标异常, 分组不足一组.");
383 | return self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
384 | }
385 |
386 | if (centerIndex == 1) {
387 | return self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
388 | }
389 |
390 | // 取中间一组展示
391 | self.currentIndexPath = [NSIndexPath indexPathForRow:centerIndex / 2 * num inSection:0];
392 | return self.currentIndexPath;
393 | }
394 |
395 | // 检测是否到了边界
396 | - (void)checkOutofBounds {
397 | if ([self numbers] <= 0) {return;}
398 |
399 | BOOL scroll = NO;
400 | NSInteger index = self.currentIndex;
401 |
402 | // 越界检查
403 | if(self.currentIndexPath.row == [self infactNumbers] - 1) {
404 | index = [self caculateIndex:self.currentIndexPath.row] - 1; //最后一张
405 | scroll = YES;
406 | }else if(self.currentIndexPath.row == 0) {
407 | index = [self caculateIndex:self.currentIndexPath.row]; //第一张
408 | scroll = YES;
409 | }
410 |
411 | self.carouselView.userInteractionEnabled = YES;
412 | if (scroll == NO) {
413 | return;
414 | }
415 |
416 | NSIndexPath *origin = [self originIndexPath];
417 | self.currentIndexPath = [NSIndexPath indexPathForRow:origin.row + index inSection:origin.section];
418 | [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
419 | }
420 |
421 | /**
422 | 实际下标转换成业务需求下标
423 |
424 | @param factIndex 实际下标
425 | @return 业务需求下标
426 | */
427 | - (NSInteger)caculateIndex:(NSInteger)factIndex {
428 | NSInteger num = [self numbers];
429 | if (num <= 0) {
430 | return 0;
431 | }
432 | NSInteger row = factIndex % num;
433 | if(self.endless == NO && self.flowLayout.style != CWCarouselStyle_Normal) {
434 | // 这种情况有占位cell
435 | row = factIndex % [self infactNumbers] - 1;
436 | }
437 | return row;
438 | }
439 |
440 | - (void)adjustErrorCell:(BOOL)isScroll {
441 | NSArray *indexPaths = [self.carouselView indexPathsForVisibleItems];
442 | CGFloat centerX = self.carouselView.contentOffset.x + CGRectGetWidth(self.carouselView.frame) * 0.5;
443 | __block CGFloat minSpace = MAXFLOAT;
444 | [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
445 | UICollectionViewLayoutAttributes *attri = [self.carouselView layoutAttributesForItemAtIndexPath:obj];
446 | attri.zIndex = 0;
447 | if(ABS(minSpace) > ABS(attri.center.x - centerX)) {
448 | minSpace = attri.center.x - centerX;
449 | self.currentIndexPath = attri.indexPath;
450 | }
451 | }];
452 | if(isScroll) {
453 | [self scrollViewWillBeginDecelerating:self.carouselView];
454 | }
455 | }
456 |
457 | /// velocity == 0时的处理
458 | - (void)velocityZero {
459 | // 还有一种情况,当滑动后手指按住不放,然后松开,此时的加速度其实是为0的
460 | [self adjustErrorCell:NO];
461 | if (@available(iOS 14.0, *)) {
462 | // iOS14以前,就算加速度为0,后续系统会还是会走scrollViewWillBeginDecelerating:回调
463 | // 但是iOS14以后,加速度为0时,不会在后续执行回调.这里手动触发一下
464 | [self scrollViewWillBeginDecelerating:self.carouselView];
465 | }
466 | }
467 |
468 | - (void)play {
469 | if(self.isPause) {
470 | return;
471 | }
472 |
473 | if (NO == self.isAuto) {
474 | return;
475 | }
476 |
477 | if (self.timer) {
478 | [self resumePlay];
479 | return;
480 | }
481 | self.timer = [NSTimer timerWithTimeInterval:self.autoTimInterval target:self.timerProxy selector:@selector(nextCell) userInfo:nil repeats:YES];
482 | [self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:self.autoTimInterval]];
483 | [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
484 | }
485 |
486 | - (void)nextCell {
487 |
488 | if([self numbers] <= 0) {
489 | return;
490 | }
491 |
492 | if (!self.isAuto) {
493 | return;
494 | }
495 |
496 | NSInteger maxIndex = 1;
497 | if(!self.endless && self.flowLayout.style != CWCarouselStyle_Normal)
498 | {
499 | maxIndex = 2;
500 | }
501 | if(self.currentIndexPath.row < [self infactNumbers] - maxIndex)
502 | {
503 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section];
504 | [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
505 | self.currentIndexPath = indexPath;
506 | }
507 | else if(!self.endless)
508 | {
509 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:maxIndex - 1 inSection:self.currentIndexPath.section];
510 | [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
511 | self.currentIndexPath = indexPath;
512 | }
513 | }
514 |
515 | - (void)stop {
516 | if (!self.timer) {
517 | return;
518 | }
519 | [self.timer setFireDate:[NSDate distantFuture]];
520 | }
521 |
522 | - (void)resumePlay {
523 | self.isPause = NO;
524 | if (self.timer) {
525 | [self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:self.autoTimInterval]];
526 | return;
527 | }
528 | [self play];
529 | }
530 |
531 | - (void)pause {
532 | self.isPause = YES;
533 | [self stop];
534 | }
535 |
536 | - (void)releaseTimer {
537 | // [self stop];
538 | if (!self.timer) {
539 | return;
540 | }
541 | [self.timer setFireDate:[NSDate distantFuture]];
542 | [self.timer invalidate];
543 | self.timer = nil;
544 | }
545 |
546 | - (void)scrollTo:(NSInteger)index animation:(BOOL)animation {
547 |
548 | if (index < 0 || index >= [self numbers]) {
549 | // 防止越界
550 | return;
551 | }
552 |
553 | [self stop];
554 |
555 | if (index == self.currentIndex) {
556 | [self play];
557 | return;
558 | }
559 |
560 | self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + (index - self.currentIndex) inSection:0];
561 | [self cusScrollViewWillBeginDecelerating:animation scroll:self.carouselView];
562 | }
563 |
564 | - (void)configurePageControl {
565 | UIView *control = self.customPageControl;
566 | BOOL isDefault = NO;
567 |
568 | if (nil == control || NO == [control isKindOfClass:[UIView class]]) {
569 | control = self.pageControl;
570 | isDefault = YES;
571 | }
572 |
573 | if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:addPageControl:isDefault:)]) {
574 | [self.delegate CWCarousel:self addPageControl:control isDefault:isDefault];
575 | return;
576 | }
577 |
578 | [self addSubview:control];
579 | control.translatesAutoresizingMaskIntoConstraints = NO;
580 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[control(20)]-0-|"
581 | options:kNilOptions
582 | metrics:nil
583 | views:@{@"control" : control}]];
584 | [[control.centerXAnchor constraintEqualToAnchor:self.centerXAnchor] setActive:YES];
585 | }
586 |
587 | #pragma mark - < Configure View>
588 | - (void)configureView {
589 | self.backgroundColor = [UIColor blackColor];
590 | self.carouselView.showsVerticalScrollIndicator = NO;
591 | self.carouselView.showsHorizontalScrollIndicator = NO;
592 | self.carouselView.decelerationRate = 0;
593 | }
594 |
595 | #pragma mark - < Delegate, Datasource >
596 | - (nonnull __kindof UICollectionViewCell *)collectionView:(nonnull UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
597 |
598 | __weak __typeof(&*self) weakSelf = self;
599 |
600 | UICollectionViewCell* (^returnCell)(NSIndexPath *) = ^UICollectionViewCell* (NSIndexPath *idx) {
601 | if (self.datasource && [self.datasource respondsToSelector:@selector(viewForCarousel:indexPath:index:)]) {
602 | UICollectionViewCell *cell = [weakSelf.datasource viewForCarousel:weakSelf indexPath:indexPath index:[weakSelf caculateIndex:indexPath.row]];
603 | return cell;
604 | }
605 | return nil;
606 | };
607 |
608 |
609 | if (self.endless) {
610 | return returnCell(indexPath);
611 | }
612 |
613 | if (self.flowLayout.style != CWCarouselStyle_Normal
614 | && (indexPath.row == 0 || indexPath.row == [self infactNumbers] - 1)) {
615 | // 非无限轮播情况下, "第一个"和"最后一个"是占位cell
616 | UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"tempCell" forIndexPath:indexPath];
617 | cell.contentView.backgroundColor = [UIColor clearColor];
618 | return cell;
619 | }
620 |
621 | return returnCell(indexPath);
622 |
623 | }
624 |
625 | - (NSInteger)collectionView:(nonnull UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
626 | return [self infactNumbers];
627 | }
628 |
629 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
630 | if(self.delegate &&
631 | [self.delegate respondsToSelector:@selector(CWCarousel:didSelectedAtIndex:)]) {
632 | [self.delegate CWCarousel:self didSelectedAtIndex:[self caculateIndex:indexPath.row]];
633 | }
634 | // 处于动画中时,点击cell,可能会出现cell不居中问题.这里处理下
635 | // 将里中心点最近的那个cell居中
636 | [self adjustErrorCell:YES];
637 | }
638 |
639 | #pragma mark -
640 | - (void)setBackgroundColor:(UIColor *)backgroundColor {
641 | self.carouselView.backgroundColor = backgroundColor;
642 | [super setBackgroundColor:backgroundColor];
643 | }
644 |
645 | - (void)setCurrentIndexPath:(NSIndexPath *)currentIndexPath {
646 | _currentIndexPath = currentIndexPath;
647 |
648 | if (_currentIndexPath) {
649 | self.currentIndex = [self caculateIndex:_currentIndexPath.row];
650 | }
651 |
652 | if(self.customPageControl == nil)
653 | self.pageControl.currentPage = [self caculateIndex:currentIndexPath.row];
654 | else
655 | self.customPageControl.currentPage = [self caculateIndex:currentIndexPath.row];
656 | }
657 |
658 | - (void)setEndless:(BOOL)endless {
659 | if(_endless != endless) {
660 | _endless = endless;
661 | }
662 | }
663 |
664 | - (void)setCustomPageControl:(UIView *)customPageControl {
665 |
666 | if (_customPageControl == customPageControl) {
667 | return;
668 | }
669 |
670 | if (_customPageControl) {
671 | [_customPageControl removeFromSuperview];
672 | }
673 |
674 | _customPageControl = customPageControl;
675 | if (customPageControl && self.pageControl) {
676 | [self.pageControl removeFromSuperview];
677 | self.pageControl = nil;
678 | }
679 |
680 | [self configurePageControl];
681 | }
682 |
683 | #pragma mark - < getter >
684 | - (UICollectionView *)carouselView {
685 | if(!_carouselView) {
686 | self.carouselView = [[CWCarouselCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.flowLayout];
687 | _carouselView.delegate = self;
688 | _carouselView.dataSource = self;
689 | _carouselView.translatesAutoresizingMaskIntoConstraints = NO;
690 | [_carouselView registerClass:[CWTempleteCell class] forCellWithReuseIdentifier:@"tempCell"];
691 | [self addSubview:_carouselView];
692 |
693 | NSDictionary *views = @{@"view" : self.carouselView};
694 | NSDictionary *margins = @{@"top" : @(self.addHeight * 0.5),
695 | @"bottom" : @(self.addHeight * 0.5)
696 | };
697 | NSString *str = @"H:|-0-[view]-0-|";
698 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str
699 | options:kNilOptions
700 | metrics:margins
701 | views:views]];
702 | str = @"V:|-top-[view]-top-|";
703 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str
704 | options:kNilOptions
705 | metrics:margins
706 | views:views]];
707 |
708 | __weak __typeof(&*self) weakSelf = self;
709 | [_carouselView setTapCallback:^{
710 | [weakSelf adjustErrorCell:YES];
711 | }];
712 | }
713 | return _carouselView;
714 | }
715 |
716 |
717 | - (CWCarouselStyle)style {
718 | if(self.flowLayout) {
719 | return self.flowLayout.style;
720 | }
721 | return CWCarouselStyle_Unknow;
722 | }
723 |
724 |
725 | /**
726 | Description
727 |
728 | @return 业务需求需要展示轮播图个数
729 | */
730 | - (NSInteger)numbers {
731 | if(self.datasource &&
732 | [self.datasource respondsToSelector:@selector(numbersForCarousel)]) {
733 | NSInteger num = [self.datasource numbersForCarousel];
734 | if (self.pageControl) {
735 | self.pageControl.numberOfPages = num;
736 | }
737 | if (self.customPageControl) {
738 | self.customPageControl.pageNumbers = num;
739 | }
740 | return num;
741 | }
742 | return 0;
743 | }
744 |
745 | /**
746 | 轮播图实际加载视图个数
747 |
748 | @return 轮播图实际加载视图个数
749 | */
750 | - (NSInteger)infactNumbers {
751 |
752 | NSInteger num = [self numbers];
753 |
754 | if ( num <= 0) {
755 | return 0;
756 | }
757 |
758 | [self.carouselView setScrollEnabled:YES];
759 |
760 | if (self.endless) {
761 | // 无限轮播
762 | if (num == 1) {
763 | // 只有一张, 不让滚动
764 | [self.carouselView setScrollEnabled:NO];
765 | return num;
766 | }
767 | // 不止一张, 默认加载300个 (cell有复用机制, 不用担心)
768 | return 300;
769 | }
770 |
771 | // 非无限轮播, 除了第一种样式, 其他的样式要加2个占位空cell
772 | if(self.flowLayout.style == CWCarouselStyle_Normal) {
773 | return num;
774 | }
775 |
776 | if (num == 1) {
777 | [self.carouselView setScrollEnabled:NO];
778 | }
779 | // 前后2个占位cell,所以+2
780 | return num + 2;
781 | }
782 |
783 | - (UIPageControl *)pageControl {
784 | if(!_pageControl) {
785 | self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectZero];
786 | _pageControl.pageIndicatorTintColor = [UIColor blackColor];
787 | _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor];
788 | _pageControl.userInteractionEnabled = NO;
789 | }
790 | return _pageControl;
791 | }
792 |
793 | - (MyProxy *)timerProxy {
794 | if (!_timerProxy) {
795 | self.timerProxy = [[MyProxy alloc] init:self];
796 | }
797 | return _timerProxy;
798 | }
799 |
800 | - (NSString *)version {
801 | return @"1.1.9";
802 | }
803 | @end
804 |
805 |
806 |
807 |
808 |
--------------------------------------------------------------------------------
/CWCarouselDemo/CWCarousel/Sources/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 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
--------------------------------------------------------------------------------