SDCycleScrollView源码解析

1,656 阅读9分钟

基本使用

  • pod集成
    pod 'SDCycleScrollView'
  • 使用
#import <SDCycleScrollView.h>
SDCycleScrollView *cycleScrollView = [[SDCycleScrollView alloc] initWithFrame:CGRectMake(50, 100, 300, 200)];
cycleScrollView.imageURLStringsGroup = @[@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512376&di=7b0a51e43842420769d28038b464c025&imgtype=0&src=http%3A%2F%2Fimg03.tooopen.com%2Fuploadfile%2Fdowns%2Fimages%2F20110714%2Fsy_20110714135215645030.jpg",@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512376&di=39f792203f27ff222fc4169ccd2b2510&imgtype=0&src=http%3A%2F%2Fpic.58pic.com%2F58pic%2F15%2F68%2F59%2F71X58PICNjx_1024.jpg",@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512375&di=81eb34e98aab8012d83852957b9a0bda&imgtype=0&src=http%3A%2F%2Fwww.zt5.com%2Fuploadfile%2F2019%2F0127%2F20190127010113674.jpg"];
cycleScrollView.showPageControl = NO;
[self.view addSubview:cycleScrollView];

源码分析

该轮播图框架使用简单,让我们来分析下源代码。先看下初始化代码。

  • 初始化部分
SDCycleScrollView *cycleScrollView = [[SDCycleScrollView alloc] initWithFrame:CGRectMake(50, 100, 300, 200)];

调用以上代码会执行SDCycleScrollView内部的初始化代码如下:

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self initialization];
        [self setupMainView];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self initialization];
    [self setupMainView];
}

可以看到主要是调用了initialization方法和setupMainView方法。来到initialization方法内部看看做了什么。

- (void)initialization
{
    _pageControlAliment = SDCycleScrollViewPageContolAlimentCenter;
    _autoScrollTimeInterval = 2.0;
    _titleLabelTextColor = [UIColor whiteColor];
    _titleLabelTextFont= [UIFont systemFontOfSize:14];
    _titleLabelBackgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
    _titleLabelHeight = 30;
    _titleLabelTextAlignment = NSTextAlignmentLeft;
    _autoScroll = YES;
    _infiniteLoop = YES;
    _showPageControl = YES;
    _pageControlDotSize = kCycleScrollViewInitialPageControlDotSize;
    _pageControlBottomOffset = 0;
    _pageControlRightOffset = 0;
    _pageControlStyle = SDCycleScrollViewPageContolStyleClassic;
    _hidesForSinglePage = YES;
    _currentPageDotColor = [UIColor whiteColor];
    _pageDotColor = [UIColor lightGrayColor];
    _bannerImageViewContentMode = UIViewContentModeScaleToFill;
    
    self.backgroundColor = [UIColor lightGrayColor];
    
}

可以看到主要是对配置的初始化。主要是初始化了颜色,是否自动轮播等等。这样写的好处在于,如果框架的使用者没有做这些配置,那么该框架将会采用这种默认的配置。如果框架的使用者在外边进行了配置,那么相当于在默认的配置上进行了修改,框架内部将会使用外部的配置。也就是说框架的使用者没有进行配置,那么就采取这么默认的配置。如果框架的使用者进行了配置,那么就使用框架的使用者的配置。
接下来看看setupMainView方法内部

// 设置显示图片的collectionView
- (void)setupMainView
{
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.minimumLineSpacing = 0;
    flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    _flowLayout = flowLayout;
    
    UICollectionView *mainView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:flowLayout];
    mainView.backgroundColor = [UIColor clearColor];
    mainView.pagingEnabled = YES;
    mainView.showsHorizontalScrollIndicator = NO;
    mainView.showsVerticalScrollIndicator = NO;
    [mainView registerClass:[SDCollectionViewCell class] forCellWithReuseIdentifier:ID];
    
    mainView.dataSource = self;
    mainView.delegate = self;
    mainView.scrollsToTop = NO;
    [self addSubview:mainView];
    _mainView = mainView;
}

