布局万花筒:UIColletionview
UICollectionView是iOS6的時(shí)候引入的,它同UITableView共享一套API設(shè)計(jì),都是基于datasource和delegate,都繼承自UIScrollView。但它又與UITableView有很大不同,它進(jìn)行了進(jìn)一步的抽象,將它的所有子視圖的位置、大小、transform委托給了一個(gè)單獨(dú)的布局對(duì)象:UICollectionViewLayout。這是一個(gè)抽象類,我們可以繼承它來(lái)實(shí)現(xiàn)任何想要的布局,系統(tǒng)也為我們提供了一個(gè)開(kāi)箱即食的實(shí)現(xiàn)UICollectionViewFlowLayout。在我看來(lái),沒(méi)有任何排列布局是UICollectionViewLayout不能實(shí)現(xiàn)的,如果有那就自定義一個(gè)。
UITableView只能提供豎直滑動(dòng)的布局,而且默認(rèn)情況下cell的寬度和tableView的寬度一致,而且cell的排列順序也是挨次排列。UICollectionView則為我們提供了另一種可能:它能提供豎直滑動(dòng)的布局也能提供水平滑動(dòng)的布局,而且cell的位置、大小等完全由你自己決定。所以我們?cè)谟龅剿交瑒?dòng)的布局時(shí),不要忙著用UIScrollView去實(shí)現(xiàn),可以先考慮UICollectionView能不能滿足要求,還有一個(gè)好處是你不要自己考慮滑動(dòng)視圖(cell)重用的問(wèn)題。
這篇文章會(huì)講解如何自定義UICollectionViewLayout來(lái)實(shí)現(xiàn)任意布局,默認(rèn)你已經(jīng)會(huì)使用系統(tǒng)提供的UICollectionViewFlowLayout來(lái)進(jìn)行標(biāo)準(zhǔn)的Grid View布局了。
1、UICollectionViewFlowLayout
系統(tǒng)為我們提供了一個(gè)自定義的布局實(shí)現(xiàn):UICollectionViewFlowLayout,通過(guò)它我們可以實(shí)現(xiàn)Grid View類型的布局,也就是像一個(gè)一個(gè)格了挨次排列的布局,對(duì)于大多數(shù)的情況,使用它就能滿足我們的要求了。系統(tǒng)為我們提供了布局所需的參數(shù),我們?cè)谑褂玫臅r(shí)候只需要去確定這些參數(shù)就行:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 |
Grid View樣式的UICollectionView如下所示:
如果上面所說(shuō)的Grid View類型的布局不能滿足我們的需求,這時(shí)就需要自定義一個(gè)Layout。
2、UICollectionViewLayout VS UICollectionViewFlowLayout
UICollectionViewFlowLayout繼承自UICollectionViewLayout,我們可以直接使用它,我們只需要提供cell的大小,以及行間距、列間距,它就會(huì)自己計(jì)算出每個(gè)cell的位置以及UICollectionView的滑動(dòng)范圍contentSize。但它只能提供一個(gè)方向的滑動(dòng),也就是說(shuō)我們自定義的類如果繼承自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ù)不對(duì),那么你布局的內(nèi)容就不能完全展示,而collectionViewContentSize就是為了得到這個(gè)參數(shù),UICollection就像一個(gè)畫板,而collectionViewContentSize則規(guī)定了畫板的大小,如果是繼承自UICollectionViewFlowLayout,而且每個(gè)section里面的cell大小是通過(guò)UICollectionViewFlowLayout的參數(shù)設(shè)定的,大小和位置也不在自定義的過(guò)程中隨意更改,那么collectionViewContentSize是可以不自己重寫的,系統(tǒng)會(huì)自己計(jì)算contentSize,如果是繼承自UICollectionViewLayout,那就需要根據(jù)你自己的展示布局去提供合適的CGSize給collectionViewContentSize。
layoutAttributesForElementsInRect
這個(gè)方法的參數(shù)是UICollectionView當(dāng)前的bounds,也就是視圖當(dāng)前的可見(jiàn)區(qū)域,返回值是一個(gè)包含對(duì)象為UICollectionViewLayoutAttributes的數(shù)組,UICollectionView的可見(jiàn)區(qū)域內(nèi)包含cell、supplementary view、decoration view(這里統(tǒng)稱cell,因?yàn)樗鼈兌际莄ollectionView的一個(gè)子視圖),它們的位置、大小等信息都由對(duì)應(yīng)的UICollectionViewLayoutAttributes控制。默認(rèn)情況下這個(gè)LayoutAttributes包含indexPath、frame、center、size、transform3D、alpha以及hidden屬性。如果你還需要控制其他的屬性,你可以自己自定義一個(gè)UICollectionViewLayoutAttributes的子類,加上任意你想要的屬性。
布局屬性對(duì)象(UICollectionViewLayoutAttributes)通過(guò)indexPath和cell關(guān)聯(lián)起來(lái),當(dāng)collectionView展示cell時(shí),會(huì)通過(guò)這些布局屬性對(duì)象拿到布局信息。
返回原話題,layoutAttributesForElementsInRect方法的返回值是一個(gè)數(shù)組,這個(gè)數(shù)組里面是傳遞進(jìn)來(lái)的可見(jiàn)區(qū)域內(nèi)的cell所對(duì)應(yīng)的UICollectionViewLayoutAttributes。
要拿到可見(jiàn)區(qū)域內(nèi)的布局屬性,通常的做法如下:
-
如果你是繼承自UICollectionViewFlowLayout,并且設(shè)置好了itemSize、行間距、列間距等信息,那么你通過(guò)[super layoutAttributesForElementsInRect:rect]就能拿到可見(jiàn)區(qū)域內(nèi)的布局屬性,反之,則進(jìn)入步奏2。
-
創(chuàng)建一個(gè)空數(shù)組,用于存放可見(jiàn)區(qū)域內(nèi)的布局屬性。
-
從UICollectionView的數(shù)據(jù)源中取出你需要展示的數(shù)據(jù),然后根據(jù)你想要的布局計(jì)算出哪些indexPath在當(dāng)前可見(jiàn)區(qū)域內(nèi),通過(guò)CGRectIntersectsRect函數(shù)可以判斷兩個(gè)CGRect是否有交集來(lái)確定。然后循環(huán)調(diào)用layoutAttributesForItemAtIndexPath:來(lái)確定每一個(gè)布局屬性的frame等數(shù)據(jù)。同樣,如果當(dāng)前區(qū)域內(nèi)有supplementary view或者decoration view,你也需要調(diào)用:layoutAttributesForSupplementaryViewOfKind:atIndexPath或者layoutAttributesForDecorationViewOfKind:atIndexPath,最后將這些布局屬性添加到數(shù)組中返回。這里需要多說(shuō)一點(diǎn)的是,有些布局屬性在UICollectionViewLayout的prepareLayout就根據(jù)數(shù)據(jù)源全部計(jì)算了出來(lái),比如瀑布流樣式的布局,這個(gè)時(shí)候你就只需要返回布局屬性的frame和當(dāng)前可見(jiàn)區(qū)域有交集的對(duì)象就行。
layoutAttributesFor…IndexPath
這里用三個(gè)點(diǎn),是因?yàn)橛腥齻€(gè)類似的方法:
-
layoutAttributesForItemAtIndexPath:
-
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
-
layoutAttributesForDecorationViewOfKind:atIndexPath:
它們分別為cell、supplementaryView、decorationView返回布局屬性,它們的實(shí)現(xiàn)不是必須的,它們只是為對(duì)應(yīng)的IndexPath返回布局屬性,如果你能通過(guò)其他方法拿到對(duì)應(yīng)indexPath處的布局屬性,那就沒(méi)必要非要實(shí)現(xiàn)這幾個(gè)方法。
以layoutAttributesForItemAtIndexPath:為例,你可以通過(guò)+[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]方法拿到一個(gè)布局屬性對(duì)象,然后你可能需要訪問(wèn)你的數(shù)據(jù)源去算出該indexPath處的布局屬性的frame等信息,然后賦值給它。
shouldInvalidateLayoutForBoundsChange
這個(gè)是用來(lái)告訴collectionView是否需要根據(jù)bounds的改變而重新計(jì)算布局屬性,比如橫豎屏的旋轉(zhuǎn)。通常的寫法如下:
| 1 2 3 4 5 6 7 8 | ??-?(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { ????CGRect?oldBounds?=?self.collectionView.bounds; ????if?(CGRectGetWidth(newBounds)?!=?CGRectGetWidth(oldBounds))?{ ????????return?YES; ????} ????????return?NO; } |
需要注意的是,當(dāng)在滑動(dòng)的過(guò)程中,需要對(duì)某些cell的布局進(jìn)行更改,那么就需要在這個(gè)方法里面返回YES,告訴UICollectionView重新計(jì)算布局。因?yàn)橐粋€(gè)cell的改變會(huì)引起整個(gè)UICollectionView布局的改變。
4、示例一:瀑布流實(shí)現(xiàn)
瀑布流的排列一般用于圖片或者商品的展示,它的布局特點(diǎn)是等寬變高,cell的排列是找到最短的那一列,然后把cell放到那個(gè)位置,效果如下:
下面我們來(lái)看看具體的實(shí)現(xiàn),這里的布局行間距和列間距都定位10,列數(shù)固定為3列,如上圖所示。
系統(tǒng)提供給我們的UICollectionViewFlowLayout顯然不能實(shí)現(xiàn)瀑布流的布局,因?yàn)樗哪J(rèn)實(shí)現(xiàn)是一行一列整齊對(duì)齊的,所以我們需要新建一個(gè)繼承自UICollectionViewFlowLayout的類,然后來(lái)講解一下這個(gè)類的實(shí)現(xiàn)。
prepareLayout
在講解如何布局瀑布流之前需要先說(shuō)明一下UICollectionViewFlowLayout的prepareLayout方法,他會(huì)在UICollectionView布局之前調(diào)用,調(diào)用[self.collectionView reloadData]和[self.collectionView.collectionViewLayout invalidateLayout]的時(shí)候prepareLayout也會(huì)進(jìn)行調(diào)用,如果shouldInvalidateLayoutForBoundsChange返回YES,prepareLayout方法同樣也會(huì)調(diào)用。所以這個(gè)函數(shù)是提前進(jìn)行數(shù)據(jù)布局計(jì)算的絕佳地方。
在進(jìn)行瀑布流布局的時(shí)候我們可以在prepareLayout里面根據(jù)數(shù)據(jù)源,計(jì)算出所有的布局屬性并緩存起來(lái):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ??-?(void)prepareLayout?{ ????[super?prepareLayout]; ????//記錄布局需要的contentSize的高度 ????self.contentHeight?=?0; ????//columnHeights數(shù)組會(huì)記錄各列的當(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]; ????//通過(guò)數(shù)據(jù)源拿到需要展示的cell數(shù)量 ????NSInteger?count?=?[self.collectionView?numberOfItemsInSection:0]; ????//開(kāi)始創(chuàng)建每一個(gè)cell對(duì)應(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ù)源里面對(duì)應(yīng)位置的展示數(shù)據(jù),根據(jù)等寬的前提,等比例的獲得布局屬性的高度,然后根據(jù)記錄每列當(dāng)前布局到的高度的數(shù)組columnHeights來(lái)找到當(dāng)前布局最短的那一列,從而獲取到布局屬性的origin信息,這樣在等寬的前提下就獲取到了當(dāng)前indexPath處的布局屬性的frame信息。然后更新columnHeights里面的數(shù)據(jù),并且讓記錄布局所需高度的變量contentHeight等于當(dāng)前列高度數(shù)組里面的最大值。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | ??-(UICollectionViewLayoutAttributes?*)layoutAttributesForItemAtIndexPath:(NSIndexPath?*)indexPath?{ ????//獲取一個(gè)UICollectionViewLayoutAttributes對(duì)象 ????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?=?通過(guò)數(shù)據(jù)源以及寬度信息,獲取對(duì)應(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)的過(guò)程在,cell會(huì)不斷重用,系統(tǒng)會(huì)調(diào)用layoutAttributesForElementsInRect方法來(lái)獲取當(dāng)前可見(jiàn)區(qū)域內(nèi)的布局屬性,由于所有的布局屬性都緩存了起來(lái),則只需返回布局屬性的frame和當(dāng)前可見(jiàn)區(qū)域有交集的布局屬性就行。
| 1 2 3 4 5 6 7 8 9 10 | ??-(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)到可見(jiàn)區(qū)域里來(lái)。
| 1 2 3 | ??-(CGSize)collectionViewContentSize?{ ????return?CGSizeMake(CGRectGetWidth(self.collectionView.frame),?self.contentHeight?+?self.edgeInsets.bottom); } |
至此,瀑布流的布局就完成了,實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單,最關(guān)鍵的地方就是計(jì)算布局屬性的frame信息。
5、示例二:卡片吸頂布局
卡片吸頂布局的效果如下:
可以看到滑到頂部的cell本應(yīng)該移出當(dāng)前可見(jiàn)區(qū)域,但我們實(shí)現(xiàn)的效果是移到頂部后就懸停,并且可以被后來(lái)的cell覆蓋。
實(shí)現(xiàn)的原理非常簡(jiǎn)單,cell的布局使用UICollectionViewFlowLayout就能實(shí)現(xiàn),我們新建一個(gè)繼承自UICollectionViewFlowLayout的子類,利用這個(gè)子類創(chuàng)建布局,可以利用UICollectionViewFlowLayout提供的參數(shù)來(lái)構(gòu)建一個(gè)不吸頂展示的collectionView:
只需要提供給UICollectionViewFlowLayoutitemSize和minimumLineSpacing就行,行間距minimumLineSpacing設(shè)置為一個(gè)負(fù)數(shù)就能建立起互相疊加的效果。
要建立吸頂?shù)男Ч?#xff0c;只需要在原來(lái)的布局基礎(chǔ)上,判斷布局屬性frame小于布局頂部的y值,就將布局屬性的frame的y值設(shè)置為頂部的y值就行,這樣滑動(dòng)到頂部的cell都會(huì)在頂部懸停下來(lái)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @implementation?CardCollectionViewFlowLayout -?(NSArray?*)layoutAttributesForElementsInRect:(CGRect)rect { ????//拿到當(dāng)前可見(jiàn)區(qū)域內(nèi)的布局屬性 ????NSArray?*oldItems?=?[super?layoutAttributesForElementsInRect:rect]; ????//處理當(dāng)前可見(jiàn)區(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被后來(lái)的cell覆蓋的層級(jí)關(guān)系 ????attributes.zIndex?=?attributes.indexPath.row; } -?(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { ????//由于cell在滑動(dòng)過(guò)程中會(huì)不斷修改cell的位置,所以需要不斷重新計(jì)算所有布局屬性的信息 ????return?YES; } @end |
在實(shí)現(xiàn)里面不需要-(CGSize)collectionViewContentSize方法的原因是,對(duì)于利用UICollectionViewFlowLayout來(lái)進(jìn)行布局,而不是自定義的布局,系統(tǒng)會(huì)自動(dòng)根據(jù)你設(shè)置的itemSize等信息計(jì)算出contentSize。
6、總結(jié)
通過(guò)上面的例子我們可以看到,UICollectionView相到于一個(gè)畫板,而UICollectionViewLayout則可以幫我們組織畫板的大小,以及畫板內(nèi)容的組織形態(tài)。在日常開(kāi)發(fā)需求中,我們也需要重視UICollectionView,利用好它可以達(dá)到事半功倍的效果。
總結(jié)
以上是生活随笔為你收集整理的布局万花筒:UIColletionview的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: radio美化
- 下一篇: DDE学习1、DDE的原理和基本特点简介