布局萬花筒:UIColletionview

UICollection是iOS6的時候引入的,它是同UITableview共享一套API設(shè)計,都是基于datasource和delegate,都繼承自UIScrollView。但它又與UITableview有很大不同,它進行了進一步的抽象,將它的所有的子視圖的位置、大小、transform委托給了一個單獨的布局對象:UICollectionViewLayout。這是一個抽象類,我們可以繼承它來實現(xiàn)任何想要的布局,系統(tǒng)也為我們提供了一個開箱即食的實現(xiàn)UICollectionViewFlowLayout。在我看來,沒有任何布局是UICollenctionViewLayout不能實現(xiàn)的,如果有那就自定義一個。

UITableview只能提供豎直滑動的布局,而且默認情況下cell的寬度和tableView的寬度一致,而且cell的排列順序也是挨次排列。UICollectionView則為我們提供了另一種可能:它能提供豎直滑動的布局也能提供豎屏滑動的布局,而且cell的位置、大小等完全由你自己決定。所以在我們用到水平滑動的布局時,不要忙著用UIScrollView去實現(xiàn),可以先考慮UICollectionView能不能滿足要求,還有一個好處是你不要自己考慮滑動視圖cell的重用問題。

這篇文章會如何自定義UICollectionViewLayout來實現(xiàn)任意布局,默認你已經(jīng)會使用系統(tǒng)提供的UICollectionViewFlowLayout來進行標準的Grid View布局了。

1、UICollectuonViewFlowLayout

系統(tǒng)為我們提供了一個自定義的布局實現(xiàn):UICollectionViewFlowLayout,通過它我們可以實現(xiàn)Grid View類型的布局,也就是像一個一個格子挨次排列的布局,對于大多數(shù)的情況下,使用它就能滿足我們的要求了。系統(tǒng)為我們提供了布局所用的參數(shù),我們在使用的時候只需去確認這些參數(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類型的布局不能滿足我們的需求,這是就需要自定義一個Layout。

2、UICollectionViewLayout ?VS ? UICollectionViewFlowLayout

UICollectionViewFlowLayout繼承自UICollectionViewLayout,我們可以直接使用它,我們只需要提供cell的大小,以及行間距、列間距,他就會自己計算出每個cell的位置以及UICollectionView的滑動范圍contentSize。但它只能提供一個方向的滑動,也就是說我們自定義的類如果繼承自UICollectionViewFlowLayout,則只能在一個方向上滑動的布局,要么水平方向要么豎直方向。反之,則需要繼承自UICollectionViewLayout,UICollectionViewLayout是一個抽象類,不能直接使用。

3、自定義布局需要實現(xiàn)的方法

UICollectionViewLayout文檔為我們列出了需要實現(xiàn)的方法:

以上列出的這六個方法不是都需要我們自己實現(xiàn)的,而是根據(jù)需要,選擇其中的某些方法實現(xiàn)。

collectionViewContentSize

UICollection繼承自UIScrollView,我們都知道UIScrollView的一個重要參數(shù):contentSize,如果這個參數(shù)不對,那么你布局的內(nèi)容就不能完全展示,而collectionViewContentSize就是為了得到這個參數(shù),UICollection就像一個畫板,而collectionViewContentSize則規(guī)定了畫板的大小,如果是繼承自UICollectionViewFlowLayout,而且每個section里面的cell大小是通過UICollectionViewFlowLayout的參數(shù)設(shè)定的,大小和位置也不在自定義的過程中隨意更改,那么collectionViewContentSize是可以不自己重寫的,系統(tǒng)會自己計算contentSize,如果是繼承自UICollectionViewLayout,那就需要根據(jù)你自己的展示布局去提供合適的CGSize給collectionViewContentSize。

layoutAttributesForElementsInRect

這個方法的參數(shù)是UICollectionView當前的bounds,也就是視圖當前的可見區(qū)域,返回值是一個包含對象為UICollectionViewLayoutAttributes的數(shù)組,UICollectionView的可見區(qū)域內(nèi)包含cell、supplementary view、decoration view(這里統(tǒng)稱cell,因為它們都是collectionView的一個子視圖),它們的位置、大小等信息都由對應(yīng)的UICollectionViewLayoutAttributes控制。默認情況下這個LayoutAttributes包含indexPath、frame、center、size、transform3D、alpha以及hidden屬性。如果你還需要控制其他的屬性,你可以自己自定義一個UICollectionViewLayoutAttributes的子類,加上任意你想要的屬性。

布局屬性對象(UICollectionViewLayoutAttributes)通過indexPath和cell關(guān)聯(lián)起來,當collectionView展示cell時,會通過這些布局屬性對象拿到布局信息。

返回原話題,layoutAttributesForElementsInRect方法的返回值是一個數(shù)組,這個數(shù)組里面是傳遞進來的可見區(qū)域內(nèi)的cell所對應(yīng)的UICollectionViewLayoutAttributes。

要拿到可見區(qū)域內(nèi)的布局屬性,通常的做法如下:

如果你是繼承自UICollectionViewFlowLayout,并且設(shè)置好了itemSize、行間距、列間距等信息,那么你通過[super layoutAttributesForElementsInRect:rect]就能拿到可見區(qū)域內(nèi)的布局屬性,反之,則進入步奏2。

創(chuàng)建一個空數(shù)組,用于存放可見區(qū)域內(nèi)的布局屬性。

從UICollectionView的數(shù)據(jù)源中取出你需要展示的數(shù)據(jù),然后根據(jù)你想要的布局計算出哪些indexPath在當前可見區(qū)域內(nèi),通過CGRectIntersectsRect函數(shù)可以判斷兩個CGRect是否有交集來確定。然后循環(huán)調(diào)用layoutAttributesForItemAtIndexPath:來確定每一個布局屬性的frame等數(shù)據(jù)。同樣,如果當前區(qū)域內(nèi)有supplementary view或者decoration view,你也需要調(diào)用:layoutAttributesForSupplementaryViewOfKind:atIndexPath或者layoutAttributesForDecorationViewOfKind:atIndexPath,最后將這些布局屬性添加到數(shù)組中返回。這里需要多說一點的是,有些布局屬性在UICollectionViewLayout的prepareLayout就根據(jù)數(shù)據(jù)源全部計算了出來,比如瀑布流樣式的布局,這個時候你就只需要返回布局屬性的frame和當前可見區(qū)域有交集的對象就行。

layoutAttributesFor…IndexPath

這里用三個點,是因為有三個類似的方法:

layoutAttributesForItemAtIndexPath:

layoutAttributesForSupplementaryViewOfKind:atIndexPath:

layoutAttributesForDecorationViewOfKind:atIndexPath:

它們分別為cell、supplementaryView、decorationView返回布局屬性,它們的實現(xiàn)不是必須的,它們只是為對應(yīng)的IndexPath返回布局屬性,如果你能通過其他方法拿到對應(yīng)indexPath處的布局屬性,那就沒必要非要實現(xiàn)這幾個方法。

以layoutAttributesForItemAtIndexPath:為例,你可以通過+[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]方法拿到一個布局屬性對象,然后你可能需要訪問你的數(shù)據(jù)源去算出該indexPath處的布局屬性的frame等信息,然后賦值給它。

shouldInvalidateLayoutForBoundsChange

這個是用來告訴collectionView是否需要根據(jù)bounds的改變而重新計算布局屬性,比如橫豎屏的旋轉(zhuǎn)。通常的寫法如下:

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

{

CGRect oldBounds = self.collectionView.bounds;

if (CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds)) {

return YES;

}

return NO;

}

