序言
前段時間開發(fā)的時候誓军,需要在tableView上拉的時候?qū)崿F(xiàn)最底下的cell隨著滑動從左邊移動出來的效果(淘寶客戶端在上拉加載的時候從左邊滑動出現(xiàn)的效果)。苦思了很久脑溢,最終通過在scrollView的代理中通過判斷偏移量來改變當(dāng)前最下面的cell的frame實現(xiàn)這種效果喻奥,但是這樣的實現(xiàn)卻遠(yuǎn)遠(yuǎn)達(dá)不到我想要的目標(biāo)席纽。同時,在滑動tableView時進(jìn)行大量繁雜的計算還造成了上拉時輕微卡頓的現(xiàn)象撞蚕,于是我導(dǎo)出尋找另外的解決方案润梯。終于,被我忽視了很久的UICollectionView成為了解決這一問題的最佳選擇甥厦。
關(guān)于UICollectionView
UICollectionView在iOS6之后第一次被引入纺铭,它和tableView共享一套API設(shè)計,但是功能卻遠(yuǎn)比tableView強大刀疙,其最大的特點在于完美的靈活性和可定制化舶赔。它對于子視圖顯示的過程而言僅僅扮演了容器的對象,它不在乎子視圖內(nèi)真正的內(nèi)容谦秧。由于它將決定子視圖位置竟纳、大小以及外觀等屬性的任務(wù)委托給單獨的一個布局對象(UICollectionViewLayout)撵溃,我們可以通過繼承這個布局類來實現(xiàn)自定義化的collectionView。
通過上圖锥累,我們可以看到UICollectionView不同于tableView的一個特點是前者每一個item并不是單獨的一行缘挑,官方文檔中提及
During layout, the flow layout object adds items to the current line until there is not enough space left to fit an entire item.
在把新的item添加到當(dāng)前行上的時候,flowLayout對象會計算當(dāng)前行上剩余的寬度是否足以容納下這個item桶略,如果無法容納语淘,那么就換行。更多關(guān)于collectionViewLayout特性可以查看這篇文章际歼。
對于自定義collectionView來說惶翻,瀑布流可能是最為基本的自定義方案。因此鹅心,我們今天的例子將從定制瀑布流開始维贺。蘋果官方文檔對于UICollectionViewLayout的使用有以下說明:
You can configure the flow layout either programmatically or using Interface Builder in Xcode. The steps for configuring the flow layout are as follows:
1、Create a flow layout object and assign it to your collection view.
2巴帮、Configure the width and height of cells.
3溯泣、Set the spacing options (as needed) for the lines and items.
4、If you want section headers or section footers, specify their size.
5榕茧、Set the scroll direction for the layout.
大意是通過創(chuàng)建UICollectionViewLayout對象來創(chuàng)建我們的collectionView垃沦,然后配置cell的尺寸、間距行距等用押,有必要的時候還能對組頭組尾視圖進(jìn)行設(shè)置肢簿。
UICollectionViewLayout
在學(xué)習(xí)如何自定義之前,我們需要了解一下UICollectionView中不同類的依賴關(guān)系
在顯示cell的時候蜻拨,collectionView會向UICollectionViewLayout詢問布局屬性池充。這時候,我們可以通過重載方法創(chuàng)建UICollectionViewLayoutAttributes對象缎讼,每個對象保存一個item的布局屬性收夸。接下來,我們會通過創(chuàng)建UICollectionViewFlowLayout的子類來實現(xiàn)瀑布流血崭,之所以選擇這個類的原因在于它的定制要比定制繼承自UICollectionViewLayout的子類簡單卧惜,因為它包括了itemSize、minimumLineSpacing等重要的布局屬性夹纫。
瀑布流最大的特點在于不同尺寸的cell之間進(jìn)行緊密的縫合連接咽瓷,但是如果我們使用的是默認(rèn)的布局對象,那么顯示的效果就會跟下面的圖一樣不堪入目:
我們可以看到舰讹,系統(tǒng)計算下一行行高的時候是基于當(dāng)前本行中y坐標(biāo)和高度和的最大值加上行距就是下一行的y坐標(biāo)起始點 nextLineY = MAX(cell.y + cell.height)+lineSpacing茅姜。所以我們想要實現(xiàn)瀑布流的做法就是通過保存每一列當(dāng)前的高度,然后用來修改這一列上下一個item的起始坐標(biāo)來實現(xiàn)每一個cell之間緊湊縫合的效果月匣。
相關(guān)方法
- (void)prepareLayout
系統(tǒng)在準(zhǔn)備對item進(jìn)行布局前會調(diào)用這個方法钻洒,我們重寫這個方法之后可以在方法里面預(yù)先設(shè)置好需要用到的變量屬性等奋姿。比如在瀑布流開始布局前,我們可以對存儲瀑布流高度的數(shù)組進(jìn)行初始化航唆。有時我們還需要將布局屬性對象進(jìn)行存儲,比如卡片動畫式的定制院刁,也可以在這個方法里面進(jìn)行初始化數(shù)組糯钙。切記要調(diào)用[super prepareLayout];
- (CGSize)collectionViewContentSize
由于collectionView將item的布局任務(wù)委托給layout對象,那么滾動區(qū)域的大小對于它而言是不可知的退腥。自定義的布局對象必須在這個方法里面計算出顯示內(nèi)容的大小任岸,包括supplementaryView和decorationView在內(nèi)。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
個人覺得完成定制布局最核心的方法狡刘,沒有之一享潜。collectionView調(diào)用這個方法并將自身坐標(biāo)系統(tǒng)中的矩形傳過來,這個矩形代表著當(dāng)前collectionView可視的范圍嗅蔬。我們需要在這個方法里面返回一個包括UICollectionViewLayoutAttributes對象的數(shù)組剑按,這個布局屬性對象決定了當(dāng)前顯示的item的大小、層次澜术、可視屬性在內(nèi)的布局屬性艺蝴。同時,這個方法還可以設(shè)置supplementaryView和decorationView的布局屬性鸟废。合理使用這個方法的前提是不要隨便返回所有的屬性猜敢,除非這個view處在當(dāng)前collectionView的可視范圍內(nèi),又或者大量額外的計算造成的用戶體驗下降——你加班的原因盒延。
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
相當(dāng)重要的方法缩擂。collectionView可能會為了某些特殊的item請求特殊的布局屬性,我們可以在這個方法中創(chuàng)建并且返回特別定制的布局屬性添寺。根據(jù)傳入的indexPath調(diào)用[UICollectionViewLayoutAttributes layoutAttributesWithIndexPath: ]方法來創(chuàng)建屬性對象胯盯,然后設(shè)置創(chuàng)建好的屬性,包括定制形變计露、位移等動畫效果在內(nèi)
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
當(dāng)collectionView的bounds改變的時候陨闹,我們需要告訴collectionView是否需要重新計算布局屬性,通過這個方法返回是否需要重新計算的結(jié)果薄坏。簡單的返回YES會導(dǎo)致我們的布局在每一秒都在進(jìn)行不斷的重繪布局趋厉,造成額外的計算任務(wù)。
準(zhǔn)備工作
打開Xcode創(chuàng)建一個新項目胶坠,命名為名字前綴+WaterFlowDemo君账。創(chuàng)建好項目之后,選擇ViewController.h沈善,然后修改父類為UICollectionViewController
打開Main故事板乡数,然后刪除已經(jīng)存在的ViewController椭蹄,顯示右側(cè)控件欄拉進(jìn)來一個UICollectionViewController。然后選中新增進(jìn)來的控制器净赴,設(shè)置為故事板的初始化控制器绳矩。
然后command+N新建文件,選擇父類為UICollectionViewFlowLayout玖翅,命名為WaterFlowLayout翼馆,創(chuàng)建布局類。接著金度,在故事板的控制器里面選擇collectionView的布局對象应媚。在開始重寫方法實現(xiàn)瀑布流之前,我們要思考好瀑布流的實現(xiàn)思路:
首先猜极,由于瀑布流的item尺寸長寬不定中姜,正常而言分為等寬(豎向)、等高(橫向)兩種(ps:如果不等寬也不等高跟伏,先不說代碼上實現(xiàn)起來的復(fù)雜程度丢胚,單單是視覺上就不合格了)。每一個item都是緊湊連接的受扳,因此我們需要一個容器來存儲每一列/行當(dāng)前的最大長/寬值嗜桌。這里我們將使用一個存儲不同列高度(等寬)的數(shù)組來實現(xiàn)。
其次辞色,每一個item的尺寸在第一次展示的時候就應(yīng)該確定好骨宠。雖然瀑布流的尺寸是隨機的(實際應(yīng)用中經(jīng)常是由圖片尺寸決定的),但是我們并不希望在下拉出一大截位置后回頭滾動回來的時候相满,這些item的尺寸再次發(fā)生變化层亿。這不符合邏輯,也會導(dǎo)致高度計算上的巨大偏差立美。所以我們還需要把這些坐標(biāo)尺寸存儲起來匿又,并和item對應(yīng)的indexPath成對存儲。因此我們用NSStringFromCGRect()方法將item的位置信息轉(zhuǎn)換成字符串后和indexPath成對存儲在字典中建蹄,而且由于frame可能會出現(xiàn)相同值碌更,所以我們將frame轉(zhuǎn)換成字符串存儲并讓indexPath作為key。
綜合上面的考慮洞慎,我們的layout類當(dāng)中應(yīng)該包括兩個成員屬性
@property (nonatomic, strong) NSMutableDictionary * attributes;?
@property (nonatomic, strong) NSMutableArray * colArray; ? ?
除此之外痛单,我們還需要幾個宏定義來表示包括item間距、行距劲腿、列數(shù)以及每一個item的寬度:
#define COLUMNCOUNT 3
#define SCREENWIDTH [UIScreen mainScreen].bounds.size.width
#define INTERITEMSPACING 10.0f
#define LINESPACING 10.0f 10.0f
#define ITEMWIDTH (SCREENWIDTH - (COLUMNCOUNT - 1)*INTERITEMSPACING) / 3
代碼實現(xiàn)
/**
* 準(zhǔn)備布局item前調(diào)用旭绒,我們要在這里面完成必要屬性的初始化
*/
- (void)prepareLayout
{
? ? [super prepareLayout];
? ? //初始化行距間距
? ? self.minimumLineSpacing = LINESPACING;
? ? self.minimumInteritemSpacing = INTERITEMSPACING;
? ? //初始化存儲容器
? ? _attributes = [NSMutableDictionary dictionary];
? ? _colArray = [NSMutableArray arrayWithCapacity: COLUMNCOUNT];
? ? for (int i = 0; i < COLUMNCOUNT; i++) {
? ? ? ? [_colArray addObject: @(.0f)];
? ? }
? ? //遍歷所有item獲取位置信息并進(jìn)行存儲
? ? NSUInteger sectionCount = [self.collectionView numberOfSections]; ? ?
? ? for (int section = 0; section < sectionCount; section++) {
? ? ? ? NSUInteger itemCount = [self.collectionView numberOfSection: section];
? ? ? ? for (int item = 0; item < itemCount; item++) {
? ? ? ? ? ? [self layoutItemFrameAtIndexPath: [NSIndexPath indexPathWithItem: item section: section]];
? ? ? ? }
? ? }
}
/**
* 用來設(shè)置每一個item的尺寸,然后和indexPath存儲起來
*/
- (void)layoutItemFrameAtIndexPath: (NSIndexPath *)indexPath
{
? ? CGSize itemSize = CGSizeMake(ITEMWIDTH, 100+arc4random%101);
? ? //獲取當(dāng)前三列高度中高度最低的一列
? ? NSUInteger smallestCol = 0;
? ? CGFloat lessHeight = [_colArray[smallestCol] doubleValue];
? ? for (int col = 1; col < _colArray.count; col++) {
? ? ? ? if (lessHeight < [_colArray[col] doubleValue]) {
? ? ? ? ? ? shortHeight = [_colArray[col] doubleValue];
? ? ? ? ? ? smallestCol = col;
? ? ? ? }
? ? }
? ? //在當(dāng)前高度最低的列上面追加item并且存儲位置信息
? ? UIEdgeInsets insets = self.collectionView.contentInset;
? ? CGFloat x = insets.left + smallestCol * (INTERITEMSPACING + ITEMWIDTH);
? ? CGRect frame = {x, insets.top + shortHeight, itemSize};
? ? [_attributes setValue: indexPath forKey: NSStringFromCGRect(frame)];
? ? [_colArray replaceObjectAtIndex: smallestCol withObject: @(CGRectGetMaxY(frame))];
}
/**
* 返回所有當(dāng)前在可視范圍內(nèi)的item的布局屬性
*/
- (NSArray *)layoutAttributesForElementsInRect: (CGRect)rect
{
? ? //獲取當(dāng)前所有可視item的indexPath。通過調(diào)用父類獲取的布局屬性數(shù)組會缺失一部分可視item的布局屬性
? ? NSMutableArray * indexPaths = [NSMutableArray array];
? ? for (NSString * rectStr in _attributes) {
? ? ? ? CGRect cellRect = CGRectFromString(rectStr);
? ? ? ? if (CGRectIntersectsRect(cellRect, rect)) {
? ? ? ? ? ? NSIndexPath * indexPath = _attributes[rectStr];
? ? ? ? ? ? [indexPaths addObject: indexPath];
? ? ? ? }
? ? }
? ? //獲取當(dāng)前要顯示的所有item的布局屬性并返回
? ? NSMutableArray * layoutAttributes = [NSMutableArrayWithCapacity: indexPaths.count];
? ? [indexPaths enumerateObjectsUsingBlock: ^(NSIndexPath * indexPath, NSUInteger idx, BOOL * stop) {
? ? ? ? UICollectionViewLayoutAttributes * attributes = [self layoutAttributesForItemAtIndexPath: indexPath];
? ? ? ? [layoutAttributes addObject: attributes];
? ? }];
? ? return layoutAttributes;
}
/**
* 返回對應(yīng)indexPath的布局屬性
*/
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath: (NSIndexPath *)indexPath {
? ? UICollectionViewLayoutAttributes * attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
? ? for (NSString * frame in _attributes) {
? ? ? ? if (_attributes[frame] == indexPath) {
? ? ? ? ? ? attributes.frame = CGRectFromString(frame);
? ? ? ? ? ? break;
? ? ? ? }
? ? }
? ? return attributes;
}
/**
* 設(shè)置collectionView的可滾動范圍(瀑布流必要實現(xiàn))
*/
- (CGSize)collectionViewContentSize
{
? ? __block CGFloat maxHeight = [_colArray[0] floatValue];
? ? [_colArray enumerateObjectsUsingBlock: ^(NSNumber * height, NSUInteger idx, BOOL *stop) {? ?
? ? ? ? if (height.floatValue > maxHeight) {
? ? ? ? ? ? maxHeight = height.floatValue;
? ? ? ? }
? ? }
? ? return CGSizeMake(CGRectGetWidth(self.collectionView.frame), maxHeight + self.collectionView.contentInset.bottom);
}
/**
* 在collectionView的bounds發(fā)生改變的時候刷新布局
*/
- (BOOL)shouldInvalidateLayoutForBoundsChange: (CGRect)newBounds
{
? ? return !CGRectEqualToRect(self.collectionView.bounds, newBounds);
}
多說幾句
使用collectionView完成業(yè)務(wù)需求之后挥吵,它幾乎成為了我最喜愛的控件重父,極高的可定制性決定了它的重要地位。雖然從代碼實現(xiàn)的角度上來說忽匈,合適的布局幾乎可以讓我們實現(xiàn)tableView房午,但它不是為了取代后者而出現(xiàn)的。collectionView相較tableView而言丹允,并不那么的大眾化郭厌,畢竟常規(guī)的數(shù)據(jù)展示使用tableView就能完美顯示。
上面瀑布流的代碼中嫌松,item的高度是在layout里面隨機生成的沪曙,但在實際開發(fā)中奕污,高度的生成不該由布局對象來完成萎羔。為了解決這個問題,我們可以在自定義的布局對象中增加一個遵循UICollectionViewDelegateFlowLayout的代理人屬性:
@property (nonatomic, weak) id<UICollectionViewDelegateFlowLayout> delegate;
然后在prepareLayout方法中加上一句self.delegate = self.collectionView.delegate碳默。這樣我們就可以通過向代理對象發(fā)送協(xié)議方法消息來獲取itemSize(將item的尺寸交給controller來完成)贾陷。
除了上面提到的屬性之外,UICollectionViewLayoutAttributes還有center嘱根、zIndex髓废、transform3D等屬性能讓我們定制滑動的形變等動畫效果,例如卡片動畫就是我非常喜愛的效果之一该抒。此外還有下面六個方法幫助我們在對item進(jìn)行刪除和添加操作的時候定制動畫效果
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
關(guān)于這些方法的使用可以學(xué)習(xí)這篇文章慌洪。
文集:iOS開發(fā)
轉(zhuǎn)載注明原文地址以及作者