iOS 自定義UICollectionViewFlowLayout又跛,實(shí)現(xiàn)瀑布流布局

本人菜鳥(niǎo)小白慨蓝,最近研究了下UICollectionView自定義布局實(shí)現(xiàn)瀑布流等布局静暂,主要是應(yīng)對(duì)公司需求摹迷,產(chǎn)品這么設(shè)計(jì)我也很無(wú)奈啊,初次寫(xiě)文章吉执,如有不對(duì)之處,歡迎大家提出,謝謝府阀。

github地址

豎向等寬等間隔瀑布流

先上一張效果圖
瀑布流

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

@interface CandyFlowLayout : UICollectionViewFlowLayout

/** default 0 */
@property (nonatomic, assign) UIEdgeInsets sectionInsets;
/** default 0  左右*/
@property (nonatomic, assign) CGFloat minItemSpacing;
/** default 0  上下*/
@property (nonatomic, assign) CGFloat minLineSpacing;

@property (nonatomic, assign) CandyFlowLayoutStyle style;

@property (nonatomic, weak) id<CandyFlowLayoutDelegate> delegate;

/** 瀑布流每行item總數(shù)鲸拥,寬度等分 */
@property (nonatomic, assign) NSInteger waterfallRowNumber;

- (instancetype)initSectionInsets:(UIEdgeInsets)sectionInsets minItemSpacing:(CGFloat)minItemSpacing minLineSpacing:(CGFloat)minLineSpacing;

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

@protocol CandyFlowLayoutDelegate <NSObject>

@optional

/** 返回item size */
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

/** 返回item height 瀑布流時(shí)使用 */
- (CGFloat)heightForItemAtIndexPath:(NSIndexPath *)indexPath;

@end

.m文件主要實(shí)現(xiàn)幾個(gè)方法就能自定義布局

  • (void)prepareLayout // 一定要實(shí)現(xiàn)此方法,筆者將布局信息全部在此重寫(xiě)铁材,當(dāng)然也可以寫(xiě)到每個(gè)item的布局方法中,也就是- (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath方法中奕锌,效果等同著觉。

  • (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect // 返回存放所有item的布局信息數(shù)組

  • (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath // 返回單個(gè)item的布局信息

  • (CGSize)collectionViewContentSize // 返回正確的contentSize,這樣就可以在外部得到contentSize惊暴,筆者主要是應(yīng)對(duì)collectionView無(wú)滑動(dòng)效果設(shè)置正確的height=contentsize.height拧篮。此方法可以不用重寫(xiě)羡洁。
    接下來(lái)看下豎向等寬等間隔瀑布流布局代碼:

- (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)];
    }
    
    // 計(jì)算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 (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            //找出每行最短的一列
            NSInteger minIndex = 0;
            CGFloat minY = [self.itemHeights[0] floatValue];
            for (NSInteger n = 1; n < self.waterfallRowNumber; n ++) {
                // 依次取出高度
                CGFloat itemY = [self.itemHeights[n] floatValue];
                if (minY > itemY) {
                    minY = itemY;
                    minIndex = n;
                }
            }
            
            CGFloat xOffset = self.sectionInsets.left + minIndex * (width + self.minItemSpacing);
            CGFloat height = 0;
            if (self.delegate && [self.delegate respondsToSelector:@selector(heightForItemAtIndexPath:)]) {
                height = [self.delegate heightForItemAtIndexPath:indexPath];
            }
            CGFloat yOffset = minY;
            if (yOffset != self.sectionInsets.top) {
                // 不是第一行朴则,要加間隔
                yOffset += self.minLineSpacing;
            }
            
            // 更新高度
            self.itemHeights[minIndex] = @(height + yOffset);
            // 更新contentSize height
            CGFloat maxHeight = [self.itemHeights[minIndex] floatValue];
            if (self.contentMaxHeight < maxHeight) {
                // 最短的一列 + 高度 > 之前的最高高度
                self.contentMaxHeight = maxHeight + self.sectionInsets.bottom;
            }
            attribute.frame = CGRectMake(xOffset, yOffset, width, height);
            [self.itemAttributes addObject:attribute];
        }
    }
}

主要思路:找出每行最短的一列帆喇,將下一個(gè)item置于此列下方。那怎樣找出最短的一列呢油啤?筆者用數(shù)組itemHeights來(lái)記錄每列的高度典徘。
1.首先設(shè)置初始默認(rèn)值

for (NSInteger i = 0; i < self.waterfallRowNumber; i ++) {
        // 默認(rèn)都是top
        [self.itemHeights addObject:@(self.sectionInsets.top)];
    }

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

//找出每行最短的一列
NSInteger minIndex = 0;
CGFloat minY = [self.itemHeights[0] floatValue];
for (NSInteger n = 1; n < self.waterfallRowNumber; n ++) {
    // 依次取出高度
    CGFloat itemY = [self.itemHeights[n] floatValue];
    if (minY > itemY) {
        minY = itemY;
        minIndex = n;
    }
 }