需要注意的是,當在滑動的過程中,需要對某些cell的布局進行更改,那么就需要在這個方法里面返回YES,告訴UICollectionView重新計算布局。因為一個cell的改變會引起整個UICollectionView布局的改變。

4、示例一:瀑布流實現(xiàn)

瀑布流的排列一般用于圖片或者商品的展示,它的布局特點是等寬變高,cell的排列是找到最短的那一列,然后把cell放到那個位置,效果如下:


下面我們來看看具體的實現(xiàn),這里的布局行間距和列間距都定位10,列數(shù)固定為3列,如上圖所示。

系統(tǒng)提供給我們的UICollectionViewFlowLayout顯然不能實現(xiàn)瀑布流的布局,因為它的默認實現(xiàn)是一行一列整齊對齊的,所以我們需要新建一個繼承自UICollectionViewFlowLayout的類,然后來講解一下這個類的實現(xiàn)。

prepareLayout

在講解如何布局瀑布流之前需要先說明一下UICollectionViewFlowLayout的prepareLayout方法,他會在UICollectionView布局之前調(diào)用,調(diào)用[self.collectionView reloadData]和[self.collectionView.collectionViewLayout invalidateLayout]的時候prepareLayout也會進行調(diào)用,如果shouldInvalidateLayoutForBoundsChange返回YES,prepareLayout方法同樣也會調(diào)用。所以這個函數(shù)是提前進行數(shù)據(jù)布局計算的絕佳地方。

在進行瀑布流布局的時候我們可以在prepareLayout里面根據(jù)數(shù)據(jù)源,計算出所有的布局屬性并緩存起來:

