iOS 利用UICollectionView做一个无限循环广告栏
一、效果图
左右丝滑滑动,并且有缩放动画。
二、分析和思路
1. 为什么选择用UICollectionView去做上面的效果?
首先无限效果永远是表现出来的,而不是程序里面创建了无数个view,如何做到无限效果的视觉差这本身就是一个技术活。
以我的知识水平,可以做无限效果的有三种方式:
1). 三个view + 滑动手势。原理图如下:
mid下面的承载view为工作区,负责添加滑动手势和根据手势滑动距离去修改left,mid,right三个view的位置和状态。当手势滑动结束的时候,需要在关闭隐式动画的基础上修改三个页面显示的内容并且重置三个view的位置和状态。
优点:逻辑简单,三个view的切换也不费事,比较的节省资源,因为是手势控制的,所以落点位置都比较好定位;
缺点:只支持全景展示,不能做到一个显示框中显示三个item(否则就要用超过3个view的了),另外如果要实现手势的快滑和慢滑则又是另外一番努力了。
2). UIScrollView:
UIScrollView 就是在 1) 的基础上去掉自己添加的手势,用UIScrollView作为控制载体。而且UIScrollView提供了良好的滑动代理,有大量的api可以使用,只要用心是可以做出很多酷炫的效果。
当然了,问题也很明显,代码控制复杂;做无限滚动的时候,到底创建多少个item比较合适?
3). UICollectionView:
用UICollectionView最大原因就是cell的重用规则,可以让你不去关心到底创建多少个item。而且UICollectionView是可以左右滑的。原理大约如下:
滑动表现出来的无限只是因为在滑动停止后,重置了section。比如你想要sectionCount个section,section里面的rows就是广告的个数,初始状态下,一定是让屏幕中央显示的是indexPath为(sectionCount/2 ,0)的item,这样当向前滑动和向后滑动的时候就不会出问题,为了在快滑过程中更加丝滑柔顺,尽量的让sectionCount大点,,因为cell的重用机制让你可以不考虑这儿的内存消耗问题,我这儿给的是100。
在滑动动画结束之后,需要关闭隐式动画条件下重置当前的滑动位置为indexPath=(sectionCount/2,currentRow),时机我选择在NSTimer的处理函数里面。
到这儿基本上把无限循环的逻辑讲完了,再说一下item的缩放效果实现。
从最开始给的效果图可以很容易得到一个结论:距离中心点位置越大,缩放比越大,屏幕中心点的缩放比最小为0,所以我们需要获取当前点到屏幕中心的distance;
这个也是比较简单的,在 UICollectionViewFlowLayout 回调函数
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
中可以轻松拿到,代码如下:
CGRect frame = CGRectZero; frame.origin = self.collectionView.contentOffset; frame.size = self.collectionView.size; for (UICollectionViewLayoutAttributes *attribute in array) { //确立cell相对于屏幕中央的距离 CGFloat distance = CGRectGetMidX(frame) - attribute.center.x; //公式一 }
为了计算出来下一步的缩放比,我们这儿需要约定一个参考宽度activeDistance,即,当前distance等于activeDistance时,缩放比最小到0,(离得越近,显示的越小,很符合人眼的观察习惯),为了更好的理解参考宽度,我们可以这样约定,距离中心距离为activeDistance,缩放比为scaleFactor,这样缩放比的计算公式为:
CGFloat scale = 1 - (distance/activeDistance)*scaleFactor; 公式二
activeDistance和scaleFactor的值需要自己多次尝试,修改,我这儿给个建议值:
activeDistance = 300.0;
scaleFactor = 0.05;
最后把求出来的scale赋值给cell的attribute,完整代码如:
1 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { 2 3 NSArray *array = [super layoutAttributesForElementsInRect:rect]; 4 5 CGRect frame = CGRectZero; 6 frame.origin = self.collectionView.contentOffset; 7 frame.size = self.collectionView.size; 8 9 for (UICollectionViewLayoutAttributes *attribute in array) { 10 //确立cell相对于屏幕中央的距离 11 CGFloat distance = CGRectGetMidX(frame) - attribute.center.x; 12 13 //到中心位置的相对于x的比例,原则就是越近的越大,越远的越小。 14 CGFloat normalDistance = fabs(distance / self.activeDistance); 15 16 CGFloat scale = 1 - self.scaleFactor * normalDistance; 17 //属性赋值 18 attribute.transform3D = CATransform3DMakeScale(scale,scale, 1); 19 } 20 return array; 21 }
到这儿基本上就完了,除了最后一步,因为你把上述代码运行起来的时候就会发现,可以缩放,可以无限滚动,但是每次滚动之后item都不会自己跑到屏幕中央,这就需要修改回调
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
这的逻辑很简单,在滑动结束后,屏幕中可能存在N个cell,从这N个cell中选择出离屏幕中央最近的cell,把选择出来的这个cell居中显示,就可以了。具体代码如下:
1 // 确定最终滚到的位置 2 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { 3 4 CGRect targetRect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height); 5 6 NSArray *array = [super layoutAttributesForElementsInRect:targetRect]; 7 8 CGFloat horizontalCenterX = proposedContentOffset.x + self.collectionView.bounds.size.width / 2.; 9 CGFloat offsetAdjustment = CGFLOAT_MAX; 10 for (UICollectionViewLayoutAttributes *attribute in array) { 11 // 12 CGFloat tempCenterX = attribute.center.x; 13 if (fabs(horizontalCenterX - tempCenterX) < fabs(offsetAdjustment)) { 14 offsetAdjustment = tempCenterX - horizontalCenterX; 15 } 16 } 17 18 CGPoint resultPoint = CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y); 19 20 return resultPoint; 21 }
三、结束
现在是真的完了,实现的思路和关键代码都列出来了,这儿再附一个demo地址:
https://github.com/goldBreak/Demos