// // CWCarousel.m // CWCarousel // // Created by WangChen on 2018/4/3. // Copyright © 2018年 ChenWang. All rights reserved. // #import "CWCarousel.h" @interface CWTempleteCell: UICollectionViewCell @end @implementation CWTempleteCell @end @interface CWCarousel () { } @property (nonatomic, strong) UICollectionView *carouselView; @property (nonatomic, assign) NSInteger numbers; @property (nonatomic, assign) NSInteger currentIndex; @property (nonatomic, assign) NSInteger infactIndex; @property (nonatomic, assign) CGFloat addHeight; /** 自动播放是否暂停 */ @property (nonatomic, assign) BOOL isPause; @end @implementation CWCarousel @synthesize carouselView = _carouselView; - (instancetype)initWithFrame:(CGRect)frame delegate:(id)delegate datasource:(id)datasource flowLayout:(CWFlowLayout *)flowLayout { CGFloat addHeight = 0; // if(flowLayout.style == CWCarouselStyle_H_3) { // /* 如果是CWCarouselStyle_H_3, 因为中间一张图片放大的原因,需要扩充一下frame的高度,所以会和实际的传入的frame // 的高度有部分偏差 // */ // addHeight = (flowLayout.maxScale - 1) * CGRectGetHeight(frame); // } frame.size.height += addHeight; self.addHeight = addHeight; if(self = [super initWithFrame:frame]) { _flowLayout = flowLayout; self.delegate = delegate; self.datasource = datasource; self.isAuto = NO; self.autoTimInterval = 3; self.endless = YES; [self configureView]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; return self; } - (void)appBecomeInactive:(NSNotification *)notification { [self adjustErrorCell:YES]; } - (void)appBecomeActive:(NSNotification *)notification { [self adjustErrorCell:YES]; } - (void)controllerWillAppear { if(self.isAuto) { [self resumePlay]; } [self adjustErrorCell:YES]; } - (void)controllerWillDisAppear { if(self.isAuto) { [self pause]; } [self adjustErrorCell:YES]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)willMoveToSuperview:(UIView *)newSuperview { newSuperview.clipsToBounds = NO; if(self.customPageControl == nil && self.pageControl.superview == nil) { [self addSubview:self.pageControl]; self.pageControl.translatesAutoresizingMaskIntoConstraints = NO; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[control]-0-|" options:kNilOptions metrics:nil views:@{@"control" : self.pageControl}]]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[control(30)]-0-|" options:kNilOptions metrics:nil views:@{@"control" : self.pageControl}]]; }else if(self.customPageControl || self.customPageControl.superview == nil) { [self addSubview:self.customPageControl]; } [super willMoveToSuperview:newSuperview]; } - (void)registerViewClass:(Class)viewClass identifier:(NSString *)identifier { [self.carouselView registerClass:viewClass forCellWithReuseIdentifier:identifier]; } - (void)registerNibView:(NSString *)nibName identifier:(NSString *)identifier { [self.carouselView registerNib:[UINib nibWithNibName:nibName bundle:[NSBundle mainBundle]] forCellWithReuseIdentifier:identifier]; } - (void)freshCarousel { if([self numbers] <= 0) { return; } [self.carouselView reloadData]; [self.carouselView layoutIfNeeded]; if (self.endless) [self.carouselView scrollToItemAtIndexPath:[self originIndexPath] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; else { if(self.flowLayout.style == CWCarouselStyle_Normal) { [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; } else { [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; } } self.carouselView.userInteractionEnabled = YES; if (self.isAuto) { [self play]; } } #pragma mark - < Scroll Delegate > /// 开始拖拽 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // 防止拖动加速度太大,一次跳过多张图片,这里设置一下 if (@available(iOS 14.0, *)) { scrollView.pagingEnabled = NO; } else { scrollView.pagingEnabled = YES; } if (self.isAuto) { [self stop]; } if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didStartScrollAtIndex:indexPathRow:)]) { [self.delegate CWCarousel:self didStartScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row]; } } /// 将要结束拖拽 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (!self.endless) { NSInteger maxIndex = [self numbers] - 1; NSInteger minIndex = 0; if(self.flowLayout.style != CWCarouselStyle_Normal) { maxIndex = [self infactNumbers] - 2; minIndex = 1; } if (velocity.x == 0) { //还有一种情况,当滑动后手指按住不放,然后松开,此时的加速度其实是为0的 [self adjustErrorCell:NO]; if (@available(iOS 14.0, *)) { // iOS14以前,就算加速度为0,后续系统会还是会走scrollViewWillBeginDecelerating:回调 // 但是iOS14以后,加速度为0时,不会在后续执行回调.这里手动触发一下 [self scrollViewWillBeginDecelerating:self.carouselView]; } return; } if (velocity.x >= 0 && self.currentIndexPath.row == maxIndex) { return; } if (velocity.x <= 0 && self.currentIndexPath.row == minIndex) { return; } } if(velocity.x > 0) { //左滑,下一张 self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section]; }else if (velocity.x < 0) { //右滑,上一张 self.currentIndexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row - 1 inSection:self.currentIndexPath.section]; }else if (velocity.x == 0) { //还有一种情况,当滑动后手指按住不放,然后松开,此时的加速度其实是为0的 [self adjustErrorCell:NO]; if (@available(iOS 14.0, *)) { // iOS14以前,就算加速度为0,后续系统会还是会走scrollViewWillBeginDecelerating:回调 // 但是iOS14以后,加速度为0时,不会在后续执行回调.这里手动触发一下 [self scrollViewWillBeginDecelerating:self.carouselView]; } } } /// 开始减速 - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { if(self.currentIndexPath != nil && self.currentIndexPath.row < [self infactNumbers] && self.currentIndexPath.row >= 0) { // 中间一张轮播,居中显示 if (!self.endless) { if (self.currentIndexPath.row == 0 && self.style != CWCarouselStyle_Normal) { self.currentIndexPath = [NSIndexPath indexPathForRow:1 inSection:self.currentIndexPath.section]; } else if (self.currentIndexPath.row == [self infactNumbers] - 1 && self.style != CWCarouselStyle_Normal) { self.currentIndexPath = [NSIndexPath indexPathForRow:[self infactNumbers] - 2 inSection:self.currentIndexPath.section]; } } [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; } } /// 减速完成 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 打开交互 scrollView.pagingEnabled = NO; if(self.isAuto) { [self play]; } } /// 滚动动画完成 - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { // 滚动完成,打开交互,关掉pagingEnabled // 为什么要关掉pagingEnabled呢,因为切换控制器的时候会有系统级bug,不信你试试. scrollView.userInteractionEnabled = YES; scrollView.pagingEnabled = NO; if(self.isAuto) { [self play]; } if (self.endless) [self checkOutofBounds]; // if(!self.endless) // { // CGFloat space = self.flowLayout.itemSpace_H + self.flowLayout.itemWidth * (1 - self.flowLayout.minScale) * 0.5; // if(self.currentIndexPath.row == 0) // self.carouselView.contentInset = UIEdgeInsetsMake(0, space, 0, 0); // else if(self.currentIndexPath.row == [self numbers] - 1) // self.carouselView.contentInset = UIEdgeInsetsMake(0, 0, 0, space); // else // self.carouselView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0); // } if (self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didEndScrollAtIndex:indexPathRow:)]) { [self.delegate CWCarousel:self didEndScrollAtIndex:[self caculateIndex:self.currentIndexPath.row] indexPathRow:self.currentIndexPath.row]; } } // 滚动中 - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // 滚动过程中关闭交互 // scrollView.userInteractionEnabled = NO; } #pragma mark - < Logic Helper > - (NSIndexPath *)originIndexPath { NSInteger num = [self numbers]; if (num <= 0) { return [[NSIndexPath alloc] initWithIndex:0]; } NSInteger centerIndex = [self infactNumbers] / [self numbers]; if (self.endless) { if(centerIndex <= 1) { if (centerIndex == 1) { self.currentIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]; }else { self.currentIndexPath = [NSIndexPath indexPathForRow:self.numbers inSection:0]; } }else { self.currentIndexPath = [NSIndexPath indexPathForRow:centerIndex / 2 * [self numbers] inSection:0]; } }else { NSInteger row = self.flowLayout.style == CWCarouselStyle_Normal ? 0 : 1; self.currentIndexPath = [NSIndexPath indexPathForRow:row inSection:0]; } return self.currentIndexPath; } - (void)checkOutofBounds { if ([self numbers] <= 0) {return;} // 越界检查 if(self.currentIndexPath.row == [self infactNumbers] - 1) { //最后一张 NSIndexPath *origin = [self originIndexPath]; NSInteger index = [self caculateIndex:self.currentIndexPath.row] - 1; self.currentIndexPath = [NSIndexPath indexPathForRow:origin.row + index inSection:origin.section]; [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; self.carouselView.userInteractionEnabled = YES; }else if(self.currentIndexPath.row == 0) { //第一张 NSIndexPath *origin = [self originIndexPath]; NSInteger index = [self caculateIndex:self.currentIndexPath.row]; self.currentIndexPath = [NSIndexPath indexPathForRow:origin.row + index inSection:origin.section]; [self.carouselView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; self.carouselView.userInteractionEnabled = YES; } } - (void)pageControlClick:(UIPageControl *)sender { if (![sender isKindOfClass:[UIPageControl class]]) { return; } NSInteger page = sender.currentPage; NSInteger prePage = [self caculateIndex:self.currentIndexPath.row]; if(page == prePage) { return; } NSIndexPath *indexPath = nil; if(prePage - page == [self numbers] - 1) { //最后一张跳到第一张 indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:0]; }else if(page - prePage == [self numbers] - 1) { //第一张跳到最后一张 indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row - 1 inSection:0]; }else { indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + page - prePage inSection:0]; } [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; self.currentIndexPath = indexPath; } /** 实际下标转换成业务需求下标 @param factIndex 实际下标 @return 业务需求下标 */ - (NSInteger)caculateIndex:(NSInteger)factIndex { if (self.numbers <= 0) { return 0; } NSInteger row = factIndex % [self numbers]; if(!self.endless && self.flowLayout.style != CWCarouselStyle_Normal) { row = factIndex % [self infactNumbers] - 1; } return row; } - (void)adjustErrorCell:(BOOL)isScroll { NSArray *indexPaths = [self.carouselView indexPathsForVisibleItems]; NSMutableArray *attriArr = [NSMutableArray arrayWithCapacity:indexPaths.count]; [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { UICollectionViewLayoutAttributes *attri = [self.carouselView layoutAttributesForItemAtIndexPath:obj]; [attriArr addObject:attri]; }]; CGFloat centerX = self.carouselView.contentOffset.x + CGRectGetWidth(self.carouselView.frame) * 0.5; __block CGFloat minSpace = MAXFLOAT; // BOOL shouldSet = YES; // if (self.flowLayout.style != CWCarouselStyle_Normal && indexPaths.count <= 2) // { // shouldSet = NO; // } [attriArr enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { obj.zIndex = 0; // if(ABS(minSpace) > ABS(obj.center.x - centerX) && shouldSet) { // minSpace = obj.center.x - centerX; // self.currentIndexPath = obj.indexPath; // } if(ABS(minSpace) > ABS(obj.center.x - centerX)) { minSpace = obj.center.x - centerX; self.currentIndexPath = obj.indexPath; } }]; if(isScroll) { [self scrollViewWillBeginDecelerating:self.carouselView]; } } - (void)play { [self stop]; if(self.isPause) { return; } [self performSelector:@selector(nextCell) withObject:nil afterDelay:self.autoTimInterval]; } - (void)scrollToIndex:(int)index { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:self.currentIndexPath.section]; [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; self.currentIndexPath = indexPath; } - (void)nextCell { if([self numbers] <= 0) { return; } NSInteger maxIndex = 1; if(!self.endless && self.flowLayout.style != CWCarouselStyle_Normal) { maxIndex = 2; } if(self.currentIndexPath.row < [self infactNumbers] - maxIndex) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.currentIndexPath.row + 1 inSection:self.currentIndexPath.section]; [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; self.currentIndexPath = indexPath; } else if(!self.endless) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:maxIndex - 1 inSection:self.currentIndexPath.section]; [self.carouselView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; self.currentIndexPath = indexPath; } [self performSelector:@selector(nextCell) withObject:nil afterDelay:self.autoTimInterval]; } - (void)stop { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(nextCell) object:nil]; [NSObject cancelPreviousPerformRequestsWithTarget:self]; } - (void)resumePlay { self.isPause = NO; [self play]; } - (void)pause { self.isPause = YES; [self stop]; } - (void)releaseTimer { [self stop]; } #pragma mark - < Configure View> - (void)configureView { self.backgroundColor = [UIColor blackColor]; self.carouselView.showsVerticalScrollIndicator = NO; self.carouselView.showsHorizontalScrollIndicator = NO; self.carouselView.decelerationRate = 0; } #pragma mark - < Delegate, Datasource > - (nonnull __kindof UICollectionViewCell *)collectionView:(nonnull UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if(!self.endless && self.flowLayout.style != CWCarouselStyle_Normal && (indexPath.row == 0 || indexPath.row == [self infactNumbers] - 1)) { // 添加占位cell UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"tempCell" forIndexPath:indexPath]; cell.contentView.backgroundColor = [UIColor clearColor]; return cell; } else { if(self.datasource && [self.datasource respondsToSelector:@selector(viewForCarousel:indexPath:index:)]) { UICollectionViewCell *cell = [self.datasource viewForCarousel:self indexPath:indexPath index:[self caculateIndex:indexPath.row]]; return cell; } return nil; } } - (NSInteger)collectionView:(nonnull UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if([self numbers] > 0) return [self infactNumbers]; else return 0; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if(self.delegate && [self.delegate respondsToSelector:@selector(CWCarousel:didSelectedAtIndex:)]) { [self.delegate CWCarousel:self didSelectedAtIndex:[self caculateIndex:indexPath.row]]; } // 处于动画中时,点击cell,可能会出现cell不居中问题.这里处理下 // 将里中心点最近的那个cell居中 [self adjustErrorCell:YES]; } #pragma mark - - (void)setBackgroundColor:(UIColor *)backgroundColor { self.carouselView.backgroundColor = backgroundColor; [super setBackgroundColor:backgroundColor]; } - (void)setCurrentIndexPath:(NSIndexPath *)currentIndexPath { _currentIndexPath = currentIndexPath; if(self.customPageControl == nil) self.pageControl.currentPage = [self caculateIndex:currentIndexPath.row]; else self.customPageControl.currentPage = [self caculateIndex:currentIndexPath.row]; } - (void)setEndless:(BOOL)endless { if(_endless != endless) { _endless = endless; } } #pragma mark - < getter > - (UICollectionView *)carouselView { if(!_carouselView) { // self.carouselView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, self.addHeight * 0.5, self.frame.size.width, self.frame.size.height - self.addHeight) collectionViewLayout:self.flowLayout]; self.carouselView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.flowLayout]; _carouselView.clipsToBounds = NO; _carouselView.delegate = self; _carouselView.dataSource = self; _carouselView.translatesAutoresizingMaskIntoConstraints = NO; [_carouselView registerClass:[CWTempleteCell class] forCellWithReuseIdentifier:@"tempCell"]; [self addSubview:_carouselView]; NSDictionary *views = @{@"view" : self.carouselView}; NSDictionary *margins = @{@"top" : @(self.addHeight * 0.5), @"bottom" : @(self.addHeight * 0.5) }; NSString *str = @"H:|-0-[view]-0-|"; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str options:kNilOptions metrics:margins views:views]]; str = @"V:|-top-[view]-top-|"; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:str options:kNilOptions metrics:margins views:views]]; } return _carouselView; } - (CWCarouselStyle)style { if(self.flowLayout) { return self.flowLayout.style; } return CWCarouselStyle_Unknow; } /** Description @return 业务需求需要展示轮播图个数 */ - (NSInteger)numbers { if(self.datasource && [self.datasource respondsToSelector:@selector(numbersForCarousel)]) { self.pageControl.numberOfPages = [self.datasource numbersForCarousel]; return self.pageControl.numberOfPages; } return 0; } /** 轮播图实际加载视图个数 @return 轮播图实际加载视图个数 */ - (NSInteger)infactNumbers { NSInteger num = [self numbers]; if ( 0 >= num) { return 0; } [self.carouselView setScrollEnabled:YES]; if (self.endless) { if (num < 1) { [self.carouselView setScrollEnabled:NO]; return num; } // 如果是无限轮播,默认加载300个 return 300; } else { // 如果不是无限轮播,出了第一种样式,其他的样式要加2个占位空cell if(self.flowLayout.style == CWCarouselStyle_Normal) { return [self numbers]; } else { if (num == 1) { [self.carouselView setScrollEnabled:NO]; } // 前后2个占位cell,所以+2 return [self numbers] + 2; } } } - (UIPageControl *)pageControl { if(!_pageControl) { self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, 0, 300, 30)]; CGPoint center = self.center; center.y = CGRectGetHeight(self.frame) - 30 * 0.5; _pageControl.pageIndicatorTintColor = [UIColor blackColor]; _pageControl.currentPageIndicatorTintColor = [UIColor whiteColor]; _pageControl.userInteractionEnabled = NO; [_pageControl addTarget:self action:@selector(pageControlClick:) forControlEvents:UIControlEventTouchUpInside]; _pageControl.center = center; } return _pageControl; } - (NSString *)version { return @"1.1.7"; } #pragma mark - Setter - (void)setCustomPageControl:(UIView *)customPageControl { _customPageControl = customPageControl; if(_customPageControl && _customPageControl.superview == nil) { [self addSubview:_customPageControl]; [self bringSubviewToFront:_customPageControl]; if(self.pageControl.superview == _customPageControl.superview) { [self.pageControl removeFromSuperview]; } } } @end