瀑布流UICollectionViewFlowLayout


第一篇:
现在我们要实现如下的效果:
瀑布流UICollectionViewFlowLayout_第1张图片
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(0200, collectionWH, collectionWH);
    
UICollectionView *collectionView = [[UICollectionView alloc]initWithFrame:frame collectionViewLayout:[[UICollectionViewFlowLayout alloc]init]];
    [
self.view addSubview:collectionView];


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

    //注册cell
    [collectionView 
registerClass:[UICollectionViewCell classforCellWithReuseIdentifier:WJCellId];


#pragma mark -
// 返回 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;
}

此时,运行会看到效果
瀑布流UICollectionViewFlowLayout_第2张图片


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


瀑布流UICollectionViewFlowLayout_第3张图片

 //创建布局
    
WJLineLayout *layout = [[WJLineLayout alloc]init];
    layout.
itemSize = CGSizeMake(100100);
    
//水平滚动
    layout.
scrollDirection = UICollectionViewScrollDirectionHorizontal;
    
    
CGFloat collectionW= self.view.frame.size.width;
    
CGFloat collectionH= 200;
    
CGRect frame = CGRectMake(0150, 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
对象决定了cellframe
        */

    }
    
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] (
    " index path: ( {length = 2, path = 0 - 0}); frame = (0 50; 100 100); ",
    " index path: ( {length = 2, path = 0 - 1}); frame = (110 50; 100 100); ",
    " index path: ( {length = 2, path = 0 - 2}); frame = (220 50; 100 100); ",
    " index path: ( {length = 2, path = 0 - 3}); frame = (330 50; 100 100); ",
    " index path: ( {length = 2, path = 0 - 4}); frame = (440 50; 100 100); ",
    " index path: ( {length = 2, path = 0 - 5}); frame = (550 50; 100 100); ",
    " index path: ( {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的大小都不一样,都是随机的
瀑布流UICollectionViewFlowLayout_第4张图片



现在我们需要修改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的值(contentsizeX偏移量+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;
}

运行得到如下结果
瀑布流UICollectionViewFlowLayout_第5张图片

这里我们发现感觉有些样子,但是很乱.实际上是当你滑动的时候,动一下就改变
因为 layoutAttributesForElementsInRect运行的时候,一进来的时候调用一次,但是滑动时并不会调用.所以现在没法达到我动一下,就根据最新点的x来再算一遍.所以这个时候要实现另外一个方法
// collectionView 的显示范围发生改变的时候 , 是否需要重新刷新布局
// 一旦重新刷新布局 , 就会重新调用 layoutAttributesForElementsInRect 方法
- (
BOOL )shouldInvalidateLayoutForBoundsChange:( CGRect )newBounds
{
    
return   YES ;
}

但是这个效果并没有实现当我手一松开的时候,它有个cell在最中间,最好还需要实现另一个方法
/*
 *
这个方法的返回时就决定了 collectionView 停止滚动时的偏移量(即将停止滚动的时候调用)
 */

- (
CGPoint )targetContentOffsetForProposedContentOffset:( CGPoint )proposedContentOffset withScrollingVelocity:( CGPoint )velocity
{
    
return   CGPointZero ;
}


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

瀑布流UICollectionViewFlowLayout_第6张图片

这个效果是只会返回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;
}


再次运行,即可得到我们看到的效果了
瀑布流UICollectionViewFlowLayout_第7张图片


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

一般来说,布局的方法不要放在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 (weaknonatomicIBOutlet UIImageView *imageView;
@end

- (
void)setImageName:(NSString *)imageName
{
    
_imageName = [imageName copy];
    
self.imageView.image = [UIImage imageNamed:imageName];
}

@end

此时运行是这个效果

瀑布流UICollectionViewFlowLayout_第8张图片

如果我们想给图片设置一个边框,可以通过以下方法实现
第一种:直接添加图片的边框的上下左右约束为10,view的背景设置成白色
瀑布流UICollectionViewFlowLayout_第9张图片

第二种,通过代码法
- ( void )awakeFromNib {
    [
super   awakeFromNib ];
    
self . imageView . layer . borderColor  = [ UIColor   whiteColor ]. CGColor ;
    
self . imageView . layer . borderWidth  =  10 ;
}

此时,再次运行就是带边框的效果了.
瀑布流UICollectionViewFlowLayout_第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下载地址:https://github.com/AllisonWangJiaoJiao/StudyMaterials/tree/master
这些资料是我在看小马哥讲解的时候整理总结的,希望能帮助到有需要的朋友们,如有侵权,请联系我,我立即删除.:


你可能感兴趣的:(瀑布流UICollectionViewFlowLayout)