这是一种很常见的布局,可以使用CollectionViewFlowLayout,在其代理方法中通过相关设置来达到此效果,但还是比较麻烦。如若直接用CollectionViewLayout来实现,会简单不少,且灵活性较好。
其实此布局主要考虑的问题就两点:
1.实现任意尺寸的分页大小,即每次滑动后某一张卡片都能停在屏幕中间(默认collectionView开启pageEnable后是以其尺寸来作为分页大小)。
2.放大过程中实现每张卡片的缩放。
从官方文档得知自定义CollectionViewLayout至少需要重写以下方法:
// 当collectionView滑动的时候,可见区域改变的时候是否使当前布局失效以重新布局
1.shouldInvalidateLayoutForBoundsChange:
// 需要在此方法中返回collectionView的内容大小
2.collectionViewContentSize
// 为每个Cell返回一个对应的Attributes,我们需要在该Attributes中设置对应的属性,如Frame等
3.layoutAttributesForItemAtIndexPath:
// 可在此方法中对可见rect中的cell的属性进行相应设置
4.layoutAttributesForElementsInRect:
我们就以横向滚动的布局为例来实现此布局,首先,定义好所需的属性:
@property (nonatomic, assign) CGFloat spacing; //cell间距
@property (nonatomic, assign) CGSize itemSize; //cell的尺寸
@property (nonatomic, assign) CGFloat scale; //缩放率
@property (nonatomic, assign) UIEdgeInsets edgeInset; //边距
接下来是重写上面提到的几个方法
1. shouldInvalidateLayoutForBoundsChange:
// 由于是此布局是平铺的效果,所以当collectionView的bounds变化时,所展现的cell的个数及显示效果可能会发生变化,故此方法应返回YES。
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
2. collectionViewContentSize
// 由于此布局一般只有一个section,故在此例中仅考虑只有一个section的情况
// 内容的宽度为: 左边距 + n*cell的宽 + (n-1)*cell的间距 + 右边距。
- (CGSize)collectionViewContentSize {
NSInteger count = [self.collectionView numberOfItemsInSection:0];
CGFloat width = count*(self.itemSize.width+self.spacing)-self.spacing+self.edgeInset.left+self.edgeInset.right;
CGFloat height = self.collectionView.bounds.size.height;
return CGSizeMake(width, height);
}
3.layoutAttributesForItemAtIndexPath:
// 此方法要求返回一个UICollectionViewLayoutAttributes * 类型的对象
// 该对象包含对应cell外观所需的必要属性,包括center、frame、transform、alpha及其他属性
// 在此方法中只需要做一件事,那就是给cell设置好正确的frame。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attribute.size = self.itemSize;
CGFloat x = self.edgeInset.left + indexPath.item*(self.spacing+self.itemSize.width);
CGFloat y = 0.5*(self.collectionView.bounds.size.height - self.itemSize.height);
attribute.frame = CGRectMake(x, y, attribute.size.width, attribute.size.height);
return attribute;
}
4.layoutAttributesForElementsInRect:
// 可在此方法中设置缩放效果
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *indexPaths = [self indexPathsInRect:rect];
//找到屏幕中间的位置
CGFloat centerX = self.collectionView.contentOffset.x + 0.5*self.collectionView.bounds.size.width;
NSMutableArray *attributes = [NSMutableArray array];
for (NSIndexPath *indexPath in indexPaths) {
UICollectionViewLayoutAttributes* attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
// 判断可见区域和此cell的frame是否有重叠,因为indexPathsInRect返回的indexPath并不是十分准确。
if (!CGRectIntersectsRect(rect, attribute.frame)) {
//若不重叠则无需进行以下的步骤
continue;
}
[attributes addObject:attribute];
//计算每一个cell离屏幕中间的距离
CGFloat offsetX = ABS(attribute.center.x - centerX);
//这是设置一个缩放区域的阈值,当cell在此区域之外不进行缩放,改值可视具体情况进行修改。
CGFloat space = self.itemSize.width+self.spacing;
if (offsetX<space) {
CGFloat scale = 1+(1-offsetX/space)*(self.scale-1);
attribute.transform = CGAffineTransformMakeScale(scale, scale);
// 设置此属性是为了当cell层叠后,使得位于中间的cell总是位于最前面,若不明白可将此行注释一试便知。
attribute.zIndex = 1;
}
}
return attributes;
}
- (NSArray *)indexPathsInRect:(CGRect)rect {
NSInteger leftIndex = (rect.origin.x-self.edgeInset.left)/(self.itemSize.width+self.spacing);
NSInteger rightIndex = (CGRectGetMaxX(rect)-self.edgeInset.left)/(self.itemSize.width+self.spacing);
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
preIndex = preIndex<0 ? 0 : preIndex;
latIndex = latIndex>=itemCount ? itemCount-1 : latIndex;
NSMutableArray *indexPaths = [NSMutableArray array];
for (NSInteger i=leftIndex; i<=rightIndex; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
[indexPaths addObject:indexPath];
}
return indexPaths;
}
至此,我们已经实现了在滚动过程中靠近屏幕中间的cell放大的效果,但是还没实现滚动停止时某一个cell正好在屏幕中间,要想实现此效果,需要在targetContentOffsetForProposedContentOffset:withScrollingVelocity
方法中实现此逻辑。此方法会给一个系统默认计算好的collectionView应该停下来的位置,返回一个collectionView最后要停下来的位置。
// 需要在此方法中获取默认情况下停止滚动时离屏幕中间最近的那个cell,并计算两者的距离,将此距离补到proposedContentOffset上即可。
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGRect rect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *attributes = [self layoutAttributesForElementsInRect:rect];
CGFloat centerX = proposedContentOffset.x + 0.5*self.collectionView.bounds.size.width;
CGFloat minOffsetX = MAXFLOAT;
for (UICollectionViewLayoutAttributes* attribute in attributes) {
CGFloat offsetX = attribute.center.x - centerX;
if (ABS(offsetX) < ABS(minOffsetX)) {
minOffsetX = offsetX;
}
}
return CGPointMake(proposedContentOffset.x + minOffsetX, proposedContentOffset.y);
}
现在已经实现了文章开头动图中展示的效果了。再将之前定义的各个属性的set方法重写,以便在设置这些属性的时候进行重新布局。
注意:若想实现多个section以及含有sectionHeader或sectionFooter,请参照此思路来实现,并且重写以下两个方法:
// 含有sectionHeader或sectionFooter应重写此方法
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
// cell含有装饰视图时要重写此方法。
layoutAttributesForDecorationViewOfKind:atIndexPath:
想要源码的可以点击这里获取,GitHub上的版本支持水平和垂直布局且已加以优化。
此外还有Swift版本的哦,请点击这里以获取。
若觉得对你有用的话,还请给个star哈~