利用UICollectionView實現一個隨機放大的不規則瀑布流,圖片寬高比和內容使用服務器接口返回的數據,效果以下圖:git
接口:github
https://www.easy-mock.com/mock/5cff89e36c54457798010709/shop/finderlist
複製代碼
數據:數組
{
"data": [
{
"img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561019906083&di=2bbf7db2124067fe80739cce43a2b00e&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201410%2F05%2F20141005095943_QY5e8.jpeg",
"width": "1200",
"height": "2249"
},
{
"img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561020045178&di=56eb95088ce1a23bbd16776ebcedb837&imgtype=0&src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2Fc8107b13c3bfd1fa8835f5dc80c541b64c6b9e901a8f7-RLJBJP_fw658",
"width": "658",
"height": "872"
},
...
{
"img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1561020635692&di=cd0dedd961380917af46c536e7f6600b&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201707%2F29%2F20170729215622_tTLBP.thumb.700_0.jpeg",
"width": "700",
"height": "701"
}
]
}
複製代碼
每一個圖片數據都指定了圖片的寬和高,因爲須要放大,而被放大佔用兩列寬度的圖片頂部必需要對齊,因此須要將高度差異不大的兩列高度作矯正。bash
建立一個UICollectionViewLayout的子類服務器
@protocol JKRFallsLayoutDelegate <NSObject>
@optional
/// 列數
- (CGFloat)columnCountInFallsLayout:(JKRFallsLayout *)fallsLayout;
/// 列間距
- (CGFloat)columnMarginInFallsLayout:(JKRFallsLayout *)fallsLayout;
/// 行間距
- (CGFloat)rowMarginInFallsLayout:(JKRFallsLayout *)fallsLayout;
/// collectionView邊距
- (UIEdgeInsets)edgeInsetsInFallsLayout:(JKRFallsLayout *)fallsLayout;
/// 返回圖片模型
- (JKRImageModel *)modelWithIndexPath:(NSIndexPath *)indexPath;
@end
@interface JKRFallsLayout : UICollectionViewLayout
@property (nonatomic, weak) id<JKRFallsLayoutDelegate> delegate;
@end
複製代碼
重寫如下方法:網絡
// collectionView 首次佈局和以後從新佈局的時候會調用
// 並非每次滑動都調用,只有在數據源變化的時候才調用
- (void)prepareLayout {
// 重寫必須調用super方法
[super prepareLayout];
}
// 返回佈局屬性,一個UICollectionViewLayoutAttributes對象數組
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return [super layoutAttributesForElementsInRect:rect];
}
// 計算佈局屬性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return [super layoutAttributesForItemAtIndexPath:indexPath];
}
// 返回collectionView的ContentSize
- (CGSize)collectionViewContentSize {
return [super collectionViewContentSize];
}
複製代碼
要實現佈局的計算,須要建立如下幾個屬性dom
@property (nonatomic, strong) NSMutableArray<UICollectionViewLayoutAttributes *> *attrsArray; ///< 全部的cell的佈局
@property (nonatomic, strong) NSMutableArray *columnHeights; ///< 每一列的高度
@property (nonatomic, assign) NSInteger noneDoubleTime; ///< 沒有生成大尺寸次數
@property (nonatomic, assign) NSInteger lastDoubleIndex; ///< 最後一次大尺寸的列數
- (CGFloat)columnCount; ///< 列數
- (CGFloat)columnMargin; ///< 列邊距
- (CGFloat)rowMargin; ///< 行邊距
- (UIEdgeInsets)edgeInsets; ///< collectionView邊距
複製代碼
在- (void)prepareLayout方法中遍歷須要計算的cell,調用計算佈局的方法,並將獲取的佈局屬性保存到attrsArray數組中:佈局
- (void)prepareLayout {
// 重寫必須調用super方法
[super prepareLayout];
if ([self.collectionView numberOfItemsInSection:0] == PageCount && self.attrsArray.count > PageCount) {
[self.attrsArray removeAllObjects];
[self.columnHeights removeAllObjects];
}
// 當列高度數組爲空時,即爲第一行計算,每一列的基礎高度加上collection的邊框的top值
if (!self.columnHeights.count) {
for (NSInteger i = 0; i < self.columnCount; i++) {
[self.columnHeights addObject:@(self.edgeInsets.top)];
}
}
// 遍歷全部的cell,計算全部cell的佈局
for (NSInteger i = self.attrsArray.count; i < [self.collectionView numberOfItemsInSection:0]; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
// 計算佈局屬性並將結果添加到佈局屬性數組中
[self.attrsArray addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
// 返回佈局屬性,一個UICollectionViewLayoutAttributes對象數組
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.attrsArray;
}
// 返回collectionView的ContentSize
- (CGSize)collectionViewContentSize {
// collectionView的contentSize的高度等於全部列高度中最大的值
CGFloat maxColumnHeight = [self.columnHeights[0] doubleValue];
for (NSInteger i = 1; i < self.columnCount; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (maxColumnHeight < columnHeight) {
maxColumnHeight = columnHeight;
}
}
return CGSizeMake(0, maxColumnHeight + self.edgeInsets.bottom);
}
複製代碼
columnHeights保存保存每一列的總高度,cell的寬度和高度,經過隨機數+不重複放大+矯正的原則來計算,下面有代碼詳細註釋:ui
// 計算佈局屬性
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
// cell的寬度
CGFloat w = (self.collectionView.frame.size.width - self.edgeInsets.left - self.edgeInsets.right - self.columnMargin * (self.columnCount - 1)) / self.columnCount;
// cell的高度
JKRImageModel*shop = [self.delegate modelWithIndexPath:indexPath];
CGFloat h = shop.height / shop.width * w;
// cell應該拼接的列數
NSInteger destColumn = 0;
// 高度最小的列數高度
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
// 獲取高度最小的列數
for (NSInteger i = 1; i < self.columnCount; i++) {
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = i;
}
}
// 計算cell的x
CGFloat x = self.edgeInsets.left + destColumn * (w + self.columnMargin);
// 計算cell的y
CGFloat y = minColumnHeight;
if (y != self.edgeInsets.top) {
y += self.rowMargin;
}
// 判斷是否放大
if (destColumn < self.columnCount - 1 // 放大的列數不能是最後一列(最後一列方法超出屏幕)
&& _noneDoubleTime >= 1 // 若是前個cell有放大就不放大,防止連續出現兩個放大
&& arc4random() % 100 > 33 // 33%概率不放大
&& [self.columnHeights[destColumn] doubleValue] == [self.columnHeights[destColumn + 1] doubleValue] // 當前列的頂部和下一列的頂部要對齊
&& (_lastDoubleIndex != destColumn) // 最後一次放大的列不等當前列,防止出現連續兩列出現放大不美觀
) {
_noneDoubleTime = 0;
_lastDoubleIndex = destColumn;
// 重定義當前cell的佈局:寬度*2,高度*2
attrs.frame = CGRectMake(x, y, w * 2 + self.columnMargin, h * 2 + self.rowMargin);
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
self.columnHeights[destColumn + 1] = @(CGRectGetMaxY(attrs.frame));
} else {
// 正常cell的佈局
if (self.columnHeights.count > destColumn + 1 && ABS(y + h - [self.columnHeights[destColumn + 1] doubleValue]) < h * 0.2) {
// 當前cell填充後和上一列的高度誤差不超過cell最大高度的10%,就和下一列對齊
attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn + 1] doubleValue] - y);
} else if (destColumn >= 1 && ABS(y + h - [self.columnHeights[destColumn - 1] doubleValue]) < h * 0.2) {
// 當前cell填充後和上上列的高度誤差不超過cell最大高度的10%,就和下一列對齊
attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn - 1] doubleValue] - y);
} else {
attrs.frame = CGRectMake(x, y, w, h);
}
// 當前cell列的高度就是當前cell的最大Y值
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
_noneDoubleTime += 1;
}
// 返回計算獲取的佈局
return attrs;
}
複製代碼
點擊查看源碼。atom