找出最短列的方法如上,minIndex即最短列所在的列數(shù)益咬。此時(shí)最難點(diǎn)已經(jīng)解決逮诲,下面就是設(shè)置frame大小即可。注意設(shè)置完每個(gè)item大小幽告,要更新itemHeights數(shù)據(jù)梅鹦。筆者稍后會(huì)上傳完整代碼。

等高等間隔不等寬的排列布局

筆者主要用于類(lèi)型篩選冗锁,每個(gè)文字寬度不等并且換行齐唆,先上一張效果圖:
等高等間隔不等寬

此布局最主要的難點(diǎn)就在于何時(shí)換行,換行之后的y如何設(shè)置冻河,下面貼出代碼:

- (void)createSameHeightItemAttributes {
    self.contentMaxHeight = 0;
    // 每行實(shí)際的寬度
    CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
    CGFloat xOffset = 0;
    CGFloat yOffset = 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 (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            
            CGSize size = CGSizeZero;
            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
                size = [self.delegate sizeForItemAtIndexPath:indexPath];
            }
            CGFloat width = size.width;
            CGFloat height = 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.itemAttributes addObject:attribute];
        }
    }
}

注意之處:判斷換行的關(guān)鍵蝶念,實(shí)際寬度 ScreenWidth - self.sectionInsets.left - self.sectionInsets.right,換行之后x芋绸,y的值要設(shè)置正確,其余無(wú)難點(diǎn)担敌。

特殊處理-首行帶有類(lèi)型名稱(chēng)或者全部等

產(chǎn)品大大要這么設(shè)計(jì)摔敛,筆者只能照辦了,先來(lái)張效果圖:

其實(shí)也挺常見(jiàn)的全封,類(lèi)型篩選或者展示時(shí)马昙,時(shí)常帶有標(biāo)題或者全部字樣桃犬。只需要簡(jiǎn)單處理下,再換行的時(shí)候空出每個(gè)section第一個(gè)item的寬度距離即可行楞,下面上代碼:

- (void)createSpecialItemAttributes {
    self.contentMaxHeight = 0;
    CGFloat realWidth = ScreenWidth - self.sectionInsets.left - self.sectionInsets.right;
    CGFloat xOffset = 0;
    CGFloat yOffset = 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 (NSInteger j = 0; j < numberOfItem; j ++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
            CGSize size = CGSizeZero;
            if (self.delegate && [self.delegate respondsToSelector:@selector(sizeForItemAtIndexPath:)]) {
                size = [self.delegate sizeForItemAtIndexPath:indexPath];
            }
            
            if (xOffset + size.width > realWidth) {
                // 換行攒暇,超過(guò)一行
                // 取出每個(gè)secction的第一個(gè)
                UICollectionViewLayoutAttributes *firstAttribute = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
                CGRect frame = firstAttribute.frame;
                // x偏移,空出第一個(gè)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.itemAttributes addObject:attribute];
        }
    }
}

換行之處已添加注釋?zhuān)卦O(shè)x子房,y值即可形用,判斷換行條件相同。
以上的方法都包含了雙層for循環(huán)嵌套证杭,如有小伙伴不喜歡太多嵌套田度,將循環(huán)內(nèi)容代碼添加至- (UICollectionViewLayoutAttributes)layoutAttributesForItemAtIndexPath:(NSIndexPath)indexPath方法即可,原理都是一樣的解愤,看喜歡哪種代碼書(shū)寫(xiě)方式镇饺。

筆者也是小白,正好多次用到了UICollectionViewFlowLayout自定義布局送讲,所以就寫(xiě)篇文章記錄一下奸笤,供有需要的小伙伴參考,如有錯(cuò)誤之處哼鬓,希望各位不吝賜教哈监右!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市魄宏,隨后出現(xiàn)的幾起案子秸侣,更是在濱河造成了極大的恐慌,老刑警劉巖宠互,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件味榛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡予跌,警方通過(guò)查閱死者的電腦和手機(jī)搏色,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)券册,“玉大人频轿,你說(shuō)我怎么就攤上這事∷副海” “怎么了航邢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)骄蝇。 經(jīng)常有香客問(wèn)我膳殷,道長(zhǎng),這世上最難降的妖魔是什么九火? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任赚窃,我火速辦了婚禮册招,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勒极。我一直安慰自己是掰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布辱匿。 她就那樣靜靜地躺著键痛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掀鹅。 梳的紋絲不亂的頭發(fā)上散休,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音乐尊,去河邊找鬼戚丸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛扔嵌,可吹牛的內(nèi)容都是我干的限府。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼痢缎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胁勺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起独旷,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤署穗,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后嵌洼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體案疲,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年麻养,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了褐啡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鳖昌,死狀恐怖备畦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情许昨,我是刑警寧澤懂盐,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站糕档,受9級(jí)特大地震影響允粤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一类垫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧琅坡,春花似錦悉患、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至茴晋,卻和暖如春陪捷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诺擅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工市袖, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烁涌。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓苍碟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親撮执。 傳聞我的和親對(duì)象是個(gè)殘疾皇子微峰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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