SDCycleScrollView源碼解析

基本使用

  • 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內部的初始化代碼以下:bash

- (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方法內部ide

// 設置顯示圖片的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進行了優化使得性能更高。這樣咱們就瞭解了該框架的初始化部分。oop

  • 開始輪播顯示部分
    通過初始化後,將要進行顯示圖片並開始輪播了。如下代碼實現了該功能:
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方法,看看內部實現:ui

- (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的需求,也考慮到了指示器,文字,輪播,等等的個性化配置。

相關文章
相關標籤/搜索