UICollection是iOS6的時(shí)候引入的咏雌,它是同UITableview共享一套API設(shè)計(jì)换团,都是基于datasource和delegate悉稠,都繼承自UIScrollView。但它又與UITableview有很大不同艘包,它進(jìn)行了進(jìn)一步的抽象的猛,將它的所有的子視圖的位置、大小想虎、transform委托給了一個(gè)單獨(dú)的布局對象:UICollectionViewLayout卦尊。這是一個(gè)抽象類,我們可以繼承它來實(shí)現(xiàn)任何想要的布局磷醋,系統(tǒng)也為我們提供了一個(gè)開箱即食的實(shí)現(xiàn)UICollectionViewFlowLayout猫牡。在我看來,沒有任何布局是UICollenctionViewLayout不能實(shí)現(xiàn)的邓线,如果有那就自定義一個(gè)淌友。
UITableview只能提供豎直滑動(dòng)的布局,而且默認(rèn)情況下cell的寬度和tableView的寬度一致骇陈,而且cell的排列順序也是挨次排列震庭。UICollectionView則為我們提供了另一種可能:它能提供豎直滑動(dòng)的布局也能提供豎屏滑動(dòng)的布局,而且cell的位置你雌、大小等完全由你自己決定器联。所以在我們用到水平滑動(dòng)的布局時(shí),不要忙著用UIScrollView去實(shí)現(xiàn)婿崭,可以先考慮UICollectionView能不能滿足要求拨拓,還有一個(gè)好處是你不要自己考慮滑動(dòng)視圖cell的重用問題。
這篇文章會如何自定義UICollectionViewLayout來實(shí)現(xiàn)任意布局氓栈,默認(rèn)你已經(jīng)會使用系統(tǒng)提供的UICollectionViewFlowLayout來進(jìn)行標(biāo)準(zhǔn)的Grid View布局了渣磷。
1、UICollectuonViewFlowLayout
系統(tǒng)為我們提供了一個(gè)自定義的布局實(shí)現(xiàn):UICollectionViewFlowLayout授瘦,通過它我們可以實(shí)現(xiàn)Grid View類型的布局醋界,也就是像一個(gè)一個(gè)格子挨次排列的布局,對于大多數(shù)的情況下提完,使用它就能滿足我們的要求了形纺。系統(tǒng)為我們提供了布局所用的參數(shù),我們在使用的時(shí)候只需去確認(rèn)這些參數(shù)就行:
NS_CLASS_AVAILABLE_IOS(6_0) @interface UICollectionViewFlowLayout : UICollectionViewLayout
@property (nonatomic) CGFloat minimumLineSpacing;
@property (nonatomic) CGFloat minimumInteritemSpacing;
@property (nonatomic) CGSize itemSize;
@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0); // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:
@property (nonatomic) UICollectionViewScrollDirection scrollDirection; // default is UICollectionViewScrollDirectionVertical
@property (nonatomic) CGSize headerReferenceSize;
@property (nonatomic) CGSize footerReferenceSize;
@property (nonatomic) UIEdgeInsets sectionInset;
// Set these properties to YES to get headers that pin to the top of the screen and footers that pin to the bottom while scrolling (similar to UITableView).
@property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
@property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
@end
如果說上面所說的GridView類型的布局不能滿足我們的需求徒欣,這是就需要自定義一個(gè)Layout逐样。
2、UICollectionViewLayout ?VS ? UICollectionViewFlowLayout
UICollectionViewFlowLayout繼承自UICollectionViewLayout,我們可以直接使用它脂新,我們只需要提供cell的大小秽澳,以及行間距、列間距戏羽,他就會自己計(jì)算出每個(gè)cell的位置以及UICollectionView的滑動(dòng)范圍contentSize担神。但它只能提供一個(gè)方向的滑動(dòng),也就是說我們自定義的類如果繼承自UICollectionViewFlowLayout始花,則只能在一個(gè)方向上滑動(dòng)的布局妄讯,要么水平方向要么豎直方向。反之酷宵,則需要繼承自UICollectionViewLayout亥贸,UICollectionViewLayout是一個(gè)抽象類,不能直接使用浇垦。
3炕置、自定義布局需要實(shí)現(xiàn)的方法
UICollectionViewLayout文檔為我們列出了需要實(shí)現(xiàn)的方法:
以上列出的這六個(gè)方法不是都需要我們自己實(shí)現(xiàn)的,而是根據(jù)需要男韧,選擇其中的某些方法實(shí)現(xiàn)朴摊。
collectionViewContentSize
UICollection繼承自UIScrollView,我們都知道UIScrollView的一個(gè)重要參數(shù):contentSize,如果這個(gè)參數(shù)不對此虑,那么你布局的內(nèi)容就不能完全展示甚纲,而collectionViewContentSize就是為了得到這個(gè)參數(shù),UICollection就像一個(gè)畫板朦前,而collectionViewContentSize則規(guī)定了畫板的大小介杆,如果是繼承自UICollectionViewFlowLayout,而且每個(gè)section里面的cell大小是通過UICollectionViewFlowLayout的參數(shù)設(shè)定的韭寸,大小和位置也不在自定義的過程中隨意更改春哨,那么collectionViewContentSize是可以不自己重寫的,系統(tǒng)會自己計(jì)算contentSize恩伺,如果是繼承自UICollectionViewLayout赴背,那就需要根據(jù)你自己的展示布局去提供合適的CGSize給collectionViewContentSize。
layoutAttributesForElementsInRect
這個(gè)方法的參數(shù)是UICollectionView當(dāng)前的bounds莫其,也就是視圖當(dāng)前的可見區(qū)域癞尚,返回值是一個(gè)包含對象為UICollectionViewLayoutAttributes的數(shù)組耸三,UICollectionView的可見區(qū)域內(nèi)包含cell乱陡、supplementary view、decoration view(這里統(tǒng)稱cell仪壮,因?yàn)樗鼈兌际莄ollectionView的一個(gè)子視圖)憨颠,它們的位置、大小等信息都由對應(yīng)的UICollectionViewLayoutAttributes控制。默認(rèn)情況下這個(gè)LayoutAttributes包含indexPath爽彤、frame养盗、center、size适篙、transform3D往核、alpha以及hidden屬性。如果你還需要控制其他的屬性嚷节,你可以自己自定義一個(gè)UICollectionViewLayoutAttributes的子類聂儒,加上任意你想要的屬性。
布局屬性對象(UICollectionViewLayoutAttributes)通過indexPath和cell關(guān)聯(lián)起來硫痰,當(dāng)collectionView展示cell時(shí)衩婚,會通過這些布局屬性對象拿到布局信息。
返回原話題效斑,layoutAttributesForElementsInRect方法的返回值是一個(gè)數(shù)組非春,這個(gè)數(shù)組里面是傳遞進(jìn)來的可見區(qū)域內(nèi)的cell所對應(yīng)的UICollectionViewLayoutAttributes。
要拿到可見區(qū)域內(nèi)的布局屬性缓屠,通常的做法如下:
如果你是繼承自UICollectionViewFlowLayout奇昙,并且設(shè)置好了itemSize、行間距敌完、列間距等信息敬矩,那么你通過[super layoutAttributesForElementsInRect:rect]就能拿到可見區(qū)域內(nèi)的布局屬性,反之蠢挡,則進(jìn)入步奏2弧岳。
創(chuàng)建一個(gè)空數(shù)組,用于存放可見區(qū)域內(nèi)的布局屬性业踏。
從UICollectionView的數(shù)據(jù)源中取出你需要展示的數(shù)據(jù)禽炬,然后根據(jù)你想要的布局計(jì)算出哪些indexPath在當(dāng)前可見區(qū)域內(nèi),通過CGRectIntersectsRect函數(shù)可以判斷兩個(gè)CGRect是否有交集來確定勤家。然后循環(huán)調(diào)用layoutAttributesForItemAtIndexPath:來確定每一個(gè)布局屬性的frame等數(shù)據(jù)腹尖。同樣,如果當(dāng)前區(qū)域內(nèi)有supplementary view或者decoration view伐脖,你也需要調(diào)用:layoutAttributesForSupplementaryViewOfKind:atIndexPath或者layoutAttributesForDecorationViewOfKind:atIndexPath热幔,最后將這些布局屬性添加到數(shù)組中返回。這里需要多說一點(diǎn)的是讼庇,有些布局屬性在UICollectionViewLayout的prepareLayout就根據(jù)數(shù)據(jù)源全部計(jì)算了出來绎巨,比如瀑布流樣式的布局,這個(gè)時(shí)候你就只需要返回布局屬性的frame和當(dāng)前可見區(qū)域有交集的對象就行蠕啄。
layoutAttributesFor…IndexPath
這里用三個(gè)點(diǎn)场勤,是因?yàn)橛腥齻€(gè)類似的方法:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
layoutAttributesForDecorationViewOfKind:atIndexPath:
它們分別為cell戈锻、supplementaryView、decorationView返回布局屬性和媳,它們的實(shí)現(xiàn)不是必須的格遭,它們只是為對應(yīng)的IndexPath返回布局屬性,如果你能通過其他方法拿到對應(yīng)indexPath處的布局屬性留瞳,那就沒必要非要實(shí)現(xiàn)這幾個(gè)方法拒迅。
以layoutAttributesForItemAtIndexPath:為例,你可以通過+[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]方法拿到一個(gè)布局屬性對象她倘,然后你可能需要訪問你的數(shù)據(jù)源去算出該indexPath處的布局屬性的frame等信息坪它,然后賦值給它。
shouldInvalidateLayoutForBoundsChange
這個(gè)是用來告訴collectionView是否需要根據(jù)bounds的改變而重新計(jì)算布局屬性帝牡,比如橫豎屏的旋轉(zhuǎn)往毡。通常的寫法如下:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
CGRect oldBounds = self.collectionView.bounds;
if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {
return YES;
}
return NO;
}
需要注意的是,當(dāng)在滑動(dòng)的過程中靶溜,需要對某些cell的布局進(jìn)行更改开瞭,那么就需要在這個(gè)方法里面返回YES,告訴UICollectionView重新計(jì)算布局罩息。因?yàn)橐粋€(gè)cell的改變會引起整個(gè)UICollectionView布局的改變嗤详。
4、示例一:瀑布流實(shí)現(xiàn)
瀑布流的排列一般用于圖片或者商品的展示瓷炮,它的布局特點(diǎn)是等寬變高葱色,cell的排列是找到最短的那一列,然后把cell放到那個(gè)位置娘香,效果如下:
下面我們來看看具體的實(shí)現(xiàn)苍狰,這里的布局行間距和列間距都定位10,列數(shù)固定為3列烘绽,如上圖所示淋昭。
系統(tǒng)提供給我們的UICollectionViewFlowLayout顯然不能實(shí)現(xiàn)瀑布流的布局,因?yàn)樗哪J(rèn)實(shí)現(xiàn)是一行一列整齊對齊的安接,所以我們需要新建一個(gè)繼承自UICollectionViewFlowLayout的類翔忽,然后來講解一下這個(gè)類的實(shí)現(xiàn)。
prepareLayout
在講解如何布局瀑布流之前需要先說明一下UICollectionViewFlowLayout的prepareLayout方法盏檐,他會在UICollectionView布局之前調(diào)用歇式,調(diào)用[self.collectionView reloadData]和[self.collectionView.collectionViewLayout invalidateLayout]的時(shí)候prepareLayout也會進(jìn)行調(diào)用,如果shouldInvalidateLayoutForBoundsChange返回YES胡野,prepareLayout方法同樣也會調(diào)用材失。所以這個(gè)函數(shù)是提前進(jìn)行數(shù)據(jù)布局計(jì)算的絕佳地方。
在進(jìn)行瀑布流布局的時(shí)候我們可以在prepareLayout里面根據(jù)數(shù)據(jù)源给涕,計(jì)算出所有的布局屬性并緩存起來:
- (void)prepareLayout {
[super prepareLayout];
//記錄布局需要的contentSize的高度
self.contentHeight = 0;
//columnHeights數(shù)組會記錄各列的當(dāng)前布局高度
[self.columnHeights removeAllObjects];
//默認(rèn)高度是sectionEdge.top
for (NSInteger i = 0; i < self.columnCount; i++) {
[self.columnHeights addObject:@(self.edgeInsets.top)];
}
//清除之前所以的布局屬性數(shù)據(jù)
[self.attrsArray removeAllObjects];
//通過數(shù)據(jù)源拿到需要展示的cell數(shù)量
NSInteger count = [self.collectionView numberOfItemsInSection:0];
//開始創(chuàng)建每一個(gè)cell對應(yīng)的布局屬性
for (NSInteger index = 0; index < count; index++) {
//創(chuàng)建indexPath
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
//獲取cell布局屬性,在layoutAttributesForItemAtIndexPath里面計(jì)算具體的布局信息
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
在layoutAttributesForItemAtIndexPath方法里面去根據(jù)參數(shù)indexPath拿到數(shù)據(jù)源里面對應(yīng)位置的展示數(shù)據(jù)豺憔,根據(jù)等寬的前提,等比例的獲得布局屬性的高度够庙,然后根據(jù)記錄每列當(dāng)前布局到的高度的數(shù)組columnHeights來找到當(dāng)前布局最短的那一列恭应,從而獲取到布局屬性的origin信息,這樣在等寬的前提下就獲取到了當(dāng)前indexPath處的布局屬性的frame信息耘眨。然后更新columnHeights里面的數(shù)據(jù)昼榛,并且讓記錄布局所需高度的變量contentHeight等于當(dāng)前列高度數(shù)組里面的最大值。
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
//獲取一個(gè)UICollectionViewLayoutAttributes對象
UICollectionViewLayoutAttributes *attrs = [super layoutAttributesForItemAtIndexPath:indexPath];
//列數(shù)是3剔难,布局屬性的寬度是固定的
CGFloat collectionViewW = self.collectionView.frame.size.width;
CGFloat width = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
CGFloat height = 通過數(shù)據(jù)源以及寬度信息胆屿,獲取對應(yīng)位置的布局屬性高度;
//找到數(shù)組內(nèi)目前高度最小的那一列
NSInteger destColumn = 0;
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
for (NSInteger index = 1; index < self.columnCount; index++) {
CGFloat columnHeight = [self.columnHeights[index] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = index;
break;
}
}
//根據(jù)列信息,計(jì)算出origin的x
CGFloat x = self.edgeInsets.left + destColumn * (width +self.columnMargin);
CGFloat y = minColumnHeight;
if (y != self.edgeInsets.top) {//不是第一行就加上行間距
y += self.rowMargin;
}
//得到布局屬性的frame信息
attrs.frame = CGRectMake(x, y, width, height);
//更新最短那列的高度
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
//更新記錄展示布局所需的高度
CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];
if (self.contentHeight < columnHeight) {
self.contentHeight = columnHeight;
}
return attrs;
}
滑動(dòng)的過程在偶宫,cell會不斷重用非迹,系統(tǒng)會調(diào)用layoutAttributesForElementsInRect方法來獲取當(dāng)前可見區(qū)域內(nèi)的布局屬性,由于所有的布局屬性都緩存了起來纯趋,則只需返回布局屬性的frame和當(dāng)前可見區(qū)域有交集的布局屬性就行憎兽。
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *rArray = [NSMutableArray array];
for (UICollectionViewLayoutAttributes *cacheAttr in _attrsArray) {
if (CGRectIntersectsRect(cacheAttr.frame, rect)) {
[rArray addObject:cacheAttr];
}
}
return rArray;
}
最后由于我們自定義了每個(gè)cell的高度及布局,所以系統(tǒng)是不知道UICollectionView當(dāng)前的contentSize的大小吵冒,所以我們需要在collectionViewContentSize方法里返回正確的size以確保所以cell都能正炒棵滑動(dòng)到可見區(qū)域里來。
-(CGSize)collectionViewContentSize {
return CGSizeMake(CGRectGetWidth(self.collectionView.frame), self.contentHeight + self.edgeInsets.bottom);
}
至此痹栖,瀑布流的布局就完成了亿汞,實(shí)現(xiàn)起來非常簡單,最關(guān)鍵的地方就是計(jì)算布局屬性的frame信息揪阿。
5疗我、示例二:卡片吸頂布局
卡片吸頂布局的效果如下:
可以看到滑到頂部的cell本應(yīng)該移出當(dāng)前可見區(qū)域,但我們實(shí)現(xiàn)的效果是移到頂部后就懸停南捂,并且可以被后來的cell覆蓋碍粥。
實(shí)現(xiàn)的原理非常簡單,cell的布局使用UICollectionViewFlowLayout就能實(shí)現(xiàn)黑毅,我們新建一個(gè)繼承自UICollectionViewFlowLayout的子類嚼摩,利用這個(gè)子類創(chuàng)建布局,可以利用UICollectionViewFlowLayout提供的參數(shù)來構(gòu)建一個(gè)不吸頂展示的collectionView:
只需要提供給UICollectionViewFlowLayoutitemSize和minimumLineSpacing就行矿瘦,行間距minimumLineSpacing設(shè)置為一個(gè)負(fù)數(shù)就能建立起互相疊加的效果枕面。
要建立吸頂?shù)男Ч恍枰谠瓉淼牟季只A(chǔ)上缚去,判斷布局屬性frame小于布局頂部的y值潮秘,就將布局屬性的frame的y值設(shè)置為頂部的y值就行,這樣滑動(dòng)到頂部的cell都會在頂部懸停下來易结。
@implementation CardCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
//拿到當(dāng)前可見區(qū)域內(nèi)的布局屬性
NSArray *oldItems = [super layoutAttributesForElementsInRect:rect];
//處理當(dāng)前可見區(qū)域內(nèi)的布局屬性吸頂
[oldItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) {
[self recomputeCellAttributesFrame:attributes];
}];
return oldItems;
}
- (void)recomputeCellAttributesFrame:(UICollectionViewLayoutAttributes *)attributes
{
//獲取懸停處的y值
CGFloat minY = CGRectGetMinY(self.collectionView.bounds) + self.collectionView.contentInset.top;
//拿到布局屬性應(yīng)該出現(xiàn)的位置
CGFloat finalY = MAX(minY, attributes.frame.origin.y);
CGPoint origin = attributes.frame.origin;
origin.y = finalY;
attributes.frame = (CGRect){origin, attributes.frame.size};
//根據(jù)IndexPath設(shè)置zIndex能確立頂部懸停的cell被后來的cell覆蓋的層級關(guān)系
attributes.zIndex = attributes.indexPath.row;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
//由于cell在滑動(dòng)過程中會不斷修改cell的位置枕荞,所以需要不斷重新計(jì)算所有布局屬性的信息
return YES;
}
@end
在實(shí)現(xiàn)里面不需要-(CGSize)collectionViewContentSize方法的原因是柜候,對于利用UICollectionViewFlowLayout來進(jìn)行布局,而不是自定義的布局躏精,系統(tǒng)會自動(dòng)根據(jù)你設(shè)置的itemSize等信息計(jì)算出contentSize渣刷。
6、總結(jié)
通過上面的例子我們可以看到矗烛,UICollectionView相到于一個(gè)畫板辅柴,而UICollectionViewLayout則可以幫我們組織畫板的大小,以及畫板內(nèi)容的組織形態(tài)瞭吃。在日常開發(fā)需求中碌嘀,我們也需要重視UICollectionView,利用好它可以達(dá)到事半功倍的效果歪架。