以上代码是对collectionView的布局。也就是该框架对图片的轮播采用的是collectionView,collectionView对cell进行了优化使得性能更高。这样我们就了解了该框架的初始化部分。

  • 开始轮播显示部分
    经过初始化后,将要进行显示图片并开始轮播了。以下代码实现了该功能:
cycleScrollView.imageURLStringsGroup = @[@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512376&di=7b0a51e43842420769d28038b464c025&imgtype=0&src=http%3A%2F%2Fimg03.tooopen.com%2Fuploadfile%2Fdowns%2Fimages%2F20110714%2Fsy_20110714135215645030.jpg",@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512376&di=39f792203f27ff222fc4169ccd2b2510&imgtype=0&src=http%3A%2F%2Fpic.58pic.com%2F58pic%2F15%2F68%2F59%2F71X58PICNjx_1024.jpg",@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1550125512375&di=81eb34e98aab8012d83852957b9a0bda&imgtype=0&src=http%3A%2F%2Fwww.zt5.com%2Fuploadfile%2F2019%2F0127%2F20190127010113674.jpg"];

我们先看下实现部分

- (void)setImageURLStringsGroup:(NSArray *)imageURLStringsGroup
{
    _imageURLStringsGroup = imageURLStringsGroup;
    
    NSMutableArray *temp = [NSMutableArray new];
    [_imageURLStringsGroup enumerateObjectsUsingBlock:^(NSString * obj, NSUInteger idx, BOOL * stop) {
        NSString *urlString;
        if ([obj isKindOfClass:[NSString class]]) {
            urlString = obj;
        } else if ([obj isKindOfClass:[NSURL class]]) {
            NSURL *url = (NSURL *)obj;
            urlString = [url absoluteString];
        }
        if (urlString) {
            [temp addObject:urlString];
        }
    }];
    self.imagePathsGroup = [temp copy];
}

该部分就是得到一个temp数组,里边的元素都是string类型的图片链接。并用self.imagePathsGroup接收这个数组。我们看看self.imagePathsGroup的setter方法内部。

- (void)setImagePathsGroup:(NSArray *)imagePathsGroup
{
    [self invalidateTimer];
    
    _imagePathsGroup = imagePathsGroup;
    
    _totalItemsCount = self.infiniteLoop ? self.imagePathsGroup.count * 100 : self.imagePathsGroup.count;
    
    if (imagePathsGroup.count > 1) { // 由于 !=1 包含count == 0等情况
        self.mainView.scrollEnabled = YES;
        [self setAutoScroll:self.autoScroll];
    } else {
        self.mainView.scrollEnabled = NO;
        [self invalidateTimer];
    }
    
    [self setupPageControl];
    [self.mainView reloadData];
}

该部分是核心代码。首先判断self.infiniteLoop是否需要进行轮播,如果是_totalItemsCount赋值为要轮播的图片个数的100倍(之后会讲解为什么*100)如果不需要轮播就赋值为图片的个数。之后判断imagePathsGroup.count图片的个数如果为1个就不需要滚动,并停止定时器。如果是大于一个就要进行滚动并调用定时器滚动代码。之后调用了reloadData代码这时候将进行显示图片执行collectionView的dataSource。

#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    //item个数为_totalItemsCount个数(也就是如果需要轮播就是*100后的值,如三张图片这里就是300)
    return _totalItemsCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //获取注册过后的cell
    SDCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
    
    //获取当前的index(该方法内部是取余操作来获取当前的index)
    long itemIndex = [self pageControlIndexWithCurrentCellIndex:indexPath.item];
    
    //一下代码是自定义cell部分,如果编写了相应的delegate方法,那么将会用自定义的cell
    if ([self.delegate respondsToSelector:@selector(setupCustomCell:forIndex:cycleScrollView:)] &&
        [self.delegate respondsToSelector:@selector(customCollectionViewCellClassForCycleScrollView:)] && [self.delegate customCollectionViewCellClassForCycleScrollView:self]) {
        //这里执行自定义的纯代码的cell赋值(其实delegate部分这里进行了新的cell的注册,所以下次获取的cell是自己注册的cell)
        [self.delegate setupCustomCell:cell forIndex:itemIndex cycleScrollView:self];
        return cell;
    }else if ([self.delegate respondsToSelector:@selector(setupCustomCell:forIndex:cycleScrollView:)] &&
              [self.delegate respondsToSelector:@selector(customCollectionViewCellNibForCycleScrollView:)] && [self.delegate customCollectionViewCellNibForCycleScrollView:self]) {
        //这里执行自定义的xib的cell赋值(其实delegate部分这里进行了新的cell的注册,所以下次获取的cell是自己注册的cell)
        [self.delegate setupCustomCell:cell forIndex:itemIndex cycleScrollView:self];
        return cell;
    }
    
    //这是获取相应的image
    NSString *imagePath = self.imagePathsGroup[itemIndex];
    
    //下边进行赋值
    if (!self.onlyDisplayText && [imagePath isKindOfClass:[NSString class]]) {
        if ([imagePath hasPrefix:@"http"]) {
            [cell.imageView sd_setImageWithURL:[NSURL URLWithString:imagePath] placeholderImage:self.placeholderImage];
        } else {
            UIImage *image = [UIImage imageNamed:imagePath];
            if (!image) {
                image = [UIImage imageWithContentsOfFile:imagePath];
            }
            cell.imageView.image = image;
        }
    } else if (!self.onlyDisplayText && [imagePath isKindOfClass:[UIImage class]]) {
        cell.imageView.image = (UIImage *)imagePath;
    }
    
    //文字赋值
    if (_titlesGroup.count && itemIndex < _titlesGroup.count) {
        cell.title = _titlesGroup[itemIndex];
    }
    
    //进行一次配置
    if (!cell.hasConfigured) {
        cell.titleLabelBackgroundColor = self.titleLabelBackgroundColor;
        cell.titleLabelHeight = self.titleLabelHeight;
        cell.titleLabelTextAlignment = self.titleLabelTextAlignment;
        cell.titleLabelTextColor = self.titleLabelTextColor;
        cell.titleLabelTextFont = self.titleLabelTextFont;
        cell.hasConfigured = YES;
        cell.imageView.contentMode = self.bannerImageViewContentMode;
        cell.clipsToBounds = YES;
        cell.onlyDisplayText = self.onlyDisplayText;
    }
    
    return cell;
}

