美文网首页
CollectionView自定义Layout之堆叠布局、圆形布

CollectionView自定义Layout之堆叠布局、圆形布

作者: 半岛夏天 | 来源:发表于2018-12-21 20:59 被阅读48次

    几个自定义的Layout

    UICollectionView的强大之处,就在于各种layout的自定义实现,以及它们之间的切换。先看几个相当exiciting的例子吧~

    比如,堆叠布局:

    圆形布局:

    和Cover Flow布局:

    所有这些布局都采用了同样的数据源和委托方法,因此完全实现了model和view的解耦。但是如果仅这样,那开源社区也已经有很多相应的解决方案了。Apple的强大和开源社区不能比拟的地方在于对SDK的全局掌控,CollectionView提供了非常简单的API可以令开发者只需要一次简单调用,就可以使用CoreAnimation在不同的layout之间进行动画切换,这种切换必定将大幅增加用户体验,代价只是几十行代码就能完成的布局实现,以及简单的一句API调用,不得不说现在所有的开源代码与之相比,都是相形见拙了…不得不佩服和感谢UIKit团队的努力。


    UICollectionViewLayoutAttributes

    UICollectionViewLayoutAttributes是一个非常重要的类,先来看看property列表:

    @property (nonatomic) CGRect frame
    @property (nonatomic) CGPoint center
    @property (nonatomic) CGSize size
    @property (nonatomic) CATransform3D transform3D
    @property (nonatomic) CGFloat alpha
    @property (nonatomic) NSInteger zIndex
    @property (nonatomic, getter=isHidden) BOOL hidden
    

    可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息(在这个层面上说的话,实现一个UICollectionViewLayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。


    自定义的UICollectionViewLayout

    UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:

    // 返回collectionView的内容的尺寸
    -(CGSize)collectionViewContentSize
    
    /*   返回rect中的所有的元素的布局属性
        *   返回的是包含UICollectionViewLayoutAttributes的NSArray
        *   UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:
    
            *   layoutAttributesForCellWithIndexPath:
            *   layoutAttributesForSupplementaryViewOfKind:withIndexPath:
            *   layoutAttributesForDecorationViewOfKind:withIndexPath:
    */
    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
    
    //  返回对应于indexPath的位置的cell的布局属性
    -(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
    
    // 返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
    -(UICollectionViewLayoutAttributes _)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath
    
    // 返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
    -(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath
    
    //  当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    

    另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。

    首先,-(void)prepareLayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

    之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。

    接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

    另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。


    CircleLayout——完全自定义的Layout,添加删除item,以及手势识别

    CircleLayout的例子稍微复杂一些,cell分布在圆周上,点击cell的话会将其从collectionView中移出,点击空白处会加入一个cell,加入和移出都有动画效果。

    这放在以前的话估计够写一阵子了,而得益于UICollectionView,基本只需要100来行代码就可以搞定这一切,非常cheap。通过CircleLayout的实现,可以完整地看到自定义的layout的编写流程,非常具有学习和借鉴的意义。

    首先,布局准备中定义了一些之后计算所需要用到的参数。

    -(void)prepareLayout
    {   //和init相似,必须call super的prepareLayout以保证初始化正确
        [super prepareLayout];
    
        CGSize size = self.collectionView.frame.size;
        _cellCount = [[self collectionView] numberOfItemsInSection:0];
        _center = CGPointMake(size.width / 2.0, size.height / 2.0);
        _radius = MIN(size.width, size.height) / 2.5;
    }
    

    其实对于一个size不变的collectionView来说,除了_cellCount之外的中心和半径的定义也可以扔到init里去做,但是显然在prepareLayout里做的话具有更大的灵活性。因为每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化。

    然后,按照UICollectionViewLayout子类的要求,重载了所需要的方法:

    //整个collectionView的内容大小就是collectionView的大小(没有滚动)
    -(CGSize)collectionViewContentSize
    {
        return [self collectionView].frame.size;
    }
    
    //通过所在的indexPath确定位置。
    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
    {
        UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path]; //生成空白的attributes对象,其中只记录了类型是cell以及对应的位置是indexPath
        //配置attributes到圆周上
        attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
        attributes.center = CGPointMake(_center.x + _radius * cosf(2 * path.item * M_PI / _cellCount), _center.y + _radius * sinf(2 * path.item * M_PI / _cellCount));
        return attributes;
    }
    
    //用来在一开始给出一套UICollectionViewLayoutAttributes
    -(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
    {
        NSMutableArray* attributes = [NSMutableArray array];
        for (NSInteger i=0 ; i < self.cellCount; i++) {
            //这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
            NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
            [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
        }    
        return attributes;
    }
    

    现在已经得到了一个circle layout。为了实现cell的添加和删除,需要为collectionView加上手势识别,这个很简单,在ViewController中:

    UITapGestureRecognizer* tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
    [self.collectionView addGestureRecognizer:tapRecognizer];
    

    对应的处理方法handleTapGesture:为

    - (void)handleTapGesture:(UITapGestureRecognizer *)sender {
        if (sender.state == UIGestureRecognizerStateEnded) {
            CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
            NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint]; //获取点击处的cell的indexPath
            if (tappedCellPath!=nil) { //点击处没有cell
                self.cellCount = self.cellCount - 1;
                [self.collectionView performBatchUpdates:^{
                    [self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:tappedCellPath]];
                } completion:nil];
            } else {
                self.cellCount = self.cellCount + 1;
                [self.collectionView performBatchUpdates:^{
                    [self.collectionView insertItemsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]];
                } completion:nil];
            }
        }
    }
    

    performBatchUpdates:completion: 再次展示了block的强大的一面..这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:

    • initialLayoutAttributesForAppearingItemAtIndexPath:
    • initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
    • finalLayoutAttributesForDisappearingItemAtIndexPath:
    • finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:

    新的示例demo在Github上也有,链接

    在CircleLayout中,实现了cell的动画。

    //插入前,cell在圆心位置,全透明
    - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForInsertedItemAtIndexPath:(NSIndexPath *)itemIndexPath
    {
        UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
        return attributes;
    }
    
    //删除时,cell在圆心位置,全透明,且只有原来的1/10大
    - (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDeletedItemAtIndexPath:(NSIndexPath *)itemIndexPath
    {
        UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        attributes.alpha = 0.0;
        attributes.center = CGPointMake(_center.x, _center.y);
        attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
        return attributes;
    }
    

    在插入或删除时,将分别以插入前和删除后的attributes和普通状态下的attributes为基准,进行UIView的动画过渡。而这一切并没有很多代码要写,几乎是free的,感谢苹果…


    堆叠式布局CustomStackLayout

    #import "CustomStackLayout.h"
    
    #define RANDOM_0_1  arc4random_uniform(100)/100.0
    
    /*
     由于CustomStackLayout是直接继承自UICollectionViewLayout的,父类没有帮它完成任何的布局,因此,
     需要用户自己完全重新对每一个item进行布局,也即设置它们的布局属性UICollectionViewLayoutAttributes
    */
    
    @implementation CustomStackLayout
    
    //重写shouldInvalidateLayoutForBoundsChange,每次重写布局内部都会自动调用
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    {
    
        return YES;
    }
    
    //重写collectionViewContentSize,可以让collectionView滚动
    -(CGSize)collectionViewContentSize
    {
        return CGSizeMake(400, 400);
    }
    
    //重写layoutAttributesForItemAtIndexPath,返回每一个item的布局属性
    -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        //创建布局实例
        UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        
        //设置布局属性
        attrs.size = CGSizeMake(100, 100);
        attrs.center = CGPointMake(self.collectionView.frame.size.width*0.5, self.collectionView.frame.size.height*0.5);
        
        //设置旋转方向
        //int direction = (i % 2 ==0)? 1: -1;
        
        NSArray *directions = @[@0.0,@1.0,@(0.05),@(-1.0),@(-0.05)];
        
        //只显示5张
        if (indexPath.item >= 5)
        {
            attrs.hidden = YES;
        }
        else
        {
            //开始旋转
            attrs.transform = CGAffineTransformMakeRotation([directions[indexPath.item]floatValue]);
            
            //zIndex值越大,图片越在上面
            attrs.zIndex = [self.collectionView numberOfItemsInSection:indexPath.section] - indexPath.item;
        }
    
        return attrs;
    }
    
    
    //重写layoutAttributesForElementsInRect,设置所有cell的布局属性(包括item、header、footer)
    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
    {
        NSMutableArray *arrayM = [NSMutableArray array];
        NSInteger count = [self.collectionView numberOfItemsInSection:0];
        
        //给每一个item创建并设置布局属性
        for (int i = 0; i < count; I++)
        {
            //创建item的布局属性
            UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
            
             [arrayM addObject:attrs];
        }
        return arrayM;
    }
    
    @end
    

    效果如图:

    堆叠式布局

    圆形布局CustomCircleLayout

    #import "CustomCircleLayout.h"
    
    @implementation CustomCircleLayout
    
    
    //重写shouldInvalidateLayoutForBoundsChange,每次重写布局内部都会自动调用
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    {
        return YES;
    }
    
    //重写layoutAttributesForItemAtIndexPath,返回每一个item的布局属性
    -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        //创建布局实例
        UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        
        //设置item的大小
        attrs.size = CGSizeMake(50, 50);
        
        //设置圆的半径
        CGFloat circleRadius = 70;
        
        //设置圆的中心点
        CGPoint circleCenter = CGPointMake(self.collectionView.frame.size.width*0.5, self.collectionView.frame.size.height *0.5);
        
        //计算每一个item之间的角度
        CGFloat angleDelta = M_PI *2 /[self.collectionView numberOfItemsInSection:indexPath.section];
        
        //计算当前item的角度
        CGFloat angle = indexPath.item * angleDelta;
        
        //计算当前item的中心
        CGFloat x = circleCenter.x + cos(angle)*circleRadius;
        CGFloat y = circleCenter.y - sin(angle)*circleRadius;
        
        //定位当前item的位置
        attrs.center = CGPointMake(x, y);
        
        //设置item的顺序,越后面的显示在前面
        attrs.zIndex = indexPath.item;
        
        return attrs;
    }
    
    
    //重写layoutAttributesForElementsInRect,设置所有cell的布局属性(包括item、header、footer)
    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
    {
        NSMutableArray *arrayM = [NSMutableArray array];
        NSInteger count = [self.collectionView numberOfItemsInSection:0];
        
        //给每一个item创建并设置布局属性
        for (int i = 0; i < count; I++)
        {
            //创建item的布局属性
            UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
            
            [arrayM addObject:attrs];
        }
        return arrayM;
    }
    
    @end
    

    效果图如下:


    圆形布局

    瀑布流布局

    在.h文件里代码结构如下:

    #import <UIKit/UIKit.h>
    
    /*
    为了体现封装性的特点,我们可以把一些数据设置为公共的,既可以提高扩展性和通用性,
    也便于外界按照自己的需求做必要的调整。
    */
    
    @protocol WaterFlowLayoutDelegate; //设置代理传递数据,降低了与其他类的耦合性,通用性更强
    
    
    @class WaterFlowLayout;
    @interface WaterFlowLayout : UICollectionViewLayout
    @property (assign,nonatomic)CGFloat columnMargin;//每一列item之间的间距
    @property (assign,nonatomic)CGFloat rowMargin;   //每一行item之间的间距
    @property (assign,nonatomic)UIEdgeInsets sectionInset;//设置于collectionView边缘的间距
    @property (assign,nonatomic)NSInteger columnCount;//设置每一行排列的个数
    
    
    @property (weak,nonatomic)id<WaterFlowLayoutDelegate> delegate; //设置代理
    @end
    
    
    @protocol WaterFlowLayoutDelegate
    -(CGFloat)waterFlowLayout:(WaterFlowLayout *) WaterFlowLayout heightForWidth:(CGFloat)width andIndexPath:(NSIndexPath *)indexPath;
    @end
    

    在.m文件里代码结构如下:

    #import "WaterFlowLayout.h"
    
    //每一列item之间的间距
    //static const CGFloat columnMargin = 10;
    //每一行item之间的间距
    //static const CGFloat rowMargin = 10;
    
    @interface WaterFlowLayout()
    /** 这个字典用来存储每一列item的高度 */
    @property (strong,nonatomic)NSMutableDictionary *maxYDic;
    /** 存放每一个item的布局属性 */
    @property (strong,nonatomic)NSMutableArray *attrsArray;
    @end
    
    @implementation WaterFlowLayout
    
    /** 懒加载 */
    -(NSMutableDictionary *)maxYDic
    {
        if (!_maxYDic)
        {
            _maxYDic = [NSMutableDictionary dictionary];
        }
        return _maxYDic;
    }
    
    /** 懒加载 */
    -(NSMutableArray *)attrsArray
    {
        if (!_attrsArray)
        {
            _attrsArray = [NSMutableArray array];
        }
        return _attrsArray;
    }
    
    //初始化
    -(instancetype)init
    {
        if (self = [super init]){
            self.columnMargin = 10;
            self.rowMargin = 10;
            self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
            self.columnCount = 3;
        }
        return self;
    }
    
    //每一次布局前的准备工作
    -(void)prepareLayout
    {
        [super prepareLayout];
        
        //清空最大的y值
        for (int i =0; i < self.columnCount; I++)
        {
            NSString *column = [NSString stringWithFormat:@"%d",I];
            self.maxYDic[column] = @(self.sectionInset.top);
        }
    
        //计算所有item的属性
        [self.attrsArray removeAllObjects];
        NSInteger count = [self.collectionView numberOfItemsInSection:0];
        for (int i=0; i<count; I++)
        {
            UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
            
            [self.attrsArray addObject:attrs];
        }
    }
    
    //设置collectionView滚动区域
    -(CGSize)collectionViewContentSize
    {
        //假设最长的那一列为第0列
        __block NSString *maxColumn = @"0";
        
        //遍历字典,找出最长的那一列
        [self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSString *column, NSNumber *maxY, BOOL *stop) {
            
            if ([maxY floatValue] > [self.maxYDic[maxColumn] floatValue])
            {
                maxColumn = column;
            }
        }];
        return CGSizeMake(0, [self.maxYDic[maxColumn]floatValue]+self.sectionInset.bottom);
    }
    
    //允许每一次重新布局
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    {
        return YES;
    }
    
    //布局每一个属性
    -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        //假设最短的那一列为第0列
        __block NSString *minColumn = @"0";
        
        //遍历字典,找出最短的那一列
        [self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSString *column, NSNumber *maxY, BOOL *stop) {
            
            if ([maxY floatValue] < [self.maxYDic[minColumn] floatValue])
            {
                minColumn = column;
            }
        }];
        
        //计算每一个item的宽度和高度
        CGFloat width = (self.collectionView.frame.size.width - self.columnMargin*(self.columnCount - 1) - self.sectionInset.left - self.sectionInset.right) / self.columnCount;
        
        CGFloat height = [self.delegate waterFlowLayout:self heightForWidth:width andIndexPath:indexPath] ;
        
        
        //计算每一个item的位置
        CGFloat x = self.sectionInset.left + (width + self.columnMargin) * [minColumn floatValue];
        CGFloat y = [self.maxYDic[minColumn] floatValue] + self.rowMargin;
        
        
        //更新这一列的y值
        self.maxYDic[minColumn] = @(y + height);
        
        
        //创建布局属性
        UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        
        //设置item的frame
        attrs.frame = CGRectMake(x, y, width, height);
        
        return attrs;
    }
    
    //布局所有item的属性,包括header、footer
    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
    {
        return self.attrsArray;
    }
    @end
    

    效果图如下:

    瀑布流布局

    相关文章

      网友评论

          本文标题:CollectionView自定义Layout之堆叠布局、圆形布

          本文链接:https://www.haomeiwen.com/subject/xbemkqtx.html