iOS-Autolayout自動(dòng)計(jì)算itemSize的UICollectionViewLayout瀑布流布局

UICollectionViewLayout基礎(chǔ)知識(shí)

Custom Layout

官方描述
An abstract base class for generating layout information for a collection view
The job of a layout object is to determine the placement of cells, supplementary views, and decoration views inside the collection view’s bounds and to report that information to the collection view when asked

官方文檔

UICollectionViewLayout的功能為向UICollectionView提供布局信息酷鸦,不僅包括cell布局信息,也包括追加視圖裝飾視圖的布局信息勇哗。實(shí)現(xiàn)一個(gè)自定義Custom Layout的常規(guī)做法是繼承UICollectionViewLayout

重載的方法

  • prepareLayout:準(zhǔn)備布局屬性
  • layoutAttributesForElementsInRect:返回rect中的所有的元素的布局屬性UICollectionViewLayoutAttributes可以是cell陆淀,追加視圖裝飾視圖的信息拼苍,通過不同的UICollectionViewLayoutAttributes初始化方法可以得到不同類型的UICollectionViewLayoutAttributes
  • layoutAttributesForCellWithIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  • layoutAttributesForDecorationViewOfKind:withIndexPath:
  • collectionViewContentSize返回contentSize

執(zhí)行順序

  1. -(void)prepareLayout將被調(diào)用纵顾,默認(rèn)下該方法什么沒做平委,但是在自己的子類實(shí)現(xiàn)中匹颤,一般在該方法中設(shè)定一些必要的layout的結(jié)構(gòu)和初始需要的參數(shù)等
  2. -(CGSize) collectionViewContentSize將被調(diào)用厚满,以確定collection應(yīng)該占據(jù)的尺寸府瞄。注意這里的尺寸不是指可視部分的尺寸,而應(yīng)該是所有內(nèi)容所占的尺寸碘箍。collectionView的本質(zhì)是一個(gè)scrollView摘能,因此需要這個(gè)尺寸來配置滾動(dòng)行為
  3. -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

引入AutoLayout自動(dòng)計(jì)算的瀑布流

關(guān)于瀑布流

網(wǎng)上前輩們已經(jīng)寫爛了续崖,這里只簡(jiǎn)述:

  • -(void)prepareLayout中:就是通過一個(gè)記錄列高度的數(shù)組(或字典),在創(chuàng)建LayoutAttributes的frame時(shí)確定當(dāng)前最短列团搞,根據(jù)外部傳入的相關(guān)的spacingcollectionViewinset屬性严望,確定寬度frame等信息逻恐,存入Attributes的數(shù)組像吻。
  • -(CGSize) collectionViewContentSize中:通過列高度數(shù)組很容易確定當(dāng)前范圍,contentSize不等于collectionview的bounds.size,計(jì)算時(shí)留意一下
  • -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect:返回第一步中計(jì)算獲得的Attributes數(shù)組即可

以上可以幫助我們實(shí)現(xiàn)一個(gè)瀑布流的效果复隆,但是離實(shí)際應(yīng)用還有一段差距拨匆。

分析:
實(shí)際應(yīng)用中,我們的網(wǎng)絡(luò)請(qǐng)求是會(huì)有一個(gè)pageSize的挽拂,而且列表的賦值通常是直接進(jìn)行數(shù)據(jù)源的賦值然后reloadData惭每。所以數(shù)據(jù)源個(gè)數(shù)等于pageSize時(shí),我們認(rèn)為是刷新亏栈,大于時(shí)台腥,則為分頁加載。
根據(jù)這套邏輯绒北,這里將pageSizedataSource作為屬性引入到Custom Layout中黎侈,同時(shí)維護(hù)一個(gè)記錄計(jì)算結(jié)果的數(shù)組itemSizeArray,提高計(jì)算效率闷游,具體代碼如下:

- (void)calculateAttributesWithItemWidth:(CGFloat)itemWidth{
    BOOL isRefresh = self.datas.count <= self.pageSize;
    if (isRefresh) {
        [self refreshLayoutCache];
    }
    NSInteger cacheCount = self.itemSizeArray.count;
    for (NSInteger i = cacheCount; i < self.datas.count; i ++) {
        CGSize itemSize = [self calculateItemSizeWithIndex:i];
        UICollectionViewLayoutAttributes *layoutAttributes = [self createLayoutAttributesWithItemSize:itemSize index:i];
        [self.itemSizeArray addObject:[NSValue valueWithCGSize:itemSize]];
        [self.layoutAttributesArray addObject:layoutAttributes];
    }
}
- (UICollectionViewLayoutAttributes *)createLayoutAttributesWithItemSize:(CGSize)itemSize index:(NSInteger)index{
    UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
    struct SPColumnInfo shortestInfo = [self shortestColumn:self.columnHeightArray];
    // x
    CGFloat itemX = (self.itemWidth + self.interitemSpacing) * shortestInfo.columnNumber;
    // y
    CGFloat itemY = self.columnHeightArray[shortestInfo.columnNumber].floatValue + self.lineSpacing;
    // size
    layoutAttributes.frame = (CGRect){CGPointMake(itemX, itemY),itemSize};
    self.columnHeightArray[shortestInfo.columnNumber] = @(CGRectGetMaxY(layoutAttributes.frame));
    return layoutAttributes;
}
- (void)refreshLayoutCache{
    [self.layoutAttributesArray removeAllObjects];
    [self.columnHeightArray removeAllObjects];
    [self.itemSizeArray removeAllObjects];
    for (NSInteger index = 0; index < self.columnNumber; index ++) {
        [self.columnHeightArray addObject:@(self.viewInset.top)];
    }
}

