美文网首页iOS Developer
iOS UIScrollView实现瀑布流

iOS UIScrollView实现瀑布流

作者: 34码的小孩子 | 来源:发表于2017-04-25 09:46 被阅读130次

前言

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

最终效果图

想法

  • 实现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];
}

demo百度云地址

相关文章

网友评论

    本文标题:iOS UIScrollView实现瀑布流

    本文链接:https://www.haomeiwen.com/subject/uyqpzttx.html