前言
小F我最近遇到了一个小需求,实现效果如下:
当然这不是静态的,需求如下:
- 当视图上下滑动时,左边的视图跟着上下滑动,反之亦然
- 当视图左右滑动时,上边的视图跟着左右滑动,反之亦然
- 当主视图在两个方向上同时滑动,上面和左面的视图也要同时联动
- 每个区域可以响应点击事件
- 每个这样的区域展示的view可以自定制
- 当然还有最基本的展示数据
需求大致就这么多。what? fuck! mmp! 小f到这里已经崩溃了。
让小f崩溃的不是界面的复杂度,而是要模仿系统控件UITableView的复用机制。
分析
行了,废话不多说了,下面开始说正事儿。
- 小F这里把整个界面分为三个部分,上、左和中间部分
- 为了提高复用性,这个三个部分都是用了同一个基础控件
- 把相对比较复杂的中间视图拿出来单独封装
准备
先附上项目GitHub地址:XFAdvanceTableView
小F最最中意面向接口编程,所以里面的封装基本都是基于协议完成的
打开项目找到“ReuseScrollViewInterface.h”文件
#import <UIKit/UIKit.h>
@protocol ReuseScrollViewIndexInterface<NSObject>
/**
初始化
@param row 行
@param col 列
@return 对象
*/
- (instancetype)initWithRow:(NSInteger)row col:(NSInteger)col;
/** 行 */
@property(nonatomic,assign)NSInteger row;
/** 列 */
@property(nonatomic,assign)NSInteger col;
@end
#pragma mark cell的接口
@protocol ReuseScrollViewCellInterface
/**
初始化方法
@param identifier 复用标记
@return 自己的对象
*/
- (instancetype)initWithReuseIdentify:(NSString*)identifier;
/** 复用标记 */
@property(nonatomic,strong,readonly)NSString* identifier;
/** 位置 */
@property(nonatomic,strong)id<ReuseScrollViewIndexInterface> indexPath;
/** 统一的点击事件 */
@property(nonatomic,strong)UITapGestureRecognizer* tap;
@end
@protocol ReuseScrollViewInterface;
#pragma mark -代理需要实现的协议
@protocol ReuseScrollViewDataSource<NSObject>
- (NSInteger)rowNumberWithReuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView;
- (NSInteger)colNumberWithReuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView;
- (UIView<ReuseScrollViewCellInterface>*)reuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView
cellAtIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
@optional
- (CGSize)maxCellSizeWithReuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView;
- (CGFloat)reuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView horizontalGapAtIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
- (CGFloat)reuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView verticaGapAtIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
@end
@protocol ReuseScrollViewDelegate<NSObject,UIScrollViewDelegate>
@optional
- (void)reuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView selectAtIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
- (void)reuseScrollView:(UIScrollView<ReuseScrollViewInterface>*)reuseScrollView didSelectAtIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
@end
@protocol ReuseScrollViewInterface
/**
获取标记对象
@param row 行
@param col 列
@return 对象
*/
- (id<ReuseScrollViewIndexInterface>)createIndexPathWithRow:(NSInteger)row col:(NSInteger)col;
/** 数据指针 */
@property(nonatomic,weak)id<ReuseScrollViewDataSource> dataSource;
/** 代理指针 */
@property(nonatomic,weak)id<ReuseScrollViewDelegate> delegate;
/**
获取复用池中的cell
@param identifier 对应的标记
@param indexPath 位置
@return cell
*/
- (__kindof UIView<ReuseScrollViewCellInterface> *)dequeueReusableCellWithIdentifier:(NSString *)identifier indexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
/**
获取cell
@param indexPath 位置信息
@return cell
*/
- (__kindof UIView<ReuseScrollViewCellInterface>*)cellForIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
/**
刷新
*/
- (void)reloadData;
- (void)reloadDataWithIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath;
@end
以上为核心组件定义了一个规范,为了方便大家实现自己的想法
- ReuseScrollViewIndexInterface :模仿NSIndexPath
- ReuseScrollViewCellInterface : 模仿UITableViewCell
- ReuseScrollViewDataSource :模仿TableViewDataSource的代理方法,基本一致
- ReuseScrollViewDelegate : 同样也是模仿TableViewdelegate的代理方法
- ReuseScrollViewInterface : 这个协议定义了核心组件的方法
当然,小F也提供了一些针对协议封装好的类,下面列出来对应项目中的文件进行参考
- TableIndexPath :标记二维位置
- TowDTableViewCell :cell类,如果需要拓展直接继承UIView类,接受ReuseScrollViewCellInterface协议即可
- ReuseScrollView :核心组件,二维的TableView
- TwoDTableView :根据核心组件组合而来,为了不抑制大家的想法,这里不做过多的介绍,有兴趣的看看源码。在ViewControll.m文件中有应用的例子
征服世界
在ScrollView上添加控件并且使其可以滑动,是一个再简单不过的事情了。但是当控件过多的时候,就会占用大量的内存,所以需要一套控件复用算法,让已经加载好的控件来回反复的使用,这就是核心组件ReuseScrollView的作用。
#import "ReuseScrollView.h"
#import "TableIndexPath.h"
@interface ReuseScrollView()
/** 复用池 */
@property(nonatomic,strong)NSMutableArray<UIView<ReuseScrollViewCellInterface>*>* reuseCellPool;
/** 可视范围内的第一个indexPath */
@property(nonatomic,strong)id<ReuseScrollViewIndexInterface> firstIndexPath;
/** 可视范围内的最后一个indexPath */
@property(nonatomic,strong)id<ReuseScrollViewIndexInterface> lastIndexPath;
@end
@implementation ReuseScrollView{
__weak id<ReuseScrollViewDataSource> _dataSource;
__weak id<ReuseScrollViewDelegate> _delegate;
}
- (void)setDelegate:(id<ReuseScrollViewDelegate>)delegate{
_delegate = delegate;
[super setDelegate:delegate];
}
#pragma mark - 懒加载
- (NSMutableArray<UIView<ReuseScrollViewCellInterface>*> *)reuseCellPool{
if (_reuseCellPool == nil) {
_reuseCellPool = [NSMutableArray new];
}
return _reuseCellPool;
}
#pragma mark - 接口实现
@synthesize dataSource = _dataSource;
@synthesize delegate = _delegate;
- (id<ReuseScrollViewIndexInterface>)createIndexPathWithRow:(NSInteger)row col:(NSInteger)col{
return [[TableIndexPath alloc] initWithRow:row col:col];
}
- (UIView<ReuseScrollViewCellInterface> *)dequeueReusableCellWithIdentifier:(NSString *)identifier{
return [self dequeueReusableCellWithIdentifier:identifier indexPath:nil];
}
- (UIView<ReuseScrollViewCellInterface> *)dequeueReusableCellWithIdentifier:(NSString *)identifier indexPath:(id<ReuseScrollViewIndexInterface>)indexPath{
__block UIView<ReuseScrollViewCellInterface> * target = nil;
//在当前可视范围内的视图中找找看
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj isKindOfClass:[UIImageView class]]) {
UIView<ReuseScrollViewCellInterface>* cell = (UIView<ReuseScrollViewCellInterface>*)obj;
if ([cell.identifier isEqualToString:identifier]) {
if (cell.indexPath.row == indexPath.row && cell.indexPath.col == indexPath.col) {
target = cell;
}
}
}
}];
if (target) {
//找到了
return target;
}
//没有找到,则在复用池中找一个没有被用过的,返回
for (UIView<ReuseScrollViewCellInterface>* cell in self.reuseCellPool) {
if ([cell.identifier isEqualToString:identifier]) {
target = cell;
[self.reuseCellPool removeObject:cell];
break;
}
}
return target;
}
- (void)queueReusableCell:(UIView<ReuseScrollViewCellInterface>*)cell{
cell.indexPath = nil;
cell.tap = nil;
[self.reuseCellPool addObject:cell];
[cell removeFromSuperview];
}
- (UIView<ReuseScrollViewCellInterface> *)cellForIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath{
for (UIView<ReuseScrollViewCellInterface>* cell in self.subviews) {
if (![cell isKindOfClass:[UIImageView class]]) {
if (indexPath.row == cell.indexPath.row && indexPath.col == cell.indexPath.col) {
return cell;
}
}
}
return nil;
}
- (void)reloadData{
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj isKindOfClass:[UIImageView class]]) {
UIView<ReuseScrollViewCellInterface>* cell = (UIView<ReuseScrollViewCellInterface>*)obj;
[self queueReusableCell:cell];
}
}];
[self setNeedsDisplay];
}
- (void)reloadDataWithIndexPath:(id<ReuseScrollViewIndexInterface>)indexPath{
[self.dataSource reuseScrollView:self cellAtIndexPath:indexPath];
}
#pragma mark - 周期
- (void)layoutSubviews{
[super layoutSubviews];
if (self.dataSource == nil) {
return;
}
[self clearUI];
[self loadUI];
}
#pragma mark - 逻辑
//清除不在范围内的视图
- (void)clearUI{
__weak typeof(self)sself = self;
//获取此时,视图的展示范围
CGRect showRect = CGRectZero;
showRect.origin = self.contentOffset;
showRect.size = self.frame.size;
NSInteger allRow = [self.dataSource rowNumberWithReuseScrollView:self];
NSInteger allCol = [self.dataSource colNumberWithReuseScrollView:self];
//记录可视范围内的位置范围
self.lastIndexPath = [self createIndexPathWithRow:0 col:0];
self.firstIndexPath = [self createIndexPathWithRow:allRow col:allCol];
[self.subviews enumerateObjectsUsingBlock:^(UIView<ReuseScrollViewCellInterface> * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//首先,不在范围内的子视图,全部移除,进入复用池等待复用
if (![obj isKindOfClass:[UIImageView class]]) {
if (!CGRectIntersectsRect(showRect, obj.frame)) {
//没有交集
[sself queueReusableCell:obj];
}else{
//找到最小的
if (sself.firstIndexPath.row >= obj.indexPath.row && sself.firstIndexPath.col >= obj.indexPath.col) {
sself.firstIndexPath = [sself createIndexPathWithRow:obj.indexPath.row col:obj.indexPath.col];
}
//找到最大的
if (sself.lastIndexPath.row <= obj.indexPath.row && sself.lastIndexPath.col <= obj.indexPath.col) {
sself.lastIndexPath = [sself createIndexPathWithRow:obj.indexPath.row col:obj.indexPath.col];
}
}
}
}];
}
//加载显示相应范围内的ui
- (void)loadUI{
//获取此时,视图的展示范围
CGRect showRect = CGRectZero;
showRect.origin = self.contentOffset;
showRect.size = self.frame.size;
NSInteger allRow = [self.dataSource rowNumberWithReuseScrollView:self];
NSInteger allCol = [self.dataSource colNumberWithReuseScrollView:self];
//获取最大的cell的size,当做每一个cell的size
CGSize maxSize = CGSizeZero;
if ([self.dataSource respondsToSelector:@selector(maxCellSizeWithReuseScrollView:)]) {
maxSize = [self.dataSource maxCellSizeWithReuseScrollView:self];
}else{
maxSize = CGSizeMake(200, 200);
}
//设置滚动范围
[self setContentSize:CGSizeMake(allCol * maxSize.width,allRow * maxSize.height)];
//计算应该显示的
for (NSInteger row = 0; row < allRow; row ++) {
//先判断这个行,是否在显示范围之内,不是的话,继续下一个循环
/*
row * maxSize.height + self.contentInset.top > self.contentOffset.y + self.frame.size.height
目的:首先这个判断条件是判断视图下边界出现的cell是否满足计算条件,即是否出现在可视范围内
由来:
1.row是行的计算,从0开始,所以row * height 其实是计算第row行的总高度,因为row = 0的情况下也是一行cell,但是高度却是0. 比如row 是3 ,其实应该表示是第四行cell,但是计算的高度却是3 * height。
2.但此时的位置信息标记的是下一个的位置,同上一个例子,如果row是3,这个判断条件就代表,第四行的cell是否超出在可视范围内了。
(row + 1) * maxSize.height + self.contentInset.top < self.contentOffset.y
目的:判断是否超出上边界的可视范围
*/
if ((row * maxSize.height + self.contentInset.top > self.contentOffset.y + self.frame.size.height) || ((row + 1) * maxSize.height + self.contentInset.top < self.contentOffset.y)) {
continue;
}
for (NSInteger col = 0; col < allCol; col ++) {
//同上
if ((col * maxSize.width + self.contentInset.left > self.contentOffset.x + self.frame.size.width) || ((col + 1) * maxSize.width + self.contentInset.left < self.contentOffset.x)) {
continue;
}
//这里需要判断,该位置上是否有视图,如果有,则不必要处理
if (row <= self.lastIndexPath.row && col <= self.lastIndexPath.col && row >= self.firstIndexPath.row && col >= self.firstIndexPath.col) {
//这个是可是范围内都有视图的
continue;
}
//走到这里,就是满足的,安全起见,再次判断是否在可视范围内
CGRect frame = CGRectMake(maxSize.width * col + self.contentInset.left, maxSize.height * row + self.contentInset.top, maxSize.width, maxSize.height);
if (CGRectIntersectsRect(frame, showRect)) {
//可以放置新的cell
id<ReuseScrollViewIndexInterface> indexPath = [self createIndexPathWithRow:row col:col];
UIView<ReuseScrollViewCellInterface>* cell = [self.dataSource reuseScrollView:self cellAtIndexPath:indexPath];
cell.indexPath = indexPath;
cell.frame = frame;
[cell setTap:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cellAction:)]];
[self insertSubview:cell atIndex:0];
}
}
}
}
- (void)cellAction:(UITapGestureRecognizer*)tap{
if(self.delegate){
if ([self.delegate respondsToSelector:@selector(reuseScrollView:selectAtIndexPath:)]) {
UIView<ReuseScrollViewCellInterface>* cell = (UIView<ReuseScrollViewCellInterface>*)tap.view;
[self.delegate reuseScrollView:self selectAtIndexPath:cell.indexPath];
}
}
}
@end
ps:项目依赖Masonry
欢迎大家指点,共同进步。 小F的QQ:928669937