以上代码完成了collectionView来显示图片。这里来解释一下index的获取为什么要取余操作。

- (int)pageControlIndexWithCurrentCellIndex:(NSInteger)index
{
    /* 这里用两种情况
     * 第一种:不需要循环轮播,那么collectionView的总共item个数就是图片个数例如3个。那么该index入参的取值是0-2对self.imagePathsGroup.count取余后仍然是0-2,没问题。
     * 第二种:这里需要循环轮播,那么collectionView的总共item个数就是图片个数乘以100例如乘完后是300个。那么该index入参范围是0-299。其实就是一共有100组一样的三张图片。这样对图片个数也就是3,入参的index对3取余刚好可以得到当前那个图片的index(范围是0-2),没问题。
     *  总结:也就是这两种情况都可以取得到图片中的那个要显示的index
     */
    return (int)index % self.imagePathsGroup.count;
}

以上代码完成了轮播图的显示,结下来程序会执行到layoutSubviews方法,看看内部实现:

- (void)layoutSubviews
{
    self.delegate = self.delegate;
    
    [super layoutSubviews];
    
    _flowLayout.itemSize = self.frame.size;
    
    //collectionView的尺寸调整
    _mainView.frame = self.bounds;
    if (_mainView.contentOffset.x == 0 &&  _totalItemsCount) {
        //当前位置是开始的位置,并且_totalItemsCount有图片
        int targetIndex = 0;
        if (self.infiniteLoop) {
            //如果是要进行循环轮播(并当前位置是最左边位置),那么就取得collectionView滑动到一半那个位置的图片位置。
            targetIndex = _totalItemsCount * 0.5;
        }else{
            //如果不需要轮播那么还是最左边那个位置
            targetIndex = 0;
        }
        //滑动到需要的相应位置
        [_mainView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
    }
    
    //下边是对pageControl的操作就不一一分析了
    ........
}

这样代码执行到这里刚好collectionView滑动到了150这个item位置。该位置显示的刚好是第一张图片。这样该collectionView左边和右边有一样的滑动偏移量,我们手指可以随意的滑动。这样不考虑定时轮播的情况下已经实现了需求了。并且这样每次只加载了三个cell。即使我们给了5张图片,这里也只是加载三个cell,性能高。耗内存小。接下来我们看看定时轮播代码。

-(void)setAutoScroll:(BOOL)autoScroll{
    _autoScroll = autoScroll;
    
    //关闭定时器
    [self invalidateTimer];
    
    //如果需要定时轮播,就打开定时器
    if (_autoScroll) {
        [self setupTimer];
    }
}

- (void)setupTimer
{
    [self invalidateTimer]; // 创建定时器前先停止定时器,不然会出现僵尸定时器,导致轮播频率错误
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTimeInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES];
    _timer = timer;
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)automaticScroll
{
    if (0 == _totalItemsCount) return;
    //获取当前位置(该部分有个相当于四舍五入的操作)
    int currentIndex = [self currentIndex];
    //下一个要加载的图片
    int targetIndex = currentIndex + 1;
    //滑动到下一个图片位置
    [self scrollToIndex:targetIndex];
}

