二维列表的实现

前言

小F我最近遇到了一个小需求,实现效果如下:
效果图

当然这不是静态的,需求如下:
  • 当视图上下滑动时,左边的视图跟着上下滑动,反之亦然
  • 当视图左右滑动时,上边的视图跟着左右滑动,反之亦然
  • 当主视图在两个方向上同时滑动,上面和左面的视图也要同时联动
  • 每个区域可以响应点击事件
  • 每个这样的区域展示的view可以自定制
  • 当然还有最基本的展示数据
需求大致就这么多。what? fuck! mmp! 小f到这里已经崩溃了。
让小f崩溃的不是界面的复杂度,而是要模仿系统控件UITableView的复用机制。

分析

行了,废话不多说了,下面开始说正事儿。

  1. 小F这里把整个界面分为三个部分,上、左和中间部分
  2. 为了提高复用性,这个三个部分都是用了同一个基础控件
  3. 把相对比较复杂的中间视图拿出来单独封装

准备

先附上项目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
以上为核心组件定义了一个规范,为了方便大家实现自己的想法
  1. ReuseScrollViewIndexInterface :模仿NSIndexPath
  2. ReuseScrollViewCellInterface : 模仿UITableViewCell
  3. ReuseScrollViewDataSource :模仿TableViewDataSource的代理方法,基本一致
  4. ReuseScrollViewDelegate : 同样也是模仿TableViewdelegate的代理方法
  5. ReuseScrollViewInterface : 这个协议定义了核心组件的方法
当然,小F也提供了一些针对协议封装好的类,下面列出来对应项目中的文件进行参考
  1. TableIndexPath :标记二维位置
  2. TowDTableViewCell :cell类,如果需要拓展直接继承UIView类,接受ReuseScrollViewCellInterface协议即可
  3. ReuseScrollView :核心组件,二维的TableView
  4. 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值