瀑布流UICollectionViewFlowLayout

609 阅读8分钟


第一篇:

现在我们要实现如下的效果:

\

1.首先创建瀑布流

   UICollectionView  *collectionView = [[ UICollectionView   alloc ] init ];
CGFloat  collectionWH=  self . view . frame . size . width ;
collectionView. frame  =  CGRectMake ( 0 ,  200 , collectionWH, collectionWH);
[ self . view   addSubview :collectionView];\

2.直接运行会报错

崩溃信息:  *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UICollectionView must be initialized with a non-nil layout parameter

原因:没有给它传布局,可以先给它传一个默认的布局 UICollectionViewLayout

\

3.将上面的代码改成如下形式

\

    CGFloat collectionWH= self.view.frame.size.width;
CGRect frame = CGRectMake(0, 200, collectionWH, collectionWH);
UICollectionView *collectionView = [[UICollectionView alloc]initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc]init]];
[self.view addSubview:collectionView];\

\

此时运行程序,是一个黑屏,没有任何东西.不要着急,因为此时没有数据源,我们给它设置数据源,通过 collectionView. dataSource  = self ;并设置响应的代理 UICollectionViewDataSource

\

    //注册cell
[collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:WJCellId];\

\

#pragma mark - <UICollectionViewDataSource>
// 返回 Item 个数
- ( NSInteger )collectionView:( UICollectionView  *)collectionView numberOfItemsInSection:( NSInteger )section
{


return   50 ;
}\

\

- ( UICollectionViewCell  *)collectionView:( UICollectionView  *)collectionView cellForItemAtIndexPath:( NSIndexPath  *)indexPath
{


UICollectionViewCell  *cell = [collectionView  dequeueReusableCellWithReuseIdentifier : WJCellId   forIndexPath :indexPath];
cell. backgroundColor  = [ UIColor   orangeColor ];
return  cell;
}\

此时,运行会看到效果

\

\

\

这样的显示方式是因为我们给它传入了流水布局,如果想要其他的布局,只需要改变layout就行了.至此,我们已经利用流水布局实现水平滚动了,但是我想在实现水平滚动的基础上加一些功能,所以这里我们需要自定义流水布局.

\

\

\

\

 //创建布局
WJLineLayout *layout = [[WJLineLayout alloc]init];
layout.itemSize = CGSizeMake(100, 100);
//水平滚动
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;

CGFloat collectionW= self.view.frame.size.width;
CGFloat collectionH= 200;
CGRect frame = CGRectMake(0, 150, collectionW, collectionH);
UICollectionView *collectionView = [[UICollectionView alloc]initWithFrame:frame collectionViewLayout:layout];
collectionView.dataSource =self;
[self.view addSubview:collectionView];\

\

当然这个效果离我们要实现的效果还是有很大的差距,在这里我们重写自定义布局的一些属性:

WJLineLayout.m

\

/**

*1.cell的放大缩小

*2.停止滚动时,cell的剧中.

*/


- ( instancetype)init
{


if (self = [super init]) {


/*
UICollectionViewLayoutAttributes *attrs;
1.一个cell对应一个UICollectionViewLayoutAttributes
2.UICollectionViewLayoutAttributes对象决定了cell的frame
*/
}
return self;
}


/*
*这个方法的返回值是一个数组(数组里面存放着rect范围内所有元素的布局属性)
*这个方法的返回值决定了rect范围内所有元素的排布(frame)
*/  
- ( NSArray  *)layoutAttributesForElementsInRect:( CGRect )rect
{


//    NSLog(@"%s",__func__);
NSArray  *array =[ super   layoutAttributesForElementsInRect :rect] ;
return  array;
}\

\

\

此时运行的话会打印一下信息

2016-07-09 09:28:37.306 CollectionViewDemo[1047:55703] (
"<UICollectionViewLayoutAttributes: 0x7fbbb252ab30> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (0 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252ae60> index path: (<NSIndexPath: 0xc000000000200016> {length = 2, path = 0 - 1}); frame = (110 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252afa0> index path: (<NSIndexPath: 0xc000000000400016> {length = 2, path = 0 - 2}); frame = (220 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252b0e0> index path: (<NSIndexPath: 0xc000000000600016> {length = 2, path = 0 - 3}); frame = (330 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252b320> index path: (<NSIndexPath: 0xc000000000800016> {length = 2, path = 0 - 4}); frame = (440 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252b490> index path: (<NSIndexPath: 0xc000000000a00016> {length = 2, path = 0 - 5}); frame = (550 50; 100 100); ",
"<UICollectionViewLayoutAttributes: 0x7fbbb252b5d0> index path: (<NSIndexPath: 0xc000000000c00016> {length = 2, path = 0 - 6}); frame = (660 50; 100 100); "
)
\

