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

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

我的StackOverflow

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

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

视差效果

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

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

ParallexBanner

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

支持

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

效果

实现视图轮播的几种方式

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

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

大致分析了下。

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

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

我们再来看看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

相关文章

软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
【Android App】实战项目之仿抖音的短视频分享App(附源码和...
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
因为我既对接过session、cookie,也对接过JWT,今年因为工作...