Swift实现"视差效果"的视图轮播

前端之家收集整理的这篇文章主要介绍了Swift实现"视差效果"的视图轮播前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

来自Leo的原创博客,转载请著名出处

我的StackOverflow

我的Github
https://github.com/LeoMobileDeveloper

注意:本文的代码是用Swift 2.2写的。


视差效果

什么是视差效果?我们来看下格瓦拉的App,就知道了

格瓦拉的视差效果算是比较明显的。所谓视差效果,就是看起来在”上面”的视图滚动的速度大于”底层”的时图滚动。所以,给人的视觉体验要比”屌丝”的滚动效果好不少。


ParallexBanner

之前项目赶进度,一直用的开源的。最近刚好在复习Swift,脑袋一热,就自己写了个。

  • ParallexBanner
  • @H_301_29@

    支持

    • 循环滚动
    • 自动滚动
    • 本地图片和网络图片
    • 视差效果
    • Storyboard和纯Code布局
    • @H_301_29@

      效果


      实现视图轮播的几种方式

      视图轮播没什么难度,大致分为几种实现方式

      • 单纯的用ScrollView实现,然后一张一张图片subview添加进去。
      • UICollectionView实现
      • UIPageViewController实现
      • @H_301_29@

        大致分析了下。

        • ScrollView实现简单粗暴,但是有一个很大的问题,视图复用。因为是一次性addSubView进去的。所以,在图片较多的时候,内存占用较多。
        • UIPageViewController实现依赖于ViewController,而作为一个视图来说,还是轻量级比较好一点。
        • UICollectionView帮我们实现了复用,我们只需要关注轮播本身就可以了。
        • @H_301_29@

          So,
          本文就选用CollectionView实现吧。


          定义接口

          写一个功能或者业务的第一步,定义接口,想要整体的类分布,值传递的逻辑。(这个很重要)

          用Swift写代码要注意一点:Swift是一个面相协议编程的语言

          所以,Try start with protocol.

          视图轮播需要数据源传递进来,同样需要把点击和滚动事件传递出去。所以,我们就采用Cocoa Touch的常用设计模式:dataSource和delegate,定义如下

          @objc public protocol ParallexBannerDelegate {
              //点击事件
              optional func banner(banner:ParallexBanner,didClickAtIndex index:NSInteger)
              //滚动事件
              optional func banner(banner:ParallexBanner,didScrollToIndex index:NSInteger)
          }
          
          @objc public protocol ParallexBannerDataSource{
               //一共有几个
              func numberOfBannersIn(bannner:ParallexBanner)->NSInteger
              //每一个index处的图片,这里可以返回String或者UIImage类型
              func banner(banner:ParallexBanner,urlOrImageAtIndex index:NSInteger)->AnyObject
              //Placeholder
              optional func banner(banner:ParallexBanner,placeHolderForIndex index:NSInteger)->UIImage?
              //Image的ContentMode
              optional func banner(banner:ParallexBanner,contentModeAtIndex index:NSInteger)->UIViewContentMode
          }

          对了,我们要支持两种类型的滚动:普通滚动,和视差滚动。这里有两种方式试下,一种是用一个Bool来表示,另一种是用枚举。

          考虑到以后,我可能添加更多的滚动模式,这里用枚举表示。

          public enum ParallexBannerTransition{
              case Normal
              case Parallex
          }

          然后,我们还需要几个属性,暴露出来给用户设置。这时候的代码如下

          public class ParallexBanner: UIView {
          // MARK: - Propertys -
              public  weak var dataSource:ParallexBannerDataSource?
              public  weak var delegate:ParallexBannerDelegate?
              public  var transitionMode:ParallexBannerTransition = ParallexBannerTransition.Parallex
              public  var autoScroll:Bool = true
              public  var enableScrollForSinglePage = false
              public  var parllexSpeed:CGFloat = 0.4
              public  var autoScrollTimeInterval:NSTimeInterval = 3.0 
              public  let pageControl:UIPageControl = UIPageControl()
              private var _currentIndex = 1
              private var collectionView:UICollectionView!
              private var timer:NSTimer?
              private var flowLayout:UICollectionViewFlowLayout!
          
          // MARK: - Init -
              override public init(frame: CGRect) {
                  super.init(frame: frame)
                  commonInit()
              }
              public required init?(coder aDecoder: NSCoder) {
                  super.init(coder: aDecoder)
                  commonInit()
              }
          }

          视图布局

          在定义好接口之后,我们要考虑布局了。
          对于ParallexBanner来说,布局比较简单

          • 底层是一个UICollectionView
          • 上层是一个UIPageControl
          • @H_301_29@

            我们再来看看CollectionViewCell
            普通的滚动CollectionViewCell中只有一个UIImageView,为了实现”视差效果”,我们需要Cell本身也能够控制ImageView滚动。所以,我们用一个ScrollView来包含ImageView,通过控制ContentOffset来控制ImageView的滚动。

            public class BannerCell:UICollectionViewCell{
                let imageView = UIImageView()
                let scrollView = UIScrollView()
                override init(frame: CGRect) {
                    super.init(frame: frame)
                    commonInit()
                }
                private func commonInit(){
                    contentView.addSubview(scrollView)
                    scrollView.scrollEnabled = false
                    //这里要设置,不然这个scrollView会吃掉我们的触摸
                    scrollView.userInteractionEnabled = false
                    scrollView.addSubview(imageView)
                    imageView.contentMode = UIViewContentMode.ScaleAspectFill;
                }
                required public init?(coder aDecoder: NSCoder) {
                    fatalError("init(coder:) has not been implemented")
                }
                override public func layoutSubviews() {
                    super.layoutSubviews()
                    scrollView.contentSize = self.bounds.size;
                    scrollView.frame = self.bounds
                    imageView.frame = scrollView.bounds
                }
            }

            循环滚动

            用CollectionView实现基于Timer的滚动没什么难度。
            无非就是一行代码

            collectionView.scrollToItemAtIndexPath(nextIndx,atScrollPosition: UICollectionViewScrollPosition.None,animated: true)

            那么如何实现循环滚动呢?有很多种方式实现,本文采用在前后插入两个额外的数据来实现。比如我有三张图,

            然后,在前后各插入两张

            当我向右滚动,滚动到如图红色虚线的临街区域的时候,就把contentOffset调整到左边的位置

            同样,当我向左滚动到临界区域,就调整contentOffset到右侧区域

            这样就实现了循环滚动。
            对应代码

            public func scrollViewDidScroll(scrollView: UIScrollView) {
                    var offSetX = scrollView.contentOffset.x
                    let width = CGRectGetWidth(scrollView.bounds)
                    guard width != 0 else{
                        return
                    }
                    if offSetX >= width * CGFloat(self.dataSource!.numberOfBannersIn(self) + 2 - 1){
                        offSetX = width;
                        scrollView.contentOffset = CGPointMake(offSetX,0);
                    }else if(offSetX < 0 ){
                        offSetX = width * CGFloat(self.dataSource!.numberOfBannersIn(self) + 2 - 2);
                        scrollView.contentOffset = CGPointMake(offSetX,0);
                    }
                }

            视差效果

            视差效果还是比较简单实现的。我们获取当前在屏幕上的Cell,然后计算相对移动的距离,然后,把Cell本身的ImageView像相反方向按照Speed来移动。

            collectionView.visibleCells().forEach { (cell) in
                        if let bannerCell = cell as? BannerCell{
                           handleEffect(bannerCell)
                        }
                    }

            调整Cell中的ScrollView的ContentOffset

            private func handleEffect(cell:BannerCell){
                    switch transitionMode {
                    case .Parallex:
                        let minusX = self.collectionView.contentOffset.x - cell.frame.origin.x
                        let imageOffsetX = -minusX * parllexSpeed;
                        cell.scrollView.contentOffset = CGPointMake(imageOffsetX,0)
                    default:
                        break
                    }
                }

            总结

            到这里,基本的原理就讲解完了。其实,所谓的视差效果,就是合理的利用ScrollView。感兴趣的同学可以看看源代码,不到300行,很简单。地址:ParallexBanner

猜你在找的Swift相关文章