**
**

因为一个LayoutAttributes对象代表一个cell,在这个方法里, 我们在父类已经算好的基础上,加以改进. 我们改了 LayoutAttributes就相当于改了cell的排布,这里先随便给一个随机数,用于测试

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{


NSArray *array =[super layoutAttributesForElementsInRect:rect] ;
****for (UICollectionViewLayoutAttributes *attrs in array) {
CGFloat scale = arc4random_uniform(100)/100.0;
attrs.transform = CGAffineTransformMakeScale
(scale, scale);
}


return array;
}\

再次运行得到如下效果,每个cell的大小都不一样,都是随机的

**
**

**
**

**
**

\

现在我们需要修改scale的属性值,来达到我们想要的结果.它的规律就是越往中间越大,越往两边越小.这里我选择通过cell的中心点和collection的中心点比较(因为中间cell的中心线正好和collection的中心线重合)

注意:这里有个误区,就是这些cell的坐标原点不一定在collectionView(0,0),是在contentSize里面,是内容的坐标(0,0).所以这里算collectionView的中心点的时候要以content size 来算,这样才有可比性.如果坐标原点不一样,不具有可比性.

\

这里我们改变数组里rect范围内所有元素的属性,具体代码如下:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{


//获得super已经计算好的布局属性
NSArray *array =[super layoutAttributesForElementsInRect:rect] ;
//计算collectionView最中心点的X的值(用contentsize的X偏移量+collectionView宽度的一半)
CGFloat centerX = self.collectionView.contentOffset.x +self.collectionView.frame.size.width*0.5;

//在原有布局的基础上,进行微调
for (UICollectionViewLayoutAttributes *attrs in array) {


CGFloat delta = ABS(attrs.center.x - centerX);
//根据间距值 计算cell的缩放比例
CGFloat scale =1- delta/self.collectionView.frame.size.width ;
//设置缩放比例
attrs.transform = CGAffineTransformMakeScale(scale, scale);
}
return array;
}\

运行得到如下结果

\

\

这里我们发现感觉有些样子,但是很乱.实际上是当你滑动的时候,动一下就改变\

因为 layoutAttributesForElementsInRect运行的时候,一进来的时候调用一次,但是滑动时并不会调用.所以现在没法达到我动一下,就根据最新点的x来再算一遍.所以这个时候要实现另外一个方法

// 当 collectionView 的显示范围发生改变的时候 , 是否需要重新刷新布局
// 一旦重新刷新布局 , 就会重新调用 layoutAttributesForElementsInRect 方法
- ( BOOL )shouldInvalidateLayoutForBoundsChange:( CGRect )newBounds
{


return   YES ;
}\

但是这个效果并没有实现当我手一松开的时候,它有个cell在最中间,最好还需要实现另一个方法

/*
* 这个方法的返回时就决定了 collectionView 停止滚动时的偏移量(即将停止滚动的时候调用)
*/
- ( CGPoint )targetContentOffsetForProposedContentOffset:( CGPoint )proposedContentOffset withScrollingVelocity:( CGPoint )velocity
{


return   CGPointZero ;
}\

\

至此,再运行就是这个正确的缩放效果了

\

\

\

这个效果是只会返回1的cell在最中间,我们想要实现那个cell离中心店最近,就把他偏移到中心点.

首先要判断那个cell离中心点最近,我们在 - ( NSArray  *)layoutAttributesForElementsInRect:( CGRect )rect方法从数组中可以拿到cell的中心点,具体代码如下

\

/*
*这个方法的返回时就决定了collectionView停止滚动时的偏移量
*/
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{


//计算出最终显示的矩形框
CGRect rect;
rect.origin.y = 0;
rect.origin.x =proposedContentOffset.x;
rect.size = self.collectionView.frame.size;

//这里建议调用super,因为用self会把里面for循环的transform再算一遍,但我们仅仅想拿到中心点X,super中已经算好中心点X的值了
NSArray *array =[super layoutAttributesForElementsInRect:rect];
//计算collectionView最中心点的X的值
/*
proposedContentOffset 目的,原本
拿到最后这个偏移量的X,最后这些cell,距离最后中心点的位置
*/
CGFloat centerX = proposedContentOffset.x +self.collectionView.frame.size.width*0.5;

//存放最小的间距值
CGFloat minDelta = MAXFLOAT;
for (UICollectionViewLayoutAttributes *attrs in array) {


if (ABS(minDelta)  ]]>ABS(attrs.center.x - centerX)) {
minDelta = attrs.center.x
- centerX;
} ;

}
//修改原有的偏移量
proposedContentOffset.x +=minDelta;

