iOS基础 - 瀑布流
一、瀑布流简介
瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格
二、瀑布流特点
l 琳琅满目:整版以图片为主,大小不一的图片按照一定的规律排列
l 唯美:图片的风格以唯美的图片为主
l 操作简单:在浏览网站的时候只需要轻轻滑动一下鼠标滚轮,一切的美妙的图片精彩便可呈现在你面前
三、瀑布流应用
瀑布流对于图片的展现,是高效而具有吸引力的,用户一眼扫过的快速阅读模式可以在短时间内获得更多的信息量,而瀑布流里懒加载模式又避免了用户鼠标点击的翻页操作
瀑布流的主要特性便是错落有致,定宽而不定高的设计让页面区别于传统的矩阵式图片布局模式,巧妙的利用视觉层级,视线的任意流动又缓解了视觉疲劳
四、使用瀑布流的网站
l 电商导购,如想去网、蘑菇街和美丽说、好享说,依托于淘宝平台
l 兴趣图谱分享,如知美、花瓣等
l 细分垂直领域,如针对吃货的零食控、针对家居行业的他部落等
五、瀑布流的优缺点
l 优点
有效的降低了界面复杂度,节省了空间:我们不再需要臃肿复杂的页码导航链接或按钮了
对触屏设备来说,交互方式更符合直觉:在移动应用的交互环境当中,通过向上滑动进行滚屏的操作已经成为最基本的用户习惯,而且所需要的操作精准程度远远低于点击链接或按钮
更高的参与度:以上两点所带来的交互便捷性可以使用户将注意力更多的集中在内容而不是操作上,从而让他们更乐于沉浸在探索与浏览当中
l 缺点
有限的用例:无限滚动的方式只适用于某些特定类型产品当中一部分特定类型的内容
额外的复杂度
l SEO:集中在一页当中动态加载数据,不利于SEO,对于网站而言,存在一定的风险
l 页面的数量:如果网站需要通过更多的内容页面展示更多的相关信息(包括广告)是很重要的策略,那么单页无限滚动的方式就不适用了
六、实现瀑布流的思路
使用多个UITableView,控制它们同时滚动,在复杂的用户操作下,会出现滚动不同步的情况;如果需要支持设备的多个方向,不利于增加图片列数及方向切换的动画效果,使用一个UIScrollView,参考UITableView的实现方式,开发一个符合需求的可重用的控件
七、瀑布流数据处理思路
每次通过一个GET方法加载一个JSON数据,数据中包含一组数据信息:图像URL、图像标题等
异步加载JSON数据包中的图像,从左至右,从上至下依次显示
滚动至数据末尾时,加载新的数据
数据加载后,从当前最末一张图像开始追加新的图像
滚动至顶部时,下拉刷新新的数据
新数据加载后,重新填充当前视图中的内容
八、开发步骤
使用沙箱数据:自定义UIScrollView,使用沙箱中的图像模拟瀑布流的实现
集成网络:通过网络加载并处理数据
九、自定义瀑布流控件
自定义UIScrollView(WaterFlowView),模拟UITableView
自定义UIView(WaterFlowViewCell),模拟UITableViewCell
单独负责图像内容及文本标签的显示
使用可重用标示符处理视图的重用
自定义UIViewController(WaterFlowViewController),模拟UITableViewController
处理自定义UIScrollView的数据源及代理方法
十、WaterFlowViewCell的定义
属性
// 可重用标示符
@property (strong, nonatomic) NSString *reuseIdentifier;
// 被选中标记
@property (assign, nonatomic) BOOL selected;
// 图像视图
@property (weak, nonatomic) UIImageView *imageView;
// 文本标签
@property (weak, nonatomic) UILabel *textLabel;
方法:
// 使用可重用标示符实例化视图
- (id)initWithResueIdentifier:(NSString *)reusedIdentifier;
十一、WaterFlowViewCell的实现
利用控件的getter方法实现控件懒加载
重写layoutSubviews方法,调整控件布局
注意:此处不需要调用父类的layoutSubViews方法
十二、WaterFlowView的定义数据源和代理
l 数据源
单元格数量
l 单元格
视图的列数(默认为1,可选方法)
l 代理
指定单元格的高度
单元格被选中
十三、数据源协议
@protocol WaterFlowViewDataSource <NSObject>
// 单元格数量
- (NSInteger)waterFlowViewNumberOfCells:(WaterFlowView *)waterFlowView;
// 单元格
- (WaterFlowViewCell *)waterFlowView:(WaterFlowView *)waterFlowView cellAtIndexPath:(NSIndexPath *)indexPath;
@optional
// 视图的列数(可选,默认1列)
- (NSInteger)waterFlowViewNumberOfColumns:(WaterFlowView *)waterFlowView;
@end
十四、代理协议
@protocol WaterFlowViewDelegate <UIScrollViewDelegate>
// 指定单元格的高度
- (CGFloat)waterFlowView:(WaterFlowView *)waterFlowView heightForCellAtIndexPath:(NSIndexPath *)indexPath;
// 单元格被选中
- (void)waterFlowView:(WaterFlowView *)waterFlowView didSelectedCellAtIndexPath:(NSIndexPath *)indexPath;
@end
十五、WaterFlowViewController中的实例化视图
- (void)loadView
{
_waterFlowView = [[WaterFlowView alloc]initWithFrame:CGRectZero];
[_waterFlowView setDataSource:self];
[_waterFlowView setDelegate:self];
// 根据父视图大小调整自身大小
[_waterFlowView setAutoresizingMask:UIViewAutoresizingFlexibleWidth
| UIViewAutoresizingFlexibleHeight];
self.view = _waterFlowView;
}
十六、编写resetView方法实现瀑布流视图的布局
1. 首先初始化根据数据行数indexPaths数组
2. 布局界面
1) 计算每列宽度
2) 使用一个数组,记录每列的当前Y值
3) 遍历self.indexPaths生成WaterFlowCellView并计算位置
4) 设置scrollView的contentSize
十七、修改WaterFlowViewController增加方向支持
#pragma mark 设备旋转结束
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self.waterFlowView reloadData];
}
#pragma mark - 列数
- (NSInteger)numberOfColumnsInWaterFlowView:(WaterFlowView *)waterFlowView
{
// 根据设备方向设定不同的瀑布流列数
if ([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft || [UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeRight) {
self.numberOfColumns = 4;
} else {
self.numberOfColumns = 3;
}
return self.numberOfColumns;
}
十八、引入缓存策略,利用数据字典记录单元格的位置
#pragma mark - 缓存数据
// 单元格位置数组
@property (strong, nonatomic) NSMutableArray *cellFramesArray;
// 单元格缓存集合
@property (strong, nonatomic) NSMutableSet *reusableCellSet;
// 正在显示单元格字典
@property (strong, nonatomic) NSMutableDictionary *screenCellsDict;
编写方法统一生成缓存数据
// 建立缓存数据
[self generateCacheData];
十九、生成缓存数据
// 1) 获取当前列数
// 2) 建立列索引数组
// 3) 建立列单元格位置数组
// 4) 建立整体索引数组
// 5. 视图缓存字典
// 6. 正在显示的单元格集合
二十、修改resetView方法,仅记录各个视图位置
// 计算出下一列的数值
NSInteger nextCol = (col + 1) % _numberOfColumns;
// 判断当前的行高是否超过下一列的行高
if (currentY[col] > currentY[nextCol]) {
col = nextCol;
}
// 在单元格数组中记录所有单元格的位置(位置&大小)
[self.cellFramesArray addObject:[NSValue valueWithCGRect:CGRectMake(x, y, colW, h)]];
二十一、判断视图是否在屏幕范围之内
#pragma mark 判断视图是否在屏幕范围之内
- (BOOL)isInScreenWithFrame:(CGRect)frame
{
return (frame.origin.y - self.contentOffset.y) < self.bounds.size.height &&
(frame.origin.y + frame.size.height - self.contentOffset.y) > 0;
}
二十二、编写dequeueReusableCellWithReuseIdentifier
#pragma mark 使用可重用标示符获取单元格对象
- (WaterFlowCellView *)dequeueReusableCellWithReuseIdentifier:(NSString *)reuseIdentifier
{
// 需要从一个集合获取可重用单元格
// 从集合中取出任意对象
WaterFlowCellView *cell = [self.reusableCellSet anyObject];
if (cell != nil) {
// 从集合中删除视图
[self.reusableCellSet removeObject:cell];
}
return cell;
}
二十三、手势处理
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
// 遍历可见视图集合,查找被点击的单元格
NSArray *array = [self.displayCellDict allKeys];
for (NSIndexPath *indexPath in array) {
WaterFlowCellView *view = self.displayCellDict[indexPath];
if (CGRectContainsPoint(view.frame, location)) {
[self.delegate waterFlowView:self didSelectRowAtIndexPath:indexPath];
}
}
}