本人菜鳥小白,最近研究了下UICollectionView自定義布局實(shí)現(xiàn)瀑布流等布局,主要是應(yīng)對公司需求,產(chǎn)品這么設(shè)計我也很無奈啊,初次寫文章,如有不對之處,歡迎大家提出,謝謝。(筆者第一次寫,發(fā)現(xiàn)排版布局太low,所以又寫了一版markdown版本,供小伙伴參考查閱markdown)。
豎向等寬等間隔瀑布流
先上一張效果圖

筆者自定義了CandyFlowLayout繼承自UICollectionViewFlowLayout,自定義了幾個屬性,其實(shí)就是UICollectionViewFlowLayout的屬性,只是重新命名了而已。

并自定義了初始化方法。其中CandyFlowLayoutDelegate協(xié)議主要實(shí)現(xiàn)兩個方法

.m文件主要實(shí)現(xiàn)幾個方法就能自定義布局
- (void)prepareLayout // 一定要實(shí)現(xiàn)此方法,筆者將布局信息全部在此重寫,當(dāng)然也可以寫到每個item的布局方法中,也就是- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath方法中,效果等同。
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect? // 返回存放所有item的布局信息數(shù)組
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath // 返回單個item的布局信息
- (CGSize)collectionViewContentSize // 返回正確的contentSize,這樣就可以在外部得到contentSize,筆者主要是應(yīng)對collectionView無滑動效果設(shè)置正確的height=contentsize.height。此方法可以不用重寫。
接下來看下豎向等寬等間隔瀑布流布局代碼:
- (void)createWaterfallItemAttributes {
? ? self.contentMaxHeight = 0;
? ? [self.itemHeights removeAllObjects];
? ? for (NSInteger i = 0; i < self.waterfallRowNumber; i ++) {
? ? ? ? // 默認(rèn)都是top
? ? ? ? [self.itemHeights addObject:@(self.sectionInsets.top)];
? ? }
? ? // 計算item width
? ? CGFloat width = (ScreenWidth - self.sectionInsets.left - self.sectionInsets.right - (self.waterfallRowNumber - 1) * self.minItemSpacing) / self.waterfallRowNumber * 1.0;
? ? for (NSInteger i = 0; i < self.numberOfSection; i ++) {
? ? ? ? NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
? ? ? ? for(NSIntegerj =0; j < numberOfItem; j ++) {
? ? ? ? ? ? NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
? ? ? ? ? ? UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
? ? ? ? ? ? //找出每行最短的一列
? ? ? ? ? ? NSIntegerminIndex =0;
? ? ? ? ? ? CGFloat minY = [self.itemHeights[0] floatValue];
? ? ? ? ? ? for(NSIntegern =1; n
? ? ? ? ? ? ? ? // 依次取出高度
? ? ? ? ? ? ? ? CGFloatitemY = [self.itemHeights[n]floatValue];
? ? ? ? ? ? ? ? if(minY > itemY) {
? ? ? ? ? ? ? ? ? ? minY = itemY;
? ? ? ? ? ? ? ? ? ? minIndex = n;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? CGFloatxOffset =self.sectionInsets.left+ minIndex * (width +self.minItemSpacing);
? ? ? ? ? ? CGFloatheight =0;
? ? ? ? ? ? if(self.delegate&& [self.delegaterespondsToSelector:@selector(heightForItemAtIndexPath:)]) {
? ? ? ? ? ? ? ? height = [self.delegateheightForItemAtIndexPath:indexPath];
? ? ? ? ? ? }
? ? ? ? ? ? CGFloatyOffset = minY;
? ? ? ? ? ? if(yOffset !=self.sectionInsets.top) {
? ? ? ? ? ? ? ? // 不是第一行,要加間隔
? ? ? ? ? ? ? ? yOffset +=self.minLineSpacing;
? ? ? ? ? ? }
? ? ? ? ? ? // 更新高度
? ? ? ? ? ? self.itemHeights[minIndex] =@(height + yOffset);
? ? ? ? ? ? // 更新contentSize height
? ? ? ? ? ? CGFloatmaxHeight = [self.itemHeights[minIndex]floatValue];
? ? ? ? ? ? if(self.contentMaxHeight< maxHeight) {
? ? ? ? ? ? ? ? // 最短的一列 + 高度 > 之前的最高高度
? ? ? ? ? ? ? ? self.contentMaxHeight = maxHeight + self.sectionInsets.bottom;
? ? ? ? ? ? }
? ? ? ? ? ? attribute.frame=CGRectMake(xOffset, yOffset, width, height);
? ? ? ? ? ? [self.itemAttributesaddObject:attribute];
? ? ? ? }
? ? }
}
主要思路:找出每行最短的一列,將下一個item置于此列下方。那怎樣找出最短的一列呢?筆者用數(shù)組itemHeights來記錄每列的高度。
首先設(shè)置初始默認(rèn)值

兩個for循環(huán)嵌套即可遍歷每個item

找出最短列的方法如上,minIndex即最短列所在的列數(shù)。此時最難點(diǎn)已經(jīng)解決,下面就是設(shè)置frame大小即可。注意設(shè)置完每個item大小,要更新itemHeights數(shù)據(jù)。筆者稍后會上傳完整代碼。
等高等間隔不等寬的排列布局
筆者主要用于類型篩選,每個文字寬度不等并且換行,先上一張效果圖:

此布局最主要的難點(diǎn)就在于何時換行,換行之后的y如何設(shè)置,下面貼出代碼:
- (void)createSameHeightItemAttributes {
? ? self.contentMaxHeight = 0;
? ? // 每行實(shí)際的寬度
? ? CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
? ? CGFloatxOffset =0;
? ? CGFloatyOffset =0;
? ? for (NSInteger i = 0; i < self.numberOfSection; i ++) {
? ? ? ? NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
? ? ? ? xOffset =self.sectionInsets.left;
? ? ? ? yOffset =self.sectionInsets.top + self.contentMaxHeight;
? ? ? ? for(NSIntegerj =0; j < numberOfItem; j ++) {
? ? ? ? ? ? NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
? ? ? ? ? ? UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
? ? ? ? ? ? CGSizesize =CGSizeZero;
? ? ? ? ? ? if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
? ? ? ? ? ? ? ? size = [self.delegatesizeForItemAtIndexPath:indexPath];
? ? ? ? ? ? }
? ? ? ? ? ? CGFloatwidth = size.width;
? ? ? ? ? ? CGFloatheight = size.height;
? ? ? ? ? ? if(xOffset + width > realWidth) {
? ? ? ? ? ? ? ? // 換行
? ? ? ? ? ? ? ? xOffset =self.sectionInsets.left;
? ? ? ? ? ? ? ? yOffset = yOffset +self.minLineSpacing+ height;
? ? ? ? ? ? ? ? attribute.frame=CGRectMake(xOffset, yOffset, width, height);
? ? ? ? ? ? ? ? xOffset = xOffset + width +self.minItemSpacing;
? ? ? ? ? ? ? ? // 更新contentSize height
? ? ? ? ? ? ? ? self.contentMaxHeight = yOffset + height + self.sectionInsets.bottom;
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? attribute.frame=CGRectMake(xOffset, yOffset, width, height);
? ? ? ? ? ? ? ? xOffset = xOffset + width +self.minItemSpacing;
? ? ? ? ? ? ? ? // 更新contentSize height
? ? ? ? ? ? ? ? self.contentMaxHeight = yOffset + height + self.sectionInsets.bottom;
? ? ? ? ? ? }
? ? ? ? ? ? [self.itemAttributesaddObject:attribute];
? ? ? ? }
? ? }
}
注意之處:判斷換行的關(guān)鍵,實(shí)際寬度?ScreenWidth - self.sectionInsets.left - self.sectionInsets.right,換行之后x,y的值要設(shè)置正確,其余無難點(diǎn)。
特殊處理-首行帶有類型名稱或者全部等
產(chǎn)品大大要這么設(shè)計,筆者只能照辦了,先來張效果圖:

其實(shí)也挺常見的,類型篩選或者展示時,時常帶有標(biāo)題或者全部字樣。只需要簡單處理下,再換行的時候空出每個section第一個item的寬度距離即可,下面上代碼:
- (void)createSpecialItemAttributes {
? ? self.contentMaxHeight = 0;
? ? CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
? ? CGFloatxOffset =0;
? ? CGFloatyOffset =0;
? ? for (NSInteger i = 0; i < self.numberOfSection; i ++) {
? ? ? ? NSInteger numberOfItem = [self.collectionView numberOfItemsInSection:i];
? ? ? ? xOffset =self.sectionInsets.left;
? ? ? ? yOffset =self.sectionInsets.top + self.contentMaxHeight;
? ? ? ? for(NSIntegerj =0; j < numberOfItem; j ++) {
? ? ? ? ? ? NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
? ? ? ? ? ? UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
? ? ? ? ? ? CGSizesize =CGSizeZero;
? ? ? ? ? ? if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
? ? ? ? ? ? ? ? size = [self.delegatesizeForItemAtIndexPath:indexPath];
? ? ? ? ? ? }
? ? ? ? ? ? if(xOffset + size.width> realWidth) {
? ? ? ? ? ? ? ? // 換行,超過一行
? ? ? ? ? ? ? ? // 取出每個secction的第一個
? ? ? ? ? ? ? ? UICollectionViewLayoutAttributes *firstAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
? ? ? ? ? ? ? ? CGRectframe = firstAttribute.frame;
? ? ? ? ? ? ? ? // x偏移,空出第一個width
? ? ? ? ? ? ? ? xOffset =CGRectGetMaxX(frame) +self.minItemSpacing;
? ? ? ? ? ? ? ? yOffset = yOffset + size.height+self.minLineSpacing;
? ? ? ? ? ? ? ? attribute.frame=CGRectMake(xOffset, yOffset, size.width, size.height);
? ? ? ? ? ? ? ? xOffset = xOffset + size.width+self.minItemSpacing;
? ? ? ? ? ? ? ? self.contentMaxHeight = CGRectGetMaxY(attribute.frame) + self.sectionInsets.bottom;
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? attribute.frame=CGRectMake(xOffset, yOffset, size.width, size.height);
? ? ? ? ? ? ? ? xOffset = xOffset + size.width+self.minItemSpacing;
? ? ? ? ? ? ? ? self.contentMaxHeight = CGRectGetMaxY(attribute.frame) + self.sectionInsets.bottom;
? ? ? ? ? ? }
? ? ? ? ? ? [self.itemAttributesaddObject:attribute];
? ? ? ? }
? ? }
}
換行之處已添加注釋,重設(shè)x,y值即可,判斷換行條件相同。
以上的方法都包含了雙層for循環(huán)嵌套,如有小伙伴不喜歡太多嵌套,將循環(huán)內(nèi)容代碼添加至- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath方法即可,原理都是一樣的,看喜歡哪種代碼書寫方式。
筆者也是小白,正好多次用到了UICollectionViewFlowLayout自定義布局,所以就寫篇文章記錄一下,供有需要的小伙伴參考,如有錯誤之處,希望各位不吝賜教哈!