幾個自定義的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
效果圖如下: