iOS 轮播图的几种实现

大概的实现思路有这么几种:
1 . UIScrollView + UIImageView
1.1. 使用比 图片数 多 2 个的 UIImageView
1.2 使用三个UIImageView 实现
1.3 使用 两个 UIImageView实现
2.UICollectionView (当然UICollectionView也是继承自UIScrollView)
2.1 使用UICollectionView 的复用特性,复制 多份 图片作为数据源,从中间开始显示
2.2 在 第一张图片前加 上最后一张图片,在最后一张图片前加上第一张图片
注意:
1.记得设置 scrollView.isPagingEnabled = true
2. 控制 UIScrollView 回滚的时候 不要使用动画,否则会调用代理方法

无限轮播示意图
1.1 使用比 图片数 多 2 个的 UIImageView 实现

思路 : 创建 比图片数多 2个UIImageView ,最一个ImageView显示第一张图片,第一个ImageView显示最后一张图片,从第二个到倒数第二个ImageView依次显示第一张到最后一张图片 当滚动到最后一张图片, 即 :原始 图片顺序为 1,2,3,4,5,6 ,则八个ImageView 显示图片的顺序为6,1,2,3,4,5,6,1,依次把ImageView添加到UIScrollView上,最开始显现第二个ImageView,当滚动到第一个 ImageView的时候,控制UIScrollView滚动到倒数第二个 ImageView,滚动到最后一个 ImageView的时候,控制UIScrollView滚到第二个ImageView ,只有 中间的 是正式用来显示的.两头的用来实现一个视差效果
核心代码如下

 private lazy var mainScrollView : UIScrollView = {
        let scrollView = UIScrollView(frame: bounds)
        let scrollVWidth = bounds.width * CGFloat(images.count + 2)
        scrollView.contentSize = CGSize(width: scrollVWidth, height: bounds.height)
        scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        scrollView.delegate = self
        scrollView.bounces = true
        scrollView.isPagingEnabled = true
        for (index, imageV) in imageVs.enumerated() {
            imageV.frame =  CGRect(x: bounds.width  * CGFloat(index), y: 0, width: bounds.width, height: bounds.height)
            scrollView.addSubview(imageV)
        }
        return scrollView
    }()
// images 为根据传 过来的图片再在头尾分别加上最后一张和第一张图片创建的新数组
 func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.x >= (bounds.width  *  CGFloat(images.count - 1)) {
            mainScrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        }
        if scrollView.contentOffset.x <= 0 {
            mainScrollView.setContentOffset(CGPoint(x: bounds.width * CGFloat(images.count - 2), y: 0), animated: false)
        }
    }
1.2 使用三个UIImageView 实现

思路: 方法1.1 的进化版,方法1.1 在有大量图片的情况下,需要创建大量的ImageView 明显不合适,所以我们 把中间的一个ImageView片来代替 1.1中的 大量中间图片,即只有中间一张图片是用来正式显示图片的,每次滚动结束都需要滚回到 中间图片位置,两边的ImageView 用来实现视差效果
核心代码如下

// 这里为每个 ImageView 添加一个 tag ,用来标识当前显示的图片和后面计算将要显示的图片
  private lazy  var  leftImageV :  UIImageView = {
        let imageV = UIImageView(frame: bounds)
        imageV.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
        imageV.tag = images.count - 1
        imageV.image = images[images.count - 1]
        return imageV
    }()
    private lazy  var  MidleImageV :  UIImageView = {
        let imageV = UIImageView(frame: bounds)
         imageV.frame = CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height)
          imageV.tag = 0
          imageV.image = images[0]
        return imageV
    }()
    private lazy  var  RightImageV :  UIImageView = {
        let imageV = UIImageView(frame: bounds)
         imageV.frame = CGRect(x: bounds.width * 2, y: 0, width: bounds.width, height: bounds.height)
          imageV.tag = 1
          imageV.image = images[1]
        return imageV
    }()
    private lazy var imageVs : Array<UIImageView> = {
        return [leftImageV,RightImageV,MidleImageV];
    }()
    
    private lazy var mainScrollView : UIScrollView = {
        let scrollView = UIScrollView(frame: bounds)
        let scrollVWidth = images.count >= 3 ? bounds.width * 3 : bounds.width * CGFloat(images.count)
        scrollView.contentSize = CGSize(width: scrollVWidth, height: bounds.height)
        scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        scrollView.delegate = self
        scrollView.bounces = true
        scrollView.isPagingEnabled = true
        scrollView.addSubview(leftImageV)
        scrollView.addSubview(MidleImageV)
        scrollView.addSubview(RightImageV)
        return scrollView
    }()

 func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        //判断 滚动方向,右滚下标 +1 ,左滚下标 -1
        var  direction  : Int = 0
        if scrollView.contentOffset.x > bounds.width {
            direction = 1
        }else if  scrollView.contentOffset.x < bounds.width{
            direction = -1
        }
      let imageVs = [leftImageV,MidleImageV,RightImageV]
        // 根据图片tag 和滚动方向 计算 要显示的 图片,更新 tag 值
        for  imageV in  imageVs {
            var  index  = imageV.tag + direction
            if index == images.count {
                index = 0
            }else if index == -1{
                index = images.count - 1
            }
            imageV.image = images[index]
            imageV.tag = index
        }
        mainScrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
    }
1.3 使用 两个 UIImageView实现

