CollectionView自定義Layout之堆疊布局、圓形布局、瀑布流布局

幾個自定義的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

效果圖如下:

瀑布流布局
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容