(本文主要講一下現在比較流行的一種布局方式----瀑布流布局 如有寫的不好的地方 還請多多指正 感謝)
1、功能分析
如圖所示: 我們可以看到該布局中的每個元素有一個共同的特點就是等寬不等高. 而且當一行排列完成之后在下一行進行排列時都是從最短的那一列開始排,否則的話就會讓每一列的差距越來越大從而顯得非常不美觀.
2、實現思路
- 根據需求 該頁面需要有滾動效果 而且可以展示很多數據 所以決定用UICollectionView來完成 那么UICollectionView中具體的每一個cell如何排列就是我們需要解決的問題了.也就是說我們需要計算出每一個cell的frame.
- 接下來就對cell的x值,y值,寬度,高度進行逐一計算
- 寬度w
我們可以很直觀的從圖中看出寬度w=(collectionView的寬度 - cell左邊的邊距 - cell右邊的邊距 - (總共的列數 - 1) * 每一列之間的間距) / 總共的列數 - 高度h
高度h是根據具體項目中的模型本身的高度來決定 - x,y值
根據上圖可以發現每一列中所有cell的x值是一樣的 所以要算x值只需要求出列號就行了. y值就是最短的那一列的cell最大y值再加上間距
綜上所述,現在需要做的首要任務就是找出最短的那一列.所以我們需要通過遍歷每一列的高度來找出最短的那一列.
3、code
以上進行簡單分析之后就要開始動手了 既然是自定義布局 我們就需要創建一個繼承自UICollectionViewLayout
的類來實現布局 在這個類中我們需要實現以下幾個方法
-
- (void)prepareLayout
這個方法是用來進行初始化的 實現代碼如下
-(void)prepareLayout
{
[super prepareLayout];
//清除之前計算的所有高度
[self.colunmHeights removeAllObjects];
for (NSInteger i = 0; i < ZDDefaultColumnCount; i++) {
[self.colunmHeights addObject:@(ZDDefaultEdgeInsets.top)];
}
//清除之前所有的布局
[self.attrsArray removeAllObjects];
//創建每一個cell對應的布局屬性
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (NSInteger i = 0; i < count; i++) {
//創建位置
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
//獲取indexPath位置cell對應的布局屬性
UICollectionViewLayoutAttributes *attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArray addObject:attrs];
}
}
其中self.colunmHeights
和self.attrsArray
是自己定義的兩個屬性 分別用來保存所有列的當前高度以及所有cell的布局屬性(兩個都是可變數組類型)
-
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
這個方法是用來決定cell的排布 實現代碼如下
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArray;
}
-
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
這個方法是用來返回indexPath的位置所對應的cell的布局屬性的 實現代碼如下
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//創建布局屬性
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//collectionView的寬度
CGFloat collectionViewW = self.collectionView.frame.size.width;
//設置布局屬性的frame
CGFloat w = (collectionViewW - ZDDefaultEdgeInsets.left - ZDDefaultEdgeInsets.right - (ZDDefaultColumnCount - 1) * ZDDefaultColumnMargin) / ZDDefaultColumnCount;
CGFloat h = 50 + arc4random_uniform(100);
//找出高度最短的那一列
NSInteger destColumn = 0;
CGFloat minColumnHeight = [self.colunmHeights[0] doubleValue];
for (NSInteger i = 1; i < ZDDefaultColumnCount; i++) {
CGFloat columnHeight = [self.colunmHeights[i] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = i;
}
}
CGFloat x = ZDDefaultEdgeInsets.left + destColumn * (w + ZDDefaultColumnMargin);
CGFloat y = minColumnHeight;
if (y != ZDDefaultEdgeInsets.top) {
y += ZDDefaultRowMargin;
}
attrs.frame = CGRectMake(x, y, w, h);
//更新最短那列的高度
self.colunmHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
// 記錄內容的高度
CGFloat columnHeight = [self.columnHeights[destColumn] doubleValue];
if (self.contentHeight < columnHeight) {
self.contentHeight = columnHeight;
}
return attrs;
}
其中self.contentHeight
是自定義的一個屬性 用來保存內容的高度
-
- (CGSize)collectionViewContentSize
這個方法是為了讓collectionView可以滾動起來 實現代碼如下
-(CGSize)collectionViewContentSize
{
return CGSizeMake(0, self.contentHeight + ZDDefaultEdgeInsets.bottom);
}
4、優化
- 考慮到代碼的復用性 方便直接將代碼拖到下個項目中去
(比如具體要展示多少列,每個cell之間的間距等等都需要根據具體的項目需求來決定) 所以對代碼進行優化 - 優化思路是根據
UITableViewDelegate
tableView具體展示什么數據,展示多少數據都是由其數據源和代理方法來具體實現的,所以我也設計了一個代理屬性 具體顯示多少列 每個cell之間的間距都是有代理方法來實現的 具體實現代碼如下: - 在
ZDWaterfallLayout.h
文件中:
@class ZDWaterfallLayout;
@protocol ZDWaterfallLayoutDelegate <NSObject>
@required
-(CGFloat)waterfallLayout:(ZDWaterfallLayout *)waterfallLayout heightForItemAtIndex:(NSInteger)index itemWidth:(CGFloat)itemWidth;
@optional
//列數
-(CGFloat)columnCountInWaterfallLayout:(ZDWaterfallLayout *)waterfallLayout;
//每一列之間的間距
-(CGFloat)columnMarginInWaterfallLayout:(ZDWaterfallLayout *)waterfallLayout;
//每一行之間的間距
-(CGFloat)rowMarginInWaterfallLayout:(ZDWaterfallLayout *)waterfallLayout;
//cell的邊距
-(UIEdgeInsets)edgeInsetsInWaterfallLayout:(ZDWaterfallLayout *)waterfallLayout;
@end
@interface ZDWaterfallLayout : UICollectionViewLayout
/**布局代理屬性*/
@property (nonatomic,weak) id<ZDWaterfallLayoutDelegate>delegate ;
@end
- 在
ZDWaterfallLayout.m
文件中
-(CGFloat)rowMargin
{
if ([self.delegate respondsToSelector:@selector(rowMarginInWaterfallLayout:)]) {
return [self.delegate rowMarginInWaterfallLayout:self];
}else{
return ZDDefaultRowMargin;
}
}
-(CGFloat)columnMargin
{
if ([self.delegate respondsToSelector:@selector(columnMarginInWaterfallLayout:)]) {
return [self.delegate columnMarginInWaterfallLayout:self];
}else{
return ZDDefaultColumnMargin;
}
}
-(NSInteger)columnCount
{
if ([self.delegate respondsToSelector:@selector(columnCountInWaterfallLayout:)]) {
return [self.delegate columnCountInWaterfallLayout:self];
}else{
return ZDDefaultColumnCount;
}
}
-(UIEdgeInsets)edgeInsets
{
if ([self.delegate respondsToSelector:@selector(edgeInsetsInWaterfallLayout:)]) {
return [self.delegate edgeInsetsInWaterfallLayout:self];
}else{
return ZDDefaultEdgeInsets;
}
}
ZDDefaultRowMargin ZDDefaultColumnMargin ZDDefaultColumnCount ZDDefaultEdgeInsets
這是我自定義的默認值
接下來想要改變布局效果就很簡單了 只需要通過具體實現幾個代理方法就可以搞定 比如我想排5列 只需要實現如下方法即可:
-(CGFloat)columnCountInWaterfallLayout:(ZDWaterfallLayout *)waterfallLayout
{
return 5;
}
顯示效果如下:
就是這么輕松愉快~
具體demo請看我的github