- (int)currentIndex
{
    if (_mainView.sd_width == 0 || _mainView.sd_height == 0) {
        return 0;
    }
    
    int index = 0;
    //横向滑动
    if (_flowLayout.scrollDirection == UICollectionViewScrollDirectionHorizontal) {
        /* 这里可以假设当前的collectionView(_mainView)的偏移量是刚好滑动到一张图片不到一半的位置,那么mainView.contentOffset.x + _flowLayout.itemSize.width * 0.5的结果将会是当前那个图片的偏移量+不足一个图片的宽度。之后再除以宽度,得到的将会是一个当前图片的inex+一个小与1的小数再取整得到的刚好是当前图片的index。结果就是:加入当前index是153,当我们向左滑动一个图片滑动不足一半时候通过该计算得到的依然是153。
         * 还有一种情况就是滑动的位置刚好是大于一个图片一半的位置通过计算得到的将会是154.也就是通过该计算可以进行四舍五入操作.
        */
        index = (_mainView.contentOffset.x + _flowLayout.itemSize.width * 0.5) / _flowLayout.itemSize.width;
    } else {
        //纵向滑动
        index = (_mainView.contentOffset.y + _flowLayout.itemSize.height * 0.5) / _flowLayout.itemSize.height;
    }
    
    return MAX(0, index);
}

- (void)scrollToIndex:(int)targetIndex
{
    if (targetIndex >= _totalItemsCount) {
        if (self.infiniteLoop) {
            //如果是循环轮播,上一个位置已经是最后一个item位置了,现在这次滚动要滚动到中间位置,也就是一开始的位置
            targetIndex = _totalItemsCount * 0.5;
            [_mainView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
        }
        //如果不是无线轮播,那么就不用管
        return;
    }
    //如果还没到最后位置,那么就开始滚动到入参的位置
    [_mainView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:YES];
}

以上代码是实现了定时轮播,轮播时候判断了是不是滚动到了最后的位置,如果是就滚回到初始的中间位置。

补充

我在用该框架进行开发时候发现没有提供相当于scrollViewDidScroll的代理。我做的处理是通过kvc得到了.m文件下的mainView(collectionView)在利用rac进行滚动的kvo实现了需求(在不改动源码的基础上)这里给了个该需求的解决方案哈。

总结

经过以上分析,我们了解了该框架的具体实现原理,我们分析的是最核心的部分,其实这个框架还有对文字,指示器的部分我们没去分析。通过分析,其实该框架并不难,它只是用到了collectionView并把图片个数乘以100作为collectionView的总item数。该框架利用了collectionView的cell重用机制提高了轮播图的性能,并提供了自定义cell的需求,也考虑到了指示器,文字,轮播,等等的个性化配置。