首先要说明一下,这里的自定义列表控件,并不是我们平常所指的通过自定义cell达到不同的效果的UITableView,而是指完全从零开始,自己重新实现一个类似与UITableView的列表控件。不得不承认,这是在重复发明轮子,而且发明的轮子还没已有的好,但是通过这一实现过程,我们可以深入到列表实现的内部,摸清UITableView、UITableViewDelegate、UITableViewDataSource以及UITableViewCell之间的关系。
从动工到初步完成大概花了一天时间,目前实现了列表的简单功能,后续将考虑加入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