LazyScrollView 继承自 ScrollView,目标是解决异构(与 TableView 的同构对比)滚动视图的复用回收问题。它可以支持跨 View 层的复用,用易用方式来生成一个高性能的滚动视图。
我们在做首页的时候,往往展示的东西会很多,随着 View 数量逐渐膨胀,没有一套复用回收机制的 ScrollView 已经影响到性能了,迫切需要处理对 ScrollView 中 View 的复用和回收。使用 TableView 只能用来解决同类 Cell 的展示,然而在实际的场景中在 ScrollView 里面,View 的种类往往会比较多,所以使用 TableView 不适合我们的场景。
而 UICollectionView 本身的布局和复用回收机制不够灵活,用起来也较为繁琐。所以诞生了 LazyScrollView 去解决这个问题。这也是天猫 iOS 客户端的首页落地方案。
LazyScrollView 的使用和 TableView 很像,不过多了一个需要实现的方法:返回对应 index 的 View 相对 LazyScrollView 的绝对坐标。
类似 TableView 的用法,我们需要使用方实现 LazyScrollViewDatasource 的 Delegate。
- @protocol TMMuiLazyScrollViewDataSource <NSObject>
- @required
- //ScrollView展示item个数
- - (NSUInteger)numberOfItemInScrollView:(TMMuiLazyScrollView *)scrollView;
- //要求根据index直接返回RectModel
- - (TMMuiRectModel *)scrollView:(TMMuiLazyScrollView *)scrollView rectModelAtIndex:(NSUInteger)index;
- //返回下标所对应的view
- - (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID;
LazyScrollView 的核心是在初始状态就得知所有 View 应该显示的位置。第一个方法很简单,获取 LazyScrollView 中 item 的个数。第二个方法需要按照 Index 返回 TMMuiRectModel ,它会携带对应 index 的 View 相对 LazyScrollView 的绝对坐标。
这里出现了一个 TMMuiRectModel ,这是个什么东西呢?我们看一下代码:
- @interface TMMuiRectModel:NSObject
- //转换后的绝对值rect
- @property (nonatomic,assign) CGRect absRect;
- //业务下标
- @property (nonatomic,copy) NSString *muiID;
这里有两个属性,absRect 是 LazyScroll 中的 View 相对 LazyScrollView 的绝对坐标,muiID 是这个 View 在 LazyScrollView 中唯一的标识符,可赋值也可不赋值。
第三个方法,返回 View。
- @interface UIView(TMMui)
- //索引过的标识,在LazyScrollView范围内唯一
- @property (nonatomic, copy) NSString *muiID;
- //重用的ID
- @property (nonatomic, copy) NSString *reuseIdentifier;
首先,我们在 UIView 之外加了一个 Category, 这个 category 可以让 View 携带 muiID 和 reuseIdentifier, 对于返回的 View 来说,只需要在乎对 View 的 reuseIdentifier 赋值,muiID 的赋值会在 lazyScrollView 中处理掉。reuseIdentifier 相同的 View 会被复用,如果这个 View 的 reuseIdentifier 是 nil 或者空字符串,则不会被复用。
首先来看一个简单的案例:
根据 DataSource 的 Delegate,拿到所有的 View 应该被显示的位置。这一步,核心是拿到的位置是确定的。根据 Demo,我们观察从 0/1 - 2/3 之间这些 View,这个时候 LazyScrollView 拿到的 Rect 如下:
Index | 标号 (MUIID) | Rect |
---|---|---|
0 | 0/0 | origin = (x = 25, y = 15), size = (width = 156, height = 150 |
1 | 0/1 | origin = (x = 194, y = 15), size = (width = 156, height = 150) |
2 | 0/2 | origin = (x = 25, y = 180), size = (width = 156, height = 150) |
3 | 0/3 | origin = (x = 194, y = 180), size = (width = 156, height = 150 |
4 | 1/0 | origin = (x = 5, y = 360), size = (width = 177.5, height = 150) |
5 | 1/1 | origin = (x = 192.5, y = 426), size = (width = 84, height = 84) |
6 | 1/2 | origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56) |
7 | 1/3 | origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84) |
8 | 2/0 | origin = (x = 25, y = 530), size = (width = 325, height = 150) |
9 | 2/1 | origin = (x = 25, y = 695), size = (width = 325, height = 150) |
10 | 2/2 | origin = (x = 25, y = 860), size = (width = 325, height = 150) |
拿到了这些位置之后,接下来做的事情就是排序。排序生成的索引会有两个:根据顶边 (y) 升序排序的索引和根据底边 (y+height) 降序排序的索引。
根据顶边 (y) 升序排序的索引
Index | 标号 (MUIID) | Rect |
---|---|---|
0 | 0/0 | origin = (x = 25, y = 15), size = (width = 156, height = 150 |
1 | 0/1 | origin = (x = 194, y = 15), size = (width = 156, height = 150) |
2 | 0/2 | origin = (x = 25, y = 180), size = (width = 156, height = 150) |
3 | 0/3 | origin = (x = 194, y = 180), size = (width = 156, height = 150 |
4 | 1/0 | origin = (x = 5, y = 360), size = (width = 177.5, height = 150) |
5 | 1/1 | origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56) |
6 | 1/2 | origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56) |
7 | 1/3 | origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84) |
8 | 2/0 | origin = (x = 25, y = 530), size = (width = 325, height = 150) |
9 | 2/1 | origin = (x = 25, y = 695), size = (width = 325, height = 150) |
10 | 2/2 | origin = (x = 25, y = 860), size = (width = 325, height = 150) |
根据底边 (y+height) 降序排序的索引
Index | 标号 (MUIID) | Rect |
---|---|---|
0 | 2/2 | origin = (x = 25, y = 860), size = (width = 325, height = 150) |
1 | 2/1 | origin = (x = 25, y = 695), size = (width = 325, height = 150) |
2 | 2/0 | origin = (x = 25, y = 530), size = (width = 325, height = 150) |
3 | 1/0 | origin = (x = 5, y = 360), size = (width = 177.5, height = 150) |
4 | 1/2 | origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56) |
5 | 1/3 | origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84) |
6 | 1/1 | origin = (x = 192.5, y = 426), size = (width = 84, height = 84) |
7 | 0/2 | origin = (x = 25, y = 180), size = (width = 156, height = 150) |
8 | 0/3 | origin = (x = 194, y = 180), size = (width = 156, height = 150 |
9 | 0/0 | origin = (x = 25, y = 15), size = (width = 156, height = 150 |
10 | 0/1 | origin = (x = 194, y = 15), size = (width = 156, height = 150) |
前两步是在执行完 reload,在视图还没有生成的时候就开始做了,而接下来的步骤在要生成视图(初始化或滚动的时候)才会去做。
我们设定了 Buffer 为上下各 20,滚动超过 20 个像素后才会指定查找视图并显示的动作。举个例子,如下图,红圈是应该显示的区域。
如上图所示,现在已知的是红圈顶边 y 是 242,底边 y 是 949,加上缓冲区 Buffer,应该是找 222 - 969 之间的 View。我们要做的是,找到底边 y 小于 969 的 Model 和顶边 y 大于 222 的 Model,取交集,就是我们要显示的 View。
采用的方法为二分查找,在根据顶边升序排序的索引中找 949,找到的 index 为 0(MUIID 为 2/2),我们使用一个 Set,把根据顶边排序中 index >= 0 的元素先放在这里。获取的 Set 中包含的 muiID 为 0/0,0/1,0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2。
根据底边排序的索引中找 222,找到的 index 为 2,我们把 index >= 2 的元素放在另一个 Set,获取的 Set 中包含的 muiID 为 0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2
两个 Set 取交集,得到的就是我们的 ResultSet,这里面都是我们要显示 View 的 Model,它们的 muiID 是 0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2。
我们知道了应该显示哪些 View,但是我们之后做的第一步是把不需要显示的 View 加入到复用池中。LazyScroll 可以取到当前显示了的 View,拿当前显示的 View 的 muiID 和将要显示 view 的 Model 的 muiID 做对比,可以知道当前显示的 View 哪些应该被回收。
LazyScrollView 中有一个 Dictionary,key 是 reuseIdentifier,Value 是对应 reuseIdentifier 被回收的 View,当 LazyScrollView 得知这个 View 不该再出现了,会把 View 放在这里,并且把这个 View hidden 掉。
然后,用 LazyScrollView 会去调用 datasource。
- - (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID;
复用还是不复用,是由 datasource 决定的。如果要复用,需要 datasource 方法内调用,即:
- - (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier
获取复用的 View,这个方法取出来的 View 就是在上一段所说的 Dictionary 中拿的。
最后我们看一下 LazyScrollView 的使用流程:找到所有 View 将要显示的位置 – 排序 – 查找应该显示的 View – 回收 – 创建 / 复用。
来源: