iOS GZWaterfall任何形式的瀑布流
概述
使用UICollectionView可以布局各种各样的瀑布流,下面我写了几种不同布局的瀑布流样式
详细
首先,将所有的类型展示给大家;
上面图片中展示的样式在Demo中都有实现。
一、项目结构
对于我们要实现的各种各样的 collectionView,根据不同的需求设置不同的列数 ,列边距,行边距,collectionView边距
二、程序实现
1、随机瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | #pragma mark - 创建collectionView - ( void )setupCollectionView { GZFallsLayout *fallsLayout = [[GZFallsLayout alloc] init]; fallsLayout.delegate = self; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:fallsLayout]; [self.view addSubview:collectionView]; _collectionView = collectionView; collectionView.dataSource = self; [collectionView registerNib:[UINib nibWithNibName:NSStringFromClass([GZShopCell class ]) bundle:nil] forCellWithReuseIdentifier:ID]; } #pragma mark - 创建上下拉刷新 - ( void )setupRefresh { self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewShops)]; self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreShops)]; self.collectionView.backgroundColor = [UIColor whiteColor]; [self.collectionView.mj_header beginRefreshing]; } #pragma mark - 加载下拉数据 - ( void )loadNewShops { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSArray *shops = [GZShop mj_objectArrayWithFilename:@ "1.plist" ]; [weakSelf.shops removeAllObjects]; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.collectionView reloadData]; [weakSelf.shops addObjectsFromArray:shops]; [weakSelf.collectionView.mj_header endRefreshing]; [weakSelf.collectionView reloadData]; }); }); } #pragma mark - 加载上拉数据 - ( void )loadMoreShops { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSArray *shops = [GZShop mj_objectArrayWithFilename:@ "1.plist" ]; [weakSelf.shops addObjectsFromArray:shops]; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.collectionView.mj_footer endRefreshing]; [weakSelf.collectionView reloadData]; }); }); } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 1; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { self.collectionView.mj_footer.hidden = self.shops.count == 0; return self.shops.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { GZShopCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath]; if (self.shops && self.shops.count >= indexPath.item+1) cell.shop = self.shops[indexPath.item]; return cell; } - (CGFloat)columnMarginInFallsLayout:(GZFallsLayout *)fallsLayout { return 5; } - (CGFloat)rowMarginInFallsLayout:(GZFallsLayout *)fallsLayout { return 5; } - (CGFloat)columnCountInFallsLayout:(GZFallsLayout *)fallsLayout { return 4; } - (UIEdgeInsets)edgeInsetsInFallsLayout:(GZFallsLayout *)fallsLayout { return UIEdgeInsetsMake(0, 10, 20, 10); } - (NSMutableArray *)shops { if (!_shops) { _shops = [NSMutableArray array]; } return _shops; } // 计算布局属性 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; // 每个collectionView的宽度 CGFloat collectionViewW = self.collectionView.frame.size.width; // 每个cell的宽度 CGFloat w = (collectionViewW - self.edgeInsets.left - self.edgeInsets.right - self.columnMargin * (self.columnCount - 1)) / self.columnCount; // cell的高度 NSUInteger randomOfHeight = arc4random() % 100; CGFloat h = w * (randomOfHeight >= 50 ? 250 : 320) / 200; // 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; } // 随机数,用来随机生成大尺寸cell NSUInteger randomOfWhetherDouble = arc4random() % 100; // 判断是否放大 if (destColumn < self.columnCount - 1 // 放大的列数不能是最后一列(最后一列方法超出屏幕) && _noneDoubleTime >= 1 // 如果前个cell有放大就不放大,防止连续出现两个放大 && (randomOfWhetherDouble >= 45 || _noneDoubleTime >= 8) // 45%几率可能放大,如果累计8次没有放大,那么满足放大条件就放大 && [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); // 当前cell列的高度就是当前cell的最大Y值 self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame)); // 当前cell列下一列的高度也是当前cell的最大Y值,因为cell宽度*2,占两列 self.columnHeights[destColumn + 1] = @(CGRectGetMaxY(attrs.frame)); } else { // 正常cell的布局 if (_noneDoubleTime <= 3 || _lastFixIndex == destColumn) { // 如果没有放大次数小于3且当前列等于上次矫正的列,就不矫正 attrs.frame = CGRectMake(x, y, w, h); } else if (self.columnHeights.count > destColumn + 1 // 越界判断 && y + h - [self.columnHeights[destColumn + 1] doubleValue] < w * 0.1) { // 当前cell填充后和上一列的高度偏差不超过cell最大高度的10%,就和下一列对齐 attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn + 1] doubleValue] - y); _lastFixIndex = destColumn; } else if (destColumn >= 1 // 越界判断 && y + h - [self.columnHeights[destColumn - 1] doubleValue] < w * 0.1) { // 当前cell填充后和上上列的高度偏差不超过cell最大高度的10%,就和下一列对齐 attrs.frame = CGRectMake(x, y, w, [self.columnHeights[destColumn - 1] doubleValue] - y); _lastFixIndex = destColumn; } else { attrs.frame = CGRectMake(x, y, w, h); } // 当前cell列的高度就是当前cell的最大Y值 self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame)); _noneDoubleTime += 1; } // 返回计算获取的布局 return attrs; } |
2、规则瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 计算布局属性 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; if (indexPath.item == 0) { attrs.frame = CGRectMake(0, 0, ([UIScreen mainScreen].bounds.size.width - 4)/3*2 +2, ([UIScreen mainScreen].bounds.size.width - 4)/3*2 +2); } else if (indexPath.item == 1){ attrs.frame = CGRectMake(([UIScreen mainScreen].bounds.size.width - 4)/3*2 +4, 0, ([UIScreen mainScreen].bounds.size.width - 4)/3, ([UIScreen mainScreen].bounds.size.width - 4)/3); } else if (indexPath.item == 2){ attrs.frame = CGRectMake(([UIScreen mainScreen].bounds.size.width - 4)/3*2 +4, ([UIScreen mainScreen].bounds.size.width - 4)/3 +2, ([UIScreen mainScreen].bounds.size.width - 4)/3, ([UIScreen mainScreen].bounds.size.width - 4)/3); } else if (indexPath.item == 3){ attrs.frame = CGRectMake(([UIScreen mainScreen].bounds.size.width - 4)/3*2 +4, ([UIScreen mainScreen].bounds.size.width - 4)/3 *2 + 4, ([UIScreen mainScreen].bounds.size.width - 4)/3, ([UIScreen mainScreen].bounds.size.width - 4)/3); } else if (indexPath.item == 4){ attrs.frame = CGRectMake(([UIScreen mainScreen].bounds.size.width - 4)/3 +2, ([UIScreen mainScreen].bounds.size.width - 4)/3 *2 +4, ([UIScreen mainScreen].bounds.size.width - 4)/3, ([UIScreen mainScreen].bounds.size.width - 4)/3); } else { attrs.frame = CGRectMake(0, ([UIScreen mainScreen].bounds.size.width - 4)/3 *2 +4, ([UIScreen mainScreen].bounds.size.width - 4)/3, ([UIScreen mainScreen].bounds.size.width - 4)/3); } // 返回计算获取的布局 return attrs; } |
3、简单两排瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | -(CGSize)collectionViewContentSize { //计算整个contentsize的大小 __block CGFloat height=0; [arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj floatValue]>height) { height=[obj floatValue]; } }]; return CGSizeMake(self.collectionView.bounds.size.width, height); } -(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { //计算每一个item的相关属性 UICollectionViewLayoutAttributes *attr=[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; int index=0; if ([arr[0] floatValue]<[arr[1] floatValue]) { index=0; } else { index=1; } CGFloat width=self.collectionView.bounds.size.width/2; CGFloat height=arc4random()%200+100; CGFloat left=index*width; CGFloat top=[arr[index] floatValue]; CGRect frame=CGRectMake(left, top, width, height); attr.frame=frame; arr[index]=@([arr[index] floatValue]+height); return attr; } |
4、两排瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | -( void )prepareLayout{ _attributeArray = [NSMutableArray array]; [super prepareLayout]; float WIDTH = ([UIScreen mainScreen].bounds.size.width-self.sectionInset.left-self.sectionInset.right-self.minimumInteritemSpacing)/2; CGFloat colHight[2] = {self.sectionInset.top,self.sectionInset.bottom}; for ( int i=0; i<_itemCount; i++) { NSIndexPath * index = [NSIndexPath indexPathForItem:i inSection:0]; UICollectionViewLayoutAttributes * attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:index]; CGFloat height = arc4random()%150+40; int width = 0; if (colHight[0]<colHight[1]) { colHight[0] = colHight[0]+height+self.minimumLineSpacing; width = 0; } else { colHight[1] = colHight[1]+height+self.minimumLineSpacing; width = 1; } attribute.frame = CGRectMake(self.sectionInset.left+(self.minimumInteritemSpacing+WIDTH)*width, colHight[width]-height-self.minimumLineSpacing, WIDTH, height); [_attributeArray addObject:attribute]; } if (colHight[0]>colHight[1]) { self.itemSize = CGSizeMake(WIDTH, (colHight[0]-self.sectionInset.top)*2/_itemCount-self.minimumLineSpacing); } else { self.itemSize = CGSizeMake(WIDTH, (colHight[1]-self.sectionInset.top)*2/_itemCount-self.minimumLineSpacing); } } |
5、环形瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 | _itemCount = ( int )[self.collectionView numberOfItemsInSection:0]; _attributeArray = [NSMutableArray array]; CGFloat radius =MIN(self.collectionView.frame.size.width, self.collectionView.frame.size.height)/2; CGPoint center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2); for ( int i=0; i<_itemCount; i++) { UICollectionViewLayoutAttributes * attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; attris.size = CGSizeMake(50, 50); float x = center.x+cosf(2*M_PI/_itemCount*i)*(radius-25); float y = center.y+sinf(2*M_PI/_itemCount*i)*(radius-25); attris.center = CGPointMake(x, y); [_attributeArray addObject:attris]; } |
6、立方瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | -(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{ //创建一个item布局属性类 UICollectionViewLayoutAttributes * atti = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; //获取item的个数 int itemCounts = ( int )[self.collectionView numberOfItemsInSection:0]; //设置每个item的大小为260*100 atti.size = CGSizeMake(260, 100); atti.center = CGPointMake(self.collectionView.frame.size.width/2, self.collectionView.frame.size.height/2+self.collectionView.contentOffset.y); CATransform3D tran3d = CATransform3DIdentity; tran3d.m34 = -1/2000.0; CGFloat radius = 50/tanf(M_PI*2/itemCounts/2); // CGFloat angle = (float)(indexPath.row)/itemCounts*M_PI*2; //获取当前的偏移量 float offset = self.collectionView.contentOffset.y; //在角度设置上,添加一个偏移角度 float angleOffset = offset/self.collectionView.frame.size.height; CGFloat angle = ( float )(indexPath.row+angleOffset-1)/itemCounts*M_PI*2; tran3d = CATransform3DRotate(tran3d, angle, 1.0, 0, 0); tran3d = CATransform3DTranslate(tran3d, 0, 0, radius); //进行设置 atti.transform3D = tran3d; return atti; } |
7、球形瀑布流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | UICollectionViewLayoutAttributes * atti = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; //获取item的个数 int itemCounts = ( int )[self.collectionView numberOfItemsInSection:0]; atti.center = CGPointMake(self.collectionView.frame.size.width/2+self.collectionView.contentOffset.x, self.collectionView.frame.size.height/2+self.collectionView.contentOffset.y); atti.size = CGSizeMake(30, 30); CATransform3D trans3D = CATransform3DIdentity; trans3D.m34 = -1/900.0; CGFloat radius = 15/tanf(M_PI*2/itemCounts/2); //根据偏移量 改变角度 //添加了一个x的偏移量 float offsety = self.collectionView.contentOffset.y; float offsetx = self.collectionView.contentOffset.x; //分别计算偏移的角度 float angleOffsety = offsety/self.collectionView.frame.size.height; float angleOffsetx = offsetx/self.collectionView.frame.size.width; CGFloat angle1 = ( float )(indexPath.row+angleOffsety-1)/itemCounts*M_PI*2; //x,y的默认方向相反 CGFloat angle2 = ( float )(indexPath.row+angleOffsetx-1)/itemCounts*M_PI*2; //这里我们进行四个方向的排列 if (indexPath.row%4==1) { trans3D = CATransform3DRotate(trans3D, angle1, 1.0,0, 0); } else if (indexPath.row%4==2){ trans3D = CATransform3DRotate(trans3D, angle2, 0, 1, 0); } else if (indexPath.row%4==3){ trans3D = CATransform3DRotate(trans3D, angle1, 0.5,0.5, 0); } else { trans3D = CATransform3DRotate(trans3D, angle1, 0.5,-0.5,0); } trans3D = CATransform3DTranslate(trans3D, 0, 0, radius); atti.transform3D = trans3D; |
三、运行效果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?