iOS UIScrollView实现瀑布流
前言
一般来说,一个界面展示的图片的比例是不相同的,而为了让图片展示得比较好看——没有拉伸变形,也没有缩小后上下的黑边,尽量让图片按实际大小的比例展示,而且很多网页喜欢用这样瀑布流的布局。
备注:这个实现方法有个限制,必须在布局前拿到图片的宽高长度或者是宽高比例。如果是本地资源就比较好办,但如果是网上下载的图片资源,则需要下载完成后才能进行布局,或者是在请求接口返回下载链接时,后台一并返回宽高。

想法
- 实现FlowScrollView继承UIScrollView,然后参考tableView的用法, 声明UIScrollViewDelegate的子类FlowScrollViewDelegate和FlowScrollViewDataSource。这样就可以像用tableView一样使用FlowScrollView了。
- 布局问题:每一列的大小相同,每个cell的高度不同,cell的高度由宽度和图片的宽高比例决定。使用一个数组将所有的cell的frame保存起来,当布局发生变化时,对数组的所有frame重新赋值。下一个展示的Cell是放置在Y值最小的那一列下面的。
- cell的复用问题:可变字典displayCellsDict以frameArray的下标为Key存储当前屏幕正在展示的cell。可变集合resuabelCells用作缓存池,存放移到屏幕外面后被移除的cell。
实现
FlowScrollView
继承UIScrollView,首先来看看delegate的声明,声明了三个方法,分别是设置cell的高度、边距和点击回调。
@protocol FlowScrollViewDelegate <UIScrollViewDelegate> @optional //设置每个cell的高度 - (CGFloat)flowScrollView:(FlowScrollView *)scrollView heightAtIndex:(NSInteger)index; //设置各种边距的大小 - (CGFloat)flowScrollView:(FlowScrollView *)scrollView marginType:(FlowMarginType)marginType; //点击cell时回调的方法 - (void)flowScrollView:(FlowScrollView *)scrollView didSelectAtIndex:(NSInteger)index; @end
然后是关于数据源的dataSource,分别声明了cell的总个数、列数和设置cell展示内容的方法。
@protocol FlowScrollViewDataSource <NSObject> @required //cell的总数量 - (NSInteger)numberOfCellsInFlowScrollView:(FlowScrollView *)scrollView; //设置cell的展示内容 - (FlowScrollViewCell *)flowScrollView:(FlowScrollView *)scrollView cellAtIndex:(NSInteger)index; @optional //cell的列数 - (NSInteger)numberOfColumnsInFlowScrollView:(FlowScrollView *)scrollView; @end
只需要在ViewController中创建一个FlowScrollView的对象,再指定改对象的delegate和dataSoure为ViewController,并且实现相应的方法,最后添加到ViewController.view中即可。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self initData]; self.imageArray = [[NSArray alloc] initWithArray:self.dataArray[0]]; CGSize size = [UIScreen mainScreen].bounds.size; self.scrollView = [[FlowScrollView alloc] initWithFrame:CGRectMake(0, 64, size.width, size.height - 64 - 49)]; self.scrollView.delegate = self; self.scrollView.dataSource = self; [self.view addSubview:self.scrollView]; }
参照tableView的刷新数据方法名称reloadData, 在需要重新布局时调用该方法,在方法中对cell的frame重新赋值。
首先清除原来的布局,然后根据delegate和dataSource获取Cell总个数numberOfCells、列数numberOfColumns和各种边距的大小。找出已布局的所有列中最小的Y值所在的列,将下一个cell放在该列中。更新存储每列最大Y值的数组,然后再进行布局下一个cell的位置。如此循环,最终得到所有cell的frame, 保存在frameArray中。
- (void)reloadData { //清除原来的frame if (self.frameArray.count > 0) { [self.frameArray removeAllObjects]; } //获取Cell的总数量和scrollView的列数 NSInteger numberOfCells = [self.dataSource numberOfCellsInFlowScrollView:self]; NSInteger numberOfColumns = [self numberOfColumns]; //获取各种间距 CGFloat topMargin = [self marginForType:FlowMarginTypeTop]; CGFloat bottomMargin = [self marginForType:FlowMarginTypeBottom]; CGFloat leftMargin = [self marginForType:FlowMarginTypeLeft]; CGFloat rightMargin = [self marginForType:FlowMarginTypeRight]; CGFloat rowMargin = [self marginForType:FlowMarginTypeRow]; CGFloat columnMargin = [self marginForType:FlowMarginTypeColumn]; //cell的宽度,保存起来,在cell计算高度时用到 self.cellWidth = (self.width - leftMargin - rightMargin - columnMargin * (numberOfColumns - 1)) / numberOfColumns; //所有列最大Y值的数组 NSMutableArray *maxOfYInColumns = [NSMutableArray new]; for (int i = 0; i < numberOfColumns; i++) { [maxOfYInColumns addObject:[NSNumber numberWithFloat:0.0]]; } //循环所有cell for (int i = 0; i < numberOfCells; i++) { //存储最小Y值Cell的列数,然后将下一个展示的cell放到该列 NSInteger minYCellColumn = 0; //存放所有列中最小的Y值 CGFloat minYInCell = [maxOfYInColumns[minYCellColumn] floatValue]; //求出最短一列的Y值 for (int j = 1; j < numberOfColumns; j++) { if ([maxOfYInColumns[j] floatValue] < minYInCell) { minYCellColumn = j; minYInCell = [maxOfYInColumns[j] floatValue]; } } //询问代理cell即将布局的cell的高度 CGFloat cellHeight = [self heightAtIndex:i]; //设置Cell的frame CGFloat cellX = leftMargin + minYCellColumn * (self.cellWidth + columnMargin); CGFloat cellY = 0; if (minYInCell == 0.0) { //首行 cellY = topMargin; } else { cellY = minYInCell + rowMargin; } CGRect cellFrame = CGRectMake(cellX, cellY, self.cellWidth, cellHeight); maxOfYInColumns[minYCellColumn] = [NSNumber numberWithFloat:CGRectGetMaxY(cellFrame)]; [self.frameArray addObject:[NSValue valueWithCGRect:cellFrame]]; //获取整个瀑布流的高度 CGFloat contentHeight = [maxOfYInColumns[0] floatValue]; for (int i = 0; i < numberOfColumns; i++) { if ([maxOfYInColumns[i] floatValue] > contentHeight) { contentHeight = [maxOfYInColumns[i] floatValue]; } } contentHeight += bottomMargin; //设置scrollView可滚动范围 self.contentSize = CGSizeMake(self.width, contentHeight); } [self setNeedsLayout]; }
以下是通过delegate获取cell高度的方法,其他方法与此类似。
- (CGFloat)heightAtIndex:(NSInteger)index { if ([self.delegate respondsToSelector:@selector(flowScrollView:heightAtIndex:)]) { return [self.delegate flowScrollView:self heightAtIndex:index]; } return FlowScrollViewDefaultHeight; }
重载layoutSubviews方法,在该方法中展示cell。首先根据frameArray的index在displayCellsDict中获取对应的cell。
- 如果能获取到cell并且frame在屏幕上,则需要更新该cell的展示内容和位置。
- 如果cell是空的,但是frame在屏幕上,则需要在缓存池中获取一个cell, 如果缓存池中没有,则需要创建一个cell,并且将cell放置到displayCellsDict中。
- 如果cell 非空,但是不在屏幕上,就需要将cell从父视图中移除,并且在displayCellsDict中移除,然后添加到缓存池resuabelCells中。
- (void)layoutSubviews { [super layoutSubviews]; NSInteger numberOfCells = self.frameArray.count; for (int i = 0; i < numberOfCells; i++) { CGRect cellFrame = [self.frameArray[i] CGRectValue]; //首先在保存显示cell的数组中获取对应的cell FlowScrollViewCell *cell = self.displayCellsDict[@(i)]; if ([self isInScreen:cellFrame]) { //在屏幕上 if (cell == nil) { cell = [self.dataSource flowScrollView:self cellAtIndex:i]; cell.frame = cellFrame; [self addSubview:cell]; //存放到字典中 self.displayCellsDict[@(i)] = cell; } else { [self.dataSource flowScrollView:self cellAtIndex:i]; cell.frame = cellFrame; } } else { //不在屏幕上 if (cell) { //从瀑布流和字典中删除 [cell removeFromSuperview]; [self.displayCellsDict removeObjectForKey:@(i)]; //存进缓存池 [self.resuabelCells addObject:cell]; } } } //防止上一次展示数量大于当次时,超出的部分没有移除 if (_preFrameCount > numberOfCells) { for (int i = (int)numberOfCells; i < _preFrameCount; i++) { FlowScrollViewCell *cell = self.displayCellsDict[@(i)]; if (cell) { //从瀑布流和字典中删除 [cell removeFromSuperview]; [self.displayCellsDict removeObjectForKey:@(i)]; //存进缓存池 [self.resuabelCells addObject:cell]; } } } }
因为layoutSubviews方法只循环了numberOfCells次,如果上一次展示的图片数量多于这次的数量时,上次的显示的图片没有移除,所以最后需要进行判断移除。
关于上面第二点,从displayCellsDict中没有获取到cell的时候,是怎么从缓存池中获取cell的。
cell = [self.dataSource flowScrollView:self cellAtIndex:i] 方法会跳转到viewController的对应方法,然后调用dequeueResuableCellWithIdentifier: index: 方法。如果该方法还是没有拿到cell,就需要创建一个。
- (FlowScrollViewCell *)flowScrollView:(FlowScrollView *)scrollView cellAtIndex:(NSInteger)index { FlowScrollViewCell *cell = [scrollView dequeueResuableCellWithIdentifier:CellIdentifier index:index]; if (cell == nil) { cell = [[FlowScrollViewCell alloc] initWithIdentifier:CellIdentifier]; } [cell setCellImageName:self.imageArray[index] width:scrollView.cellWidth]; return cell; } - (id)dequeueResuableCellWithIdentifier:(NSString *)identifier index:(NSInteger)index { FlowScrollViewCell *cell = self.displayCellsDict[@(index)]; //判断是否是正在展示的cell if (cell) { return cell; } //不是,就在缓存池中拿一个 __block FlowScrollViewCell *resuableCell = [self.resuabelCells anyObject]; [self.resuabelCells enumerateObjectsUsingBlock:^(FlowScrollViewCell *cell, BOOL * _Nonnull stop) { if ([resuableCell.identifier isEqualToString:cell.identifier]) { resuableCell = cell; *stop = YES; } }]; //从缓存池中拿走了一个,就需要从缓存池移除掉 if (resuableCell) { //从缓存池中移除cell [self.resuabelCells removeObject:resuableCell]; } return resuableCell; }
cell的点击事件:在FlowScrollView添加单击手势,然后判断点击点在哪个cell的frame范围内。
- (void)addTapGesture { UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapAction:)]; gesture.numberOfTapsRequired = 1; self.userInteractionEnabled = YES; [self addGestureRecognizer:gesture]; } - (void)singleTapAction:(UIGestureRecognizer *)sender { CGPoint point = [sender locationInView:self]; for (int i = 0; i < _frameArray.count; i++) { CGRect frame = [_frameArray[i] CGRectValue]; if (CGRectContainsPoint(frame, point)) { if ([self.delegate respondsToSelector:@selector(flowScrollView:didSelectAtIndex:)]) { return [self.delegate flowScrollView:self didSelectAtIndex:i]; } break; } } }
最后,在添加FlowScrollView到它的父视图的时候,就需要布局了,所以重载willMoveToSuperview: 方法,刷新数据。
- (void)willMoveToSuperview:(UIView *)newSuperview { [self reloadData]; }
FlowScrollViewCell
继承UIView,创建时设置唯一标识Identifier
- (instancetype)initWithIdentifier:(NSString *)identifier;
设置图片时,根据图片的实际宽高和cell的宽度,设置图片的frame。
- (void)setCellImageName:(NSString *)imageName width:(CGFloat)width { UIImage *image = [UIImage imageNamed:imageName]; CGFloat height = width / image.size.width * image.size.height; self.picture.frame = CGRectMake(0, 0, width, height); self.picture.image = image; }
使用类方法,根据cell的宽度,让viewController获取到图片的高度。
+ (CGFloat)getCellHeight:(NSString *)imageName width:(CGFloat)width { UIImage *image = [UIImage imageNamed:imageName]; CGFloat height = width / image.size.width * image.size.height; return height; } - (CGFloat)flowScrollView:(FlowScrollView *)scrollView heightAtIndex:(NSInteger)index { return [FlowScrollViewCell getCellHeight:self.imageArray[index] width:scrollView.cellWidth]; }
ViewController
用法跟tableView的用法类似,指定delegate和dataSource, 实现FlowScrollViewDelegate, FlowScrollViewDataSource的方法,在数据源改变的时候reloadData。
- (IBAction)resetLayoutAction:(UIButton *)sender { self.currentIndex += 1; int index = self.currentIndex % 4; self.scrollView.preFrameCount = self.imageArray.count; self.imageArray = [[NSMutableArray alloc] initWithArray:self.dataArray[index]]; [self.scrollView reloadData]; }
添加一个全屏的UIImageView,并添加单击手势。点击cell,展示对应的大图,点击大图,移除大图。
- (void)initFullImageView:(CGSize)size { self.fullImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)]; self.fullImageView.contentMode = UIViewContentModeScaleAspectFit; self.fullImageView.userInteractionEnabled = YES; self.fullImageView.backgroundColor = [UIColor blackColor]; UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fullImageViewTouch)]; gesture.numberOfTapsRequired = 1; [self.fullImageView addGestureRecognizer:gesture]; } - (void)flowScrollView:(FlowScrollView *)scrollView didSelectAtIndex:(NSInteger)index { self.fullImageView.image = [UIImage imageNamed:self.imageArray[index]]; [self.view addSubview:self.fullImageView]; }


浙公网安备 33010602011771号