iOS-Autolayout自動計算itemSize的UICollectionViewLayout瀑布流佈局

UICollectionViewLayout基礎知識

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 askedgit

官方文檔github

UICollectionViewLayout的功能爲向UICollectionView提供佈局信息,不只包括cell佈局信息,也包括追加視圖裝飾視圖的佈局信息。實現一個自定義Custom Layout的常規作法是繼承UICollectionViewLayout數組

重載的方法

  • prepareLayout:準備佈局屬性
  • layoutAttributesForElementsInRect:返回rect中的全部的元素的佈局屬性UICollectionViewLayoutAttributes能夠是cell追加視圖裝飾視圖的信息,經過不一樣的UICollectionViewLayoutAttributes初始化方法能夠獲得不一樣類型的UICollectionViewLayoutAttributes
  • layoutAttributesForCellWithIndexPath:
  • layoutAttributesForSupplementaryViewOfKind:withIndexPath:
  • layoutAttributesForDecorationViewOfKind:withIndexPath:
  • collectionViewContentSize返回contentSize

執行順序

  1. -(void)prepareLayout將被調用,默認下該方法什麼沒作,可是在本身的子類實現中,通常在該方法中設定一些必要的layout的結構和初始須要的參數等
  2. -(CGSize) collectionViewContentSize將被調用,以肯定collection應該佔據的尺寸。注意這裏的尺寸不是指可視部分的尺寸,而應該是全部內容所佔的尺寸。collectionView的本質是一個scrollView,所以須要這個尺寸來配置滾動行爲
  3. -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

引入AutoLayout自動計算的瀑布流

關於瀑布流

網上前輩們已經寫爛了,這裏只簡述:服務器

  • -(void)prepareLayout中:就是經過一個記錄列高度的數組(或字典),在建立LayoutAttributes的frame時肯定當前最短列,根據外部傳入的相關的spacingcollectionViewinset屬性,肯定寬度frame等信息,存入Attributes的數組。
  • -(CGSize) collectionViewContentSize中:經過列高度數組很容易肯定當前範圍,contentSize不等於collectionview的bounds.size,計算時留意一下
  • -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect:返回第一步中計算得到的Attributes數組便可

以上能夠幫助咱們實現一個瀑布流的效果,可是離實際應用還有一段差距。網絡

分析:
實際應用中,咱們的網絡請求是會有一個pageSize的,並且列表的賦值一般是直接進行數據源的賦值而後reloadData。因此數據源個數等於pageSize時,咱們認爲是刷新,大於時,則爲分頁加載。
根據這套邏輯,這裏將pageSizedataSource做爲屬性引入到Custom Layout中,同時維護一個記錄計算結果的數組itemSizeArray,提升計算效率,具體代碼以下:app

- (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的屬性,用於記錄自動計算的itemSize,經過這個屬性能夠幫助咱們減小沒必要要的重複計算ide

關於自動計算

注意佈局

  • Self-size要求咱們的約束自上而下設置,確保可以經過Constraint計算得到準確的高度。具體再也不贅述
  • 本Demo僅適用圖片比例肯定的瀑布流,若是需求是圖片size自適應,須要服務器返回可以計算的必要參數

自動計算的思路,相似UITableView-FDTemplateLayoutCell,經過xibNameclassName初始化一個template cell注入數據並添加橫向約束後,利用systemLayoutSizeFittingSize方法獲取系統計算的高度後,移除添加的橫向約束其中有個iOS10.2後的約束計算變化,須要咱們手動對cell.contentView添加四周的約束,AutoLayout才能準確計算高度。請注意代碼中對系統判斷的一步ui

這裏咱們爲UICollectionViewCell添加了一個Category,用於統一數據的傳入方式atom

#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

關鍵代碼以下:

  • 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);
}
  • 獲取一個計算使用的Template Cell,保存避免重複提取
- (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;
}

如何使用

  • 初始化時對全部必要屬性進行賦值
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的朋友,記得更新下機型判斷
waterflow.gif

Demo地址

SPWaterFlowLayout
筆者簡書地址

相關文章
相關標籤/搜索