iOS卡片控件的实现

###需求
CardPageDemo

最近公司给了个需求,实现上面卡片效果,这种控件网上也有现成的demo,但有些地方还是要自己研究下才能满足使用,下面记录一下我的研究成果。

1.使用UICollectionView控件。

如果这是单纯的轮播图片,使用UIScrollView就可以满足需要,但是上面的效果有缩放还有渐变,用UIScrollView的话代码量大,实现比较复杂,而且数据处理起来也没有用UICollectionView方便,这里再放一个用UIScrollView实现的demo,有需要的可以对比一下。

2.自定义UICollecionViewFlowLayout类。

使用过UICollectionView的同学们对这个类应该都不陌生吧,瀑布流布局类,可以通过自定义这个类实现各种瀑布流的效果,我要实现的这个效果当然也是需要自定义这个类了。

先来看看prepareLayout这个函数的实现吧,这个是布局初始化的时候调用的,可以在这个函数里面自定义每一个cell的大小,cell之间的间距等属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)prepareLayout{
[super prepareLayout];
//滑动方向
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
//两个cell的间距
self.minimumLineSpacing = kLineSpace;
//计算cell超出显示的宽度
CGFloat width = ((self.collectionView.frame.size.width - kPageCardWidth)-(kLineSpace*2))/2;
//第一个cell和最后一个cell相对于屏幕的偏移
self.sectionInset = UIEdgeInsetsMake(0, kLineSpace+width, 0, kLineSpace+width);
//每个cell实际的大小
self.itemSize = CGSizeMake(kPageCardWidth,kPageCardHeight);
}

结合下面这两张图片,几个属性的关系就很清楚了:

首尾cell
首尾cell

中间cell
中间cell

要保证每一次cell滑动之后都能精确的移动到屏幕的中间,就必须计算出图上标的这几个参数的值,这里有两种情况:

1.cell宽度固定。

当cell的宽度是一个常量,两个cell之间的间距(minimumLineSpacing)一般都是不变的,很容易计算出前后两个cell超出显示在屏幕上的宽度 CGFloat width = ((self.collectionView.frame.size.width - kPageCardWidth)-(kLineSpace*2))/2;(参考中间cell这张图),首尾情况时,只要让sectionInset左右的偏移量 = width+minimumLineSpacing,左右的空白宽度等于了另一边的非空白的宽度(参考首尾cell这张图),这样无论滑到哪个cell,就能精确计算出移动到屏幕中间所需要的偏移量了。

2.前后cell超出显示在屏幕上的宽度(图中的width)固定。
这种情况就要根据屏幕宽度计算出cell的宽度kPageCardWidth = self.collectionView.frame.size.width - (kLineSpace x 2) -(width x 2); sectionInset左右的偏移量计算方法和第一种情况一样。

我这里的cell的宽度是固定值,所以计算起来相对简单,如果要让每个cell都在屏幕的中心点,对于第一个cell,只需要sectionInset左右边的偏移量 = width + minimumLineSpacing。

而对于后面的cell,则需要通过控制每一次滑动后当前显示cell的中心坐标。这个坐标当然是有方法来返回的,看下面这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
// 分页以1/3处
if (proposedContentOffset.x > self.previousOffsetX + self.itemSize.width / 3.0) {
self.previousOffsetX += kPageCardWidth+kLineSpace ;
self.pageNum = self.previousOffsetX/(kPageCardWidth+kLineSpace);
if ([self.delegate respondsToSelector:@selector(scrollToPageIndex:)]) {
[self.delegate scrollToPageIndex:self.pageNum];
}
} else if (proposedContentOffset.x < self.previousOffsetX - self.itemSize.width / 3.0) {
self.previousOffsetX -= kPageCardWidth+kLineSpace;
self.pageNum = self.previousOffsetX/(kPageCardWidth+kLineSpace);
if ([self.delegate respondsToSelector:@selector(scrollToPageIndex:)]) {
[self.delegate scrollToPageIndex:self.pageNum];
}
}
//将当前cell移动到屏幕中间位置
proposedContentOffset.x = self.previousOffsetX;
return proposedContentOffset;
}

这个函数返回当前cell的中心点移动到哪个坐标点。

以第一个cell为基准(第一个cell已经在屏幕的中心),后面的每一个cell如果要移动到屏幕中心,X方向上的位移应该为2个cell中心点之前的距离,即previousOffsetX = kPageCardWidth(cell的宽度)+kLineSpace(2个cell的间距)。

OK,完成了一半了,然后就是缩放和渐变。

直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *superAttributes = [super layoutAttributesForElementsInRect:rect];
NSArray *attributes = [[NSArray alloc] initWithArray:superAttributes copyItems:YES];
CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x,
self.collectionView.contentOffset.y,
self.collectionView.frame.size.width,
self.collectionView.frame.size.height);
CGFloat offset = CGRectGetMidX(visibleRect);
[attributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attribute, NSUInteger idx, BOOL * _Nonnull stop) {
CGFloat distance = offset - attribute.center.x;
// 越往中心移动,值越小,那么缩放就越小,从而显示就越大
// 同样,超过中心后,越往左、右走,缩放就越大,显示就越小
CGFloat scaleForDistance = distance / self.itemSize.width;
// 0.1可调整,值越大,显示就越大
CGFloat scaleForCell = 1 + 0.1 * (1 - fabs(scaleForDistance));
//只在Y轴方向做缩放
attribute.transform3D = CATransform3DMakeScale(1, scaleForCell, 1);
attribute.zIndex = 1;
//渐变
CGFloat scaleForAlpha = 1 - fabsf(scaleForDistance)*0.4;
attribute.alpha = scaleForAlpha;
}];
return attributes;
}

上面这个函数返回一个UICollectionViewLayoutAttributes *类型的数组,简单点说就是滑动过程中动态计算每一个cell的属性然后传给UICollectionView显示出来,在这里动态修改每个cell的属性,显示的结果就是渐变和动画的效果了,具体可参考代码注释。根据位移控制好属性改变的系数即可。


###补充需求--轮播

轮播

还是原来的配方,还是熟悉的问题,轮播还是用的前后各加一个数据源的方式实现。具体参考我下面的demo。

这里我总结一下,如果要让轮播的过度看上去不那么明显应该注意两点:

1.应该让 前后cell超出显示在屏幕上的宽度(图中的width)固定 即上面说的第二种情况,且width的大小不宜过大。

2.cell的背景色和UICollectionView的背景色色差不宜过大。


###轮播补充
上面这种实现方式在处理最后一张到第一张的衔接的时候效果不太好,最近研究了下,比较好的实现方式是放多个section,初始化的时候手动滚动到中间section,每个section内容相同,这样在滑动的时候就不会出现上面跳一下的问题。这里要注意的就是处理好每个section之间的间距问题,具体参考我的demo。
新demo的效果如下:
轮播

这里是我的demo,请下载