代碼里可以看到峻汉,itemSizeArray的屬性,用于記錄自動(dòng)計(jì)算的itemSize脐往,通過這個(gè)屬性可以幫助我們減少不必要的重復(fù)計(jì)算

關(guān)于自動(dòng)計(jì)算

注意

  • Self-size要求我們的約束自上而下設(shè)置休吠,確保能夠通過Constraint計(jì)算獲得準(zhǔn)確的高度。具體不再贅述
  • 本Demo僅適用圖片比例確定的瀑布流业簿,如果需求是圖片size自適應(yīng)瘤礁,需要服務(wù)器返回能夠計(jì)算的必要參數(shù)

自動(dòng)計(jì)算的思路,類似UITableView-FDTemplateLayoutCell辖源,通過xibNameclassName初始化一個(gè)template cell注入數(shù)據(jù)并添加橫向約束后希太,利用systemLayoutSizeFittingSize方法獲取系統(tǒng)計(jì)算的高度后克饶,移除添加的橫向約束其中有個(gè)iOS10.2后的約束計(jì)算變化,需要我們手動(dòng)對(duì)cell.contentView添加四周的約束誊辉,AutoLayout才能準(zhǔn)確計(jì)算高度矾湃。請(qǐng)注意代碼中對(duì)系統(tǒng)判斷的一步

這里我們?yōu)?code>UICollectionViewCell添加了一個(gè)Category,用于統(tǒng)一數(shù)據(jù)的傳入方式

#import <UIKit/UIKit.h>

@interface UICollectionViewCell (FeedData)
@property (nonatomic, strong) id feedData;
@property (nonatomic, strong) id subfeedData;
@end

// --------------------------------------

#import "UICollectionViewCell+FeedData.h"
#import <objc/runtime.h>

static NSString *AssociateKeyFeedData = @"AssociateKeyFeedData";
static NSString *AssociateKeySubFeedData = @"AssociateKeySubFeedData";
@implementation UICollectionViewCell (FeedData)
@dynamic feedData;
@dynamic subfeedData;

- (void)setFeedData:(id)feedData{
    objc_setAssociatedObject(self, &AssociateKeyFeedData, feedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)feedData{
    return objc_getAssociatedObject(self, &AssociateKeyFeedData);
}

- (void)setSubfeedData:(id)subfeedData{
    objc_setAssociatedObject(self, &AssociateKeySubFeedData, subfeedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)subfeedData{
    return objc_getAssociatedObject(self, &AssociateKeySubFeedData);
}

@end

關(guān)鍵代碼如下:

  • itemSize
- (CGSize)calculateItemSizeWithIndex:(NSInteger)index{
    NSAssert(index < self.datas.count, @"index is incorrect");
    UICollectionViewCell *tempCell = [self templateCellWithReuseIdentifier:self.reuseIdentifier withIndex:index];
    tempCell.feedData = self.datas[index];
    CGFloat cellHeight = [self systemCalculateHeightForTemplateCell:tempCell];
    return CGSizeMake(self.itemWidth, cellHeight);
}
  • 獲取一個(gè)計(jì)算使用的Template Cell堕澄,保存避免重復(fù)提取
- (UICollectionViewCell *)templateCellwithIndex:(NSInteger)index{
    if (!self.templateCell) {
        if (self.className) {
            Class cellClass = NSClassFromString(self.className);
            UICollectionViewCell *templateCell = [[cellClass alloc] init];
            self.templateCell = templateCell;
        }else if (self.xibName){
            UICollectionViewCell *templateCell = [[NSBundle mainBundle] loadNibNamed:self.xibName owner:nil options:nil].lastObject;
            self.templateCell = templateCell;
        }
    }
    return self.templateCell;
}
  • AutoLayout Self-sizing
- (CGFloat)systemCalculateHeightForTemplateCell:(UICollectionViewCell *)cell{
    CGFloat calculateHeight = 0;
    
    NSLayoutConstraint *widthForceConstant = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:self.itemWidth];

    static BOOL isSystemVersionEqualOrGreaterThen10_2 = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        isSystemVersionEqualOrGreaterThen10_2 = [UIDevice.currentDevice.systemVersion compare:@"10.2" options:NSNumericSearch] != NSOrderedAscending;
    });
    
    NSArray<NSLayoutConstraint *> *edgeConstraints;
    if (isSystemVersionEqualOrGreaterThen10_2) {
        // To avoid conflicts, make width constraint softer than required (1000)
        widthForceConstant.priority = UILayoutPriorityRequired - 1;

        // Build edge constraints
        NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
        NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1.0 constant:0];
        NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
        NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
        edgeConstraints = @[leftConstraint, rightConstraint, topConstraint, bottomConstraint];
        [cell addConstraints:edgeConstraints];
    }
    
    // system calculate
    [cell.contentView addConstraint:widthForceConstant];
    calculateHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    // clear constraint
    [cell.contentView removeConstraint:widthForceConstant];
    if (isSystemVersionEqualOrGreaterThen10_2) {
        [cell removeConstraints:edgeConstraints];
    }
    return calculateHeight;
}

