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