return proposedContentOffset;
}\

\

再次运行,即可得到我们看到的效果了

\

\

\

虽然这个效果已经实现了,但是还是有些小问题,我们需要做下优化.

\

一般来说,布局的方法不要放在init方法里面,放在 prepareLayout里面

/*
* 用来做布局的初始化操作 ( 不建议在 init 方法里面做布局的初始化操作 )
1.prepareLayout
2.layoutAttributesForElementsInRect: 方法
*/
-( void )prepareLayout
{


CGFloat  insert = ( self . collectionView . frame . size . width  - self . itemSize . width )* 0.5 ;
self . sectionInset  =  UIEdgeInsetsMake ( 0 , insert,  0 , insert);

}\

运行可以看到第一个和最后一个都有一些间距了.至此这个基于流水布局的滑动效果基本上已经完成了.

\

小结:

1.实现 -( void )prepareLayout(目的:做一些初始化)

2. 实现 - ( NSArray  *)layoutAttributesForElementsInRect:( CGRect )rect

(目的:拿出它计算好的布局属性来做一个微调,实现cell不断变大变小)

3.实现 - ( CGPoint )targetContentOffsetForProposedContentOffset:( CGPoint )proposedContentOffset withScrollingVelocity:( CGPoint )velocity

(目的:当我们手一松开,它最终停止滚动的时候,应该去在哪.它决定了collectionView停止滚动时候的偏移量)

\

- ( BOOL )shouldInvalidateLayoutForBoundsChange:( CGRect )newBounds\

(只要滑动就会重新刷新,就会调用 prepareLayout和 layoutAttributesForElementsInRect方法 )

\

\

第二篇:

上一篇中我们确实实现了collection的布局,但是如果我们想点击cell的时候做些事情怎么办呢,这个时候就和布局没有关系了,布局只负责展示,所以我们创建一个自定义的cell

\

\

\

并给它 @property  ( weak ,  nonatomic )  IBOutlet   UIImageView  *imageView; 一个属性,重写set方法

#import "WJPhotoCell.h"

@interface WJPhotoCell()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end

- (void)setImageName:(NSString *)imageName
{


_imageName = [imageName copy];
self.imageView.image = [UIImage imageNamed:imageName];
}

@end\

此时运行是这个效果

\

\

\

如果我们想给图片设置一个边框,可以通过以下方法实现

第一种:直接添加图片的边框的上下左右约束为10,view的背景设置成白色

\

\

第二种,通过代码法

- ( void )awakeFromNib {


[ super   awakeFromNib ];
self . imageView . layer . borderColor  = [ UIColor   whiteColor ]. CGColor ;
self . imageView . layer . borderWidth  =  10 ;
}\

\

此时,再次运行就是带边框的效果了.

\

\

\

第三篇  

我们对 WJLineLayout的布局属性的代码做下优化

将 ViewController 里面水平滚动的属性剪切到

-( void )prepareLayout
{


// 水平滚动
****self.scrollDirection = UICollectionViewScrollDirectionHorizontal ;

CGFloat  insert = ( self . collectionView . frame . size . width  - self . itemSize . width )* 0.5 ;
self . sectionInset  =  UIEdgeInsetsMake ( 0 , insert,  0 , insert);

}\

\

总结:

自定义布局   继承 UICollectionViewFlowLayout

1.重写prepareLayout方法(作用:在这个方法中做一些初始化操作)

2.重写 layoutAttributesForElementsInRect方法(作用:这个方法的返回值是个数组,数组中存放的都是l ayoutAttributes对象 ,决定了cell的排布方式)

3.重写 shouldInvalidateLayoutForBoundsChange方法(作用:返回yes,那么collectionView显示范围发生改变时,就会重新刷新布局; 一旦重新刷新布局,就会按顺序调用 prepareLayout和 layoutAttributesForElementsInRect方法 )

4.重写   targetContentOffsetForProposedContentOffset: proposedContentOffset withScrollingVelocity: velocity

(作用:返回值决定了collectionView停止滚动时最终的content offset偏移量)

说明

demo下载地址:github.com/AllisonWang…
这些资料是我在看小马哥讲解的时候整理总结的,希望能帮助到有需要的朋友们,如有侵权,请联系我,我立即删除.:\

\