来自Leo的原创博客,转载请著名出处
我的StackOverflow
我的Github
https://github.com/LeoMobileDeveloper
注意:本文的代码是用Swift 2.2写的。
视差效果
什么是视差效果?我们来看下格瓦拉的App,就知道了
格瓦拉的视差效果算是比较明显的。所谓视差效果,就是看起来在”上面”的视图滚动的速度大于”底层”的时图滚动。所以,给人的视觉体验要比”屌丝”的滚动效果好不少。
ParallexBanner
之前项目赶进度,一直用的开源的。最近刚好在复习Swift,脑袋一热,就自己写了个。
实现视图轮播的几种方式
视图轮播没什么难度,大致分为几种实现方式
大致分析了下。
- 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