- (void)prepareLayout {

[super prepareLayout];

//記錄布局需要的contentSize的高度

self.contentHeight = 0;

//columnHeights數(shù)組會記錄各列的當前布局高度

[self.columnHeights removeAllObjects];

//默認高度是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)建每一個cell對應(yīng)的布局屬性

for (NSInteger index = 0; index < count; index++) {

//創(chuàng)建indexPath

NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];

//獲取cell布局屬性,在layoutAttributesForItemAtIndexPath里面計算具體的布局信息

UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];

[self.attrsArray addObject:attrs];

}

}

在layoutAttributesForItemAtIndexPath方法里面去根據(jù)參數(shù)indexPath拿到數(shù)據(jù)源里面對應(yīng)位置的展示數(shù)據(jù),根據(jù)等寬的前提,等比例的獲得布局屬性的高度,然后根據(jù)記錄每列當前布局到的高度的數(shù)組columnHeights來找到當前布局最短的那一列,從而獲取到布局屬性的origin信息,這樣在等寬的前提下就獲取到了當前indexPath處的布局屬性的frame信息。然后更新columnHeights里面的數(shù)據(jù),并且讓記錄布局所需高度的變量contentHeight等于當前列高度數(shù)組里面的最大值。

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

//獲取一個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ù)列信息,計算出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;

}

滑動的過程在,cell會不斷重用,系統(tǒng)會調(diào)用layoutAttributesForElementsInRect方法來獲取當前可見區(qū)域內(nèi)的布局屬性,由于所有的布局屬性都緩存了起來,則只需返回布局屬性的frame和當前可見區(qū)域有交集的布局屬性就行。

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

NSMutableArray *rArray = [NSMutableArray array];

for (UICollectionViewLayoutAttributes *cacheAttr in _attrsArray) {

if (CGRectIntersectsRect(cacheAttr.frame, rect)) {

[rArray addObject:cacheAttr];

}

}

return rArray;

}

最后由于我們自定義了每個cell的高度及布局,所以系統(tǒng)是不知道UICollectionView當前的contentSize的大小,所以我們需要在collectionViewContentSize方法里返回正確的size以確保所以cell都能正常滑動到可見區(qū)域里來。

-(CGSize)collectionViewContentSize {

return CGSizeMake(CGRectGetWidth(self.collectionView.frame), self.contentHeight + self.edgeInsets.bottom);

}

至此,瀑布流的布局就完成了,實現(xiàn)起來非常簡單,最關(guān)鍵的地方就是計算布局屬性的frame信息。

5、示例二:卡片吸頂布局

卡片吸頂布局的效果如下:

可以看到滑到頂部的cell本應(yīng)該移出當前可見區(qū)域,但我們實現(xiàn)的效果是移到頂部后就懸停,并且可以被后來的cell覆蓋。

實現(xiàn)的原理非常簡單,cell的布局使用UICollectionViewFlowLayout就能實現(xiàn),我們新建一個繼承自UICollectionViewFlowLayout的子類,利用這個子類創(chuàng)建布局,可以利用UICollectionViewFlowLayout提供的參數(shù)來構(gòu)建一個不吸頂展示的collectionView:

只需要提供給UICollectionViewFlowLayoutitemSize和minimumLineSpacing就行,行間距minimumLineSpacing設(shè)置為一個負數(shù)就能建立起互相疊加的效果。

要建立吸頂?shù)男Ч恍枰谠瓉淼牟季只A(chǔ)上,判斷布局屬性frame小于布局頂部的y值,就將布局屬性的frame的y值設(shè)置為頂部的y值就行,這樣滑動到頂部的cell都會在頂部懸停下來。

@implementation CardCollectionViewFlowLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

{

//拿到當前可見區(qū)域內(nèi)的布局屬性

NSArray *oldItems = [super layoutAttributesForElementsInRect:rect];

//處理當前可見區(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在滑動過程中會不斷修改cell的位置,所以需要不斷重新計算所有布局屬性的信息

return YES;

}

@end

在實現(xiàn)里面不需要-(CGSize)collectionViewContentSize方法的原因是,對于利用UICollectionViewFlowLayout來進行布局,而不是自定義的布局,系統(tǒng)會自動根據(jù)你設(shè)置的itemSize等信息計算出contentSize。

6、總結(jié)

通過上面的例子我們可以看到,UICollectionView相到于一個畫板,而UICollectionViewLayout則可以幫我們組織畫板的大小,以及畫板內(nèi)容的組織形態(tài)。在日常開發(fā)需求中,我們也需要重視UICollectionView,利用好它可以達到事半功倍的效果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容