如何使用

  • 初始化時(shí)對(duì)所有必要屬性進(jìn)行賦值
    SPWaterFlowLayout *flowlayout = [[SPWaterFlowLayout alloc] init];
    flowlayout.columnNumber = 2;
    flowlayout.interitemSpacing = 10;
    flowlayout.lineSpacing = 10;
    flowlayout.pageSize = 54;
    flowlayout.xibName = @"TestView";
    UICollectionView *test = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:flowlayout];
    test.contentInset = UIEdgeInsetsMake(10, 10, 5, 10);
    [self.view addSubview:test];
    test.delegate = self;
    test.dataSource = self;
    [test registerNib:[UINib nibWithNibName:@"TestView" bundle:nil] forCellWithReuseIdentifier:@"Cell"];
    test.backgroundColor = [UIColor whiteColor];
  • Refresh及LoadMore中更新dataSource

Refresh

test.refreshDataCallBack = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            self.pageTag = 0;
            NSArray *datas = [SPProductModel productWithIndex:0];
            flowlayout.datas = datas;
            wtest.sp_datas = [datas mutableCopy];
            [wtest doneLoadDatas];
            [wtest reloadData];
        });
    };

LoadMore

 test.loadMoreDataCallBack = ^{
        self.pageTag ++;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSArray *datas = [SPProductModel productWithIndex:self.pageTag];
            NSArray *total = [flowlayout.datas arrayByAddingObjectsFromArray:datas];
            flowlayout.datas = total;
            wtest.sp_datas = [total mutableCopy];
            [wtest doneLoadDatas];
            [wtest reloadData];
        });
    };

效果

題外話:iPhone X讓我們除了64邀跃,又記住了88和812霉咨,自己寫Refresh的朋友,記得看一下contentInset 在iOS11中拍屑,如果不關(guān)autoAdjust情況下 有什么變化

waterflow.gif

Demo地址

GitHub:SPWaterFlowLayout
筆者博客地址:Tr2e's Blog


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末途戒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子僵驰,更是在濱河造成了極大的恐慌喷斋,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒜茴,死亡現(xiàn)場(chǎng)離奇詭異星爪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)粉私,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門顽腾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诺核,你說我怎么就攤上這事抄肖。” “怎么了猪瞬?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵憎瘸,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我陈瘦,道長(zhǎng)幌甘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任痊项,我火速辦了婚禮锅风,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鞍泉。我一直安慰自己皱埠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布咖驮。 她就那樣靜靜地躺著边器,像睡著了一般。 火紅的嫁衣襯著肌膚如雪托修。 梳的紋絲不亂的頭發(fā)上忘巧,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音睦刃,去河邊找鬼砚嘴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的际长。 我是一名探鬼主播耸采,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼工育!你這毒婦竟也來了虾宇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤翅娶,失蹤者是張志新(化名)和其女友劉穎文留,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竭沫,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡燥翅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜕提。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片森书。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖谎势,靈堂內(nèi)的尸體忽然破棺而出凛膏,到底是詐尸還是另有隱情,我是刑警寧澤脏榆,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布猖毫,位于F島的核電站,受9級(jí)特大地震影響须喂,放射性物質(zhì)發(fā)生泄漏吁断。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一坞生、第九天 我趴在偏房一處隱蔽的房頂上張望仔役。 院中可真熱鬧,春花似錦是己、人聲如沸又兵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沛厨。三九已至,卻和暖如春摔认,著一層夾襖步出監(jiān)牢的瞬間逆皮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工级野, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留页屠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓蓖柔,卻偏偏與公主長(zhǎng)得像辰企,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子况鸣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

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