这两天项目比较闲,在空余之际,尝试自己实现列表控件。从动工到初步完成大概花了一天时间,目前实现了列表的简单功能,后续将考虑加入cell重用机制、惯性特征以及删除cell等功能。项目代码已经放到了github上,地址:https://github.com/wanglichun/CustomTableView。
在实现之前,需要了解列表控件的运行原理,我之前的一篇博客《列表控件实现原理解析》中有介绍。去年由于项目需要,使用lua语言自定义过双重列表(大列表嵌套小列表),这次改用objc实现,实现的思路和以前相同,只是换了个语言,因此速度比较快。下面分步骤介绍实现过程。
总体目录结构:
目录结构说明:
1、TablewView目录:CSTableView(CustomTableView的缩写)为自定义的列表控件,CSTableViewCell为列表的单元格;
2、RootViewController中包含了一个CSTableView的测试例子。
步骤一:列表单元格CSTableViewCell实现
为了节省时间,我采用xib的方式构建了cell,并且cell中只包含了标题信息,用户如果想实现复杂的cell,可以继承CSTableViewCell扩展cell。需要注意的是,cell这一层并不处理触摸事件,而是传递给父控件CSTableView处理。
代码如下:
头文件:
#import <UIKit/UIKit.h> @interface CSTableViewCell : UIView + (CSTableViewCell *)csTableViewCell; - (void)setTitle:(NSString *)title; @end
实现文件:
#import "CSTableViewCell.h" @interface CSTableViewCell() @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @end @implementation CSTableViewCell + (CSTableViewCell *)csTableViewCell { NSArray *views = [[NSBundle mainBundle] loadNibNamed:@"CSTableViewCell" owner:nil options:nil]; return views[0]; } //将事件传递给父控件CSTableView处理 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return NO; } - (void)setTitle:(NSString *)title { self.titleLabel.text = title; }
步骤二:列表控件CSTableView实现
关键步骤,如果对列表的运行原理非常了解,下面的代码比较容易理解。需要注意,目前只是简单的采用销毁创建形式实现列表,未采用cell重用机制,后续有时间补上。DataSouce和Delegate参照UITableViewDataSouce和UITableViewDelegate定义,目前只是定义了几个典型的方法。
头文件:
#import <UIKit/UIKit.h> @class CSTableView; @class CSTableViewCell; @protocol CSTableViewDataSource<NSObject> @required - (NSInteger)tableView:(CSTableView *)tableView numberOfRowsInSection:(NSInteger)section; - (CSTableViewCell *)tableView:(CSTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; @end @protocol CSTableViewDataDelegate <NSObject> @optional - (CGFloat)tableView:(CSTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; @end @interface CSTableView : UIView @property (nonatomic,assign)id<CSTableViewDataSource> dataSource; @property (nonatomic,assign)id<CSTableViewDataDelegate> delegate; + (CSTableView *)csTableView; - (void)reloadData; @end
实现文件:
#import "CSTableView.h" #import "CSTableViewCell.h" static const CGFloat kDefaultCellHeight = 27.0f; @interface CSTableView() { CGPoint _touchBeginPoint; CGPoint _touchMovePoint; CGPoint _touchEndPoint; NSInteger _numberOfRows; CGFloat _top; NSInteger _startIndex; NSInteger _endIndex; CGFloat _startY; CGFloat _dataTotalHeight; //CSTableView的逻辑高度 } @end @implementation CSTableView + (CSTableView *)csTableView { NSArray *views = [[NSBundle mainBundle] loadNibNamed:@"CSTableView" owner:nil options:nil]; return views[0]; } - (void)awakeFromNib { //不能放在这里,此时delegate和dataSource都为nil,之前犯错误了,后面移到了layoutSubviews //[self reloadData]; } - (void)layoutSubviews{ [self reloadData]; } #pragma mark -- touches events - (void)updateUIWithMoveDist:(CGFloat)moveDist { //调控速度 moveDist = 0.2 * moveDist; _top += moveDist; if (_top < 0) { _top = 0; } if (_dataTotalHeight > self.frame.size.height) { if (_top > _dataTotalHeight - self.frame.size.height) { _top = _dataTotalHeight - self.frame.size.height; } [self updateUI]; } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch=[touches anyObject]; _touchBeginPoint = [touch locationInView:self]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch=[touches anyObject]; _touchEndPoint = [touch locationInView:self]; CGFloat moveDist = _touchBeginPoint.y - _touchEndPoint.y; [self updateUIWithMoveDist:moveDist]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch=[touches anyObject]; CGPoint point = [touch locationInView:self]; CGFloat moveDist = _touchBeginPoint.y - point.y; [self updateUIWithMoveDist:moveDist]; } #pragma mark -- reloadData //计算显示范围 - (void)calculateStartIndexAndEndIndex { _startIndex = -1; CGFloat totalHeight = 0; if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { for (NSInteger i = 0; i < _numberOfRows; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1]; CGFloat cellHeight = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; totalHeight += cellHeight; if (totalHeight > _top && _startIndex == -1) { _startY = (totalHeight - cellHeight) - _top; _startIndex = i; } if (totalHeight > _top + self.frame.size.height || i == _numberOfRows - 1) { _endIndex = i; break; } } }else{ for (NSInteger i = 0; i < _numberOfRows; i++) { totalHeight += kDefaultCellHeight; if (totalHeight > _top && _startIndex == -1) { _startY = (totalHeight - kDefaultCellHeight) - _top; _startIndex = i; } if (totalHeight > _top + self.frame.size.height || i == _numberOfRows - 1) { _endIndex = i; break; } } } } //更新UI - (void)updateUI { [self calculateStartIndexAndEndIndex]; NSLog(@"startIndex = %ld", _startIndex); NSLog(@"endIndex = %ld", _endIndex); CGFloat totalHeight = 0.0f; for (NSInteger i = _startIndex; i <= _endIndex; i++) { //创建cell,如果用户没有自定义cell,则生成一个默认的cell CSTableViewCell *cell; if ([self.dataSource respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1]; cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath]; }else{ cell = [CSTableViewCell csTableViewCell]; [cell setTitle:@"默认的cell"]; } //设置cell的位置 if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1]; CGFloat cellHeight = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; totalHeight += cellHeight; cell.frame = CGRectMake(0, (totalHeight - cellHeight) + _startY, cell.frame.size.width, cell.frame.size.height); }else{ cell.frame = CGRectMake(0, (i - _startIndex) *kDefaultCellHeight + _startY, cell.frame.size.width, cell.frame.size.height); } //将cell添加到CSTabelView [self addSubview:cell]; } } //重新加载数据, 暂时没有采用cell重用机制,后面有时间再加上 //删除数据后,调用该函数,重新加载UI - (void)reloadData { for (UIView *subView in self.subviews) { [subView removeFromSuperview]; } if ([self.dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) { _numberOfRows = [self.dataSource tableView:self numberOfRowsInSection:1]; }else{ _numberOfRows = 10; //默认显示10条数据 } _dataTotalHeight = 0.0f; if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { for (NSInteger i = 0; i < _numberOfRows; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1]; CGFloat cellHeight = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; _dataTotalHeight += cellHeight; } }else{ _dataTotalHeight = kDefaultCellHeight * _numberOfRows; } //更新UI [self updateUI]; } @end
步骤三:测试用例
#import "RootViewController.h" #import "CSTableView.h" #import "CSTableViewCell.h" @interface RootViewController ()<CSTableViewDataSource, CSTableViewDataDelegate> @property (nonatomic, strong)CSTableView *csTableView; @end @implementation RootViewController - (void)viewDidLoad { [super viewDidLoad]; self.csTableView = [CSTableView csTableView]; self.csTableView.dataSource = self; self.csTableView.delegate = self; self.csTableView.frame = CGRectMake(0, 0, self.csTableView.frame.size.width, self.csTableView.frame.size.height); [self.view addSubview:self.csTableView]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } #pragma mark -- CSTableViewDataSource - (NSInteger)tableView:(CSTableView *)tableView numberOfRowsInSection:(NSInteger)section { return 50; } - (CSTableViewCell *)tableView:(CSTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CSTableViewCell *cell = [CSTableViewCell csTableViewCell]; NSString *title = [[NSString alloc] initWithFormat:@"测试数据%ld", indexPath.row]; [cell setTitle:title]; return cell; } #pragma mark -- CSTableViewDataDelegate - (CGFloat)tableView:(CSTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row < 10) { return 20; }else if (indexPath.row >= 10 && indexPath.row < 20){ return 30; }else{ return 40; } //return 27; } @end