思路: UIScrollViewcontentSize 依然 为 三倍 屏幕宽度,创建两个 ImageView 添加到 UIScrollView 上, 没有滚动时,两个imageView重叠,上面的ImageView显示当前图片,发生滚动的时候,判断滚动方向,把底下的ImageView移到 左边或者右边,滚动结束,UIScrollView 滚动 回 中间位置
核心代码如下:

     private  var isChange : Bool = true
    let images : Array<UIImage>
    lazy var  bottomImageView : UIImageView = {
        let imageView = UIImageView(image: UIImage(named: "2.png"))
        imageView.frame = CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height)
        imageView.tag = 0
        return imageView
    }()
    
    lazy var  topImageView : UIImageView = {
        let imageView = UIImageView(image: UIImage(named: "1.png"))
        imageView.frame = CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height)
         imageView.tag = 0
        return imageView
    }()
    
    lazy var  scrollView : UIScrollView = {
        let scroView = UIScrollView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
        scroView.contentSize = CGSize(width: bounds.width * 3, height: bounds.height)
        scroView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        scroView.isPagingEnabled = true
        scroView.bounces = false
        scroView.delegate = self
        scroView.addSubview(bottomImageView)
        scroView.addSubview(topImageView)
        return scroView
    }()
 
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
         imageOffset = scrollView.contentOffset.x
    }
   // 每次滚动的时候 判断 滚动方向,调整底下的 imageview  的位置和显示的图片
   
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if isChange{
        if scrollView.contentOffset.x < bounds.width {
            imageOffset =  scrollView.contentOffset.x
            var tag = bottomImageView.tag - 1
            if tag < 0{
                tag = images.count - 1
            }
            bottomImageView.image = images[tag]
            bottomImageView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
            bottomImageView.tag = tag
        }else if  scrollView.contentOffset.x > bounds.width {
            imageOffset = scrollView.contentOffset.x
            var tag = bottomImageView.tag + 1
            if tag  == images.count{
                tag = 0
            }
            bottomImageView.image = images[tag]
            bottomImageView.frame = CGRect(x: bounds.width * 2, y: 0, width: bounds.width, height: bounds.height)
            bottomImageView.tag = tag
        }
        }
        isChange = false
    }
    
   //滚动结束, scrollView 回滚到中间位置,更新 上面的 ImageView 的图片
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
                bottomImageView.frame = CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height)
                topImageView.frame = CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height)
                print(bottomImageView.tag)
                topImageView.image = images[bottomImageView.tag]
        scrollView.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        isChange = true
    }

这里 因为 scrollViewDidScroll 代理滚动过程中会一直调用,而我这里只需要判断 一下滚动方向然后调整 底下的ImageView, 所以用的 一个BOOL值来限制执行,当时没想到一个比较好的方案,所以实现看起来比较low

2.1 使用UICollectionView 的复用特性,复制 多份 图片作为数据源,从中间开始显示

思路: 复制 多份 需要显示的图片 作为UICollectionView数据源,开始的时候 就把scrollView 滚动到中间那份 数据的开头位置,因为复制的多份数据,所以滚动的时候就会无限轮播的效果,这里就不贴具体代码,大家可以自行 尝试下

2.2 在 第一张图片前加 上最后一张图片,在最后一张图片前加上第一张图片

思路:1.1 方法类似,只是使用 UICollectionView 的话因为UICollectionView自身的复用机制,所以没有 1.1方法 创建大量 ImageView的性能问题,同样的,在首尾 分别加上最后和第一张图片,当 滚动到这两个位置的时候,控制 scrollView滚动到中间显示区的对应位置
核心代码如下:

   let images : Array<UIImage>
//创建  UICollectionView 
    lazy var collectionV : UICollectionView = {
        let layout  = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        layout.itemSize = CGSize(width: bounds.width, height: bounds.height)
        layout.scrollDirection = .horizontal
        let collectionV  = UICollectionView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height), collectionViewLayout: layout)
        collectionV.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionV.dataSource = self
        collectionV.delegate = self
        collectionV.isPagingEnabled = true
        return collectionV
    }()
    //根据传进来的 图片数组构建 数据源数组
    init(frame: CGRect,images :Array<UIImage> ) {
        var tempImages : Array<UIImage> = [images[images.count - 1]]
        for  image in images {
            tempImages.append(image)
        }
        tempImages.append(images[0])
        self.images = tempImages
        super.init(frame: frame)
        addSubview(collectionV)
        collectionV.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
    }

extension XlXBannerCollectionV : 
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.imageV = UIImageView(image: images[indexPath.row])
        return cell
    }
    
// 滚动到 首尾的时候控制回滚
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.x >= (bounds.width  *  CGFloat(images.count - 1)) {
            collectionV.setContentOffset(CGPoint(x: bounds.width, y: 0), animated: false)
        }
        if scrollView.contentOffset.x <= 0 {
             collectionV.setContentOffset(CGPoint(x: bounds.width * CGFloat(images.count - 2), y: 0), animated: false)
        }
    }
}

无限轮播 整体 上就是 中间部分 是用来正式显示的,首尾的是 在滚动的时候让用户有种 后面还有内容的错觉,当滚动停止,立马回滚到中间显示区的对应位置,来达到无限轮播的视觉效果

自动轮播的话在 scrollView 代理方法里面合适的位置开启或暂停定时器来实现,代码中剔除了其他干扰只保留了核心部分,因为功能比较简单,所以也没注意代码卫生,实际开发中,各位小伙伴们还是要多多注意下代码习惯,抽取封装,配置工具类什么的来实现的漂亮一点

最后:写完了才想起来两张图片的情况下没做处理,可能还是会有些问题吧,道理都是一样的,实际开发中多多注意下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358