自定义流水布局collectionViewFlowLayout
- 本文主要讲解如何进行自定义流水布局来实现一个相册功能,如下图所示:
一、
想要实现类似这样的相册功能有三种办法
1> 利用scrollView来写,添加三个imageView,然后实时监控scrollView的滚动,一旦有imageView离开就立刻放入缓存池中以便用来复用(这种方法属于比较麻烦的)
2> 利用tableView,但是tableView只支持竖直滚动,不支持水平,但是可以改变tableView的transfrom属性来实现(这种方法看起来有点不合常理)
3> 利用Apple自带的collectionView(iOS6之后就用得比较广泛了),collectionView是一种比较牛逼的控件,利用好它我们可以做出很多漂亮的界面,collectionView默认是垂直滚动,但是它也支持水平滚动,而且也有重用机制,我们只需要负责填充数据,控制cell的缩放罢了(因此collectionView是首选)
TableView和CollectionView的排布区别
1> tableView的排布是一行一行往下排布,而collectionView的排布完全取决于Laytout,怎样的Laytout布局决定了显示怎样的cell
2> 因此我们可以看出,想要让界面显示得更加好看(如瀑布流等),就得自定义布局了
接下来就开始实现功能啦!
二、实现如下图样式
首先实现能展现数据并且水平滚动
1> viewDidLoad方法中创建collectionView,并且注册cell
- (void)viewDidLoad { [super viewDidLoad]; CGFloat w = self.view.bounds.size.width; CGRect collectionViewFrame = CGRectMake(0, 100, w, 200); // 创建collectionView UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:collectionViewFrame collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]]; // 设置代理和数据源 collectionView.delegate = self; collectionView.dataSource = self; // 注册cell [collectionView registerNib:[UINib nibWithNibName:@"DSImageCell" bundle:nil] forCellWithReuseIdentifier:ID]; // 添加到控制器View中 [self.view addSubview:collectionView]; }
2> 创建images数组模型,实现collectionView的数据源方法
@property (nonatomic, strong) NSMutableArray *images;
- (NSArray *)images
{
if (_images== nil) {
self.images = [NSMutableArray array];
for (int i = 1; i < 20; i++) {
[self.images addObject:[NSString stringWithFormat:@"%d", i]];
}
}
return _images;
}
/**
* 每一组有多少个cell
*/
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.images.count;
}
/**
* 第indexPath位置上显示什么样的cell
*/
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DSImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
cell.image = self.images[indexPath.item];
return cell;
}
注意:这里我用的是自定义cell(DSImageCell),并且用了xib来展示item,以下是DSImageCell的.h和.m文件
@interface DSImageCell : UICollectionViewCell
@property (nonatomic, copy) NSString *image;
@end
@interface DSImageCell ()
@property (weak, nonatomic) IBOutlet UIImageView *iconView;
@end
@implementation DSImageCell
- (void)awakeFromNib
{
// 边框颜色
self.iconView.layer.borderColor = [UIColor whiteColor].CGColor;
// 边框宽度
self.iconView.layer.borderWidth = 3;
// 变宽圆角
self.iconView.layer.cornerRadius = 5;
// 剪切边框超出范围
self.iconView.layer.masksToBounds = YES;
}
- (void)setImage:(NSString *)image
{
_image = [image copy];
self.iconView.image = [UIImage imageNamed:image];
}
@end
因为在创建collectionView的时候在init方法中传入的collectionViewLayout是系统自带的UICollectionViewFlowLayout,所以默认是垂直滚动的,所以此时就得自定义流水布局了!
三.
自定义流水布局,创建一个DSLineLayout继承自UICollectionViewFlowLayout,在.m文件中实现如下需求:
1.cell的缩放 2.停止滚动后cell居中
1> 设置滚动方向,cell的大小,cell之间的间隔,还有第一个cell和最后一个cell居中(注意:初始化一般在prepareLayout方法中进行,不能在init方法中,因为init方法中collectionView是没有尺寸的),而且每一个cell都有自己的UICollectionViewLayoutAttributes属性,该属性可以控制cell的位置,大小等
/**
* 一些初始化的工作最好在这里实现
*/
- (void)prepareLayout
{
[super prepareLayout];
// 水平方向
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
// 设置item的宽高
self.itemSize = CGSizeMake(DSItemWH, DSItemWH);
// 设置cell之间的间距
self.minimumLineSpacing = DSItemWH - 40;
CGFloat inset = (self.collectionView.frame.size.width - DSItemWH) * 0.5;
// 设置组头和组尾的inset(让第一个和最后一个cell显示在最中间)
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset);
}
至于DSItemWH就是cell的默认宽高
static const CGFloat DSItemWH = 100;
这个时候就可以实现上面图片所展示的样式了,所以接下来就需要实现cell在滚动中的缩放(也就是需要时刻监听屏幕范围内cell,此时得重写两个方法)
第一个方法:判断是否要在collectionView的边界发生改变的时候重新布局cell,该方法默认返回NO,如果返回YES,内部会重新调用prepareLayout和layoutAttributesForElementsInRect方法获得所有cell的布局属性
/**
* 只要边界改变的时候重新布局性
*/
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
第二个方法:也就是刚才上面所说的layoutAttributesForElementsInRect方法,该方法可以在滚动中重新布局cell,然后返回rect范围的所有cell的UICollectionViewLayoutAttributes属性,所以想要实现滚动中控制cell的缩放就得在这个方法里面实现
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
return nil;
}
下面是实现的思路:首先取得屏幕可见的rect范围,然后从遍历所有cell的UICollectionViewLayoutAttributes,判断cell是否在屏幕内,然后根据cell的centerX和屏幕的centerX之间的距离来算出需要缩放的比例,然后赋值给cell的transfrom属性
/**
* 选中该rect内的所有子控件
*
* @param rect 所有item加起来的rect
*
* @return 所有item
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
// 0.屏幕的可见范围
CGRect visiableRect;
visiableRect.size = self.collectionView.frame.size;
visiableRect.origin = self.collectionView.contentOffset;
// 1.取出所有item的UICollectionViewLayoutAttributes
NSArray *attributeArray = [super layoutAttributesForElementsInRect:rect];
// 获取屏幕中点的X
CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
// 2.遍历所有的布局属性
for (UICollectionViewLayoutAttributes *attrs in attributeArray)
{
// 如果不在屏幕上,直接跳过(两个rect是否相交)
if (!CGRectIntersectsRect(visiableRect, attrs.frame)) continue;
// 获取item的中点X
CGFloat itemCenterX = attrs.center.x;
CGFloat scale = 1+ 0.8 * (1 - ABS(centerX - itemCenterX) / (self.collectionView.frame.size.width * 0.5));
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return attributeArray;
}
接下来,相册功能就差cell滚动停止后自动回到中点位置
在实现之前还得知道一个方法
/**
* 用来设置collectionView停止滚动那一刻的位置
*
* @param proposedContentOffset 原本collectionView停止滚动那一刻的位置
* @param velocity 滚动速度
*
* @return 想要停止滚动的位置
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
}
该方法设置collectionView停止滚动那一刻的位置, proposedContentOffset表示**原本**collectionView停止滚动那一刻的位置, velocity滚动速度,而返回值就是你想要设置的位置.接下来说下实现思路:
首先获取屏幕的centerX,然后获取滚动结束后还在屏幕里面所有cell的centerX,并且找出离屏幕中点X最近的cell,求出两个centerX之间的差值(不需要求绝对值,因为差值又可能为正/负),所以cell停下来的位置就是proposedContentOffset.x加上这段差值
// 1.获取屏幕中点的X
CGFloat centerX = proposedContentOffset.x + self.collectionView.frame.size.width * 0.5;
// 2.计算屏幕滚动最后一刻的位置(lastRect大小和collectionView的frame一样)
CGRect lastRect;
lastRect.origin = proposedContentOffset;
lastRect.size = self.collectionView.frame.size;
// 3.取出lastRect范围内的item属性
NSArray *attributeArray = [super layoutAttributesForElementsInRect:lastRect];
// 4.遍历lastRect范围内的item属性
CGFloat adjustContentOffet = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in attributeArray) {
// 选出其中item的中点X和collectionView中点X的绝对值最小的item出来
if (ABS(attrs.center.x - centerX) < ABS(adjustContentOffet)) {
adjustContentOffet = attrs.center.x - centerX; // 该差值可能为正/负
}
}
return CGPointMake(proposedContentOffset.x + adjustContentOffet, proposedContentOffset.y);
到此为止,整个相册功能就实现完毕了,以上代码基本上都在了,由于代码里面注释写得十分清楚,所以就不多以言语代替了,而且因为是系统自带的循环利用,所以用起来一点都不会卡!