iOS开发--自定义列表控件

这两天项目比较闲,在空余之际,尝试自己实现列表控件。从动工到初步完成大概花了一天时间,目前实现了列表的简单功能,后续将考虑加入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
时间: 2024-08-25 12:44:01

iOS开发--自定义列表控件的相关文章

IOS开发自定义CheckBox控件

IOS本身没有系统的CheckBox组件,但是实际开发中会经常用到,所以专门写了一个CheckBox控件,直接上代码 效果图: UICheckBoxButton.h文件如下: #import #import "Common.h" @interface UICheckBoxButton : UIControl { UILabel *label; UIImageView *icon; BOOL checked; id delegate; } @property (retain, nonat

IOS开发--自定义segment控件,方便自定义样式

系统的segment控件太封闭,想换个颜色加个背景太难了,忍不住自己写一个,以备不时之需 这个控件给出了很多自定义属性的设置,用起来还是比较方便的,需要注意的 itemWidth如果不设置,则会按照控件的宽度平均分配每一项的宽度,如果设置了,那么总宽度超过控件宽度后会有滑动效果 直接上代码吧: 头文件: #import <Foundation/Foundation.h> @protocol WCSegmentControlDelegate -(void)wcSegmentControlSele

自定义列表控件

离上一次写文章已经有好几个月了,工作后比较忙碌,没有多少时间来写,想想几个月前还在学校每天睡到自然醒,是多么幸福的事,还在学校里的少年,珍惜吧! 今天是国庆放假前夕,好多同事提前请假回家了,旁边的格位都空荡荡,按部分老同事的话说:"这好比过年前的景象".没心情写代码,没心情改BUG,过来写篇文章也是好的. 闲聊了几句,进入正题.列表控件无论是在PC客户端程序.网页还是android APP都经常用到,其重要性就不在此强调了.我们先来说一下列表控件的构成: 上图是360云盘的截图,从图中

iOS开发 - 最常用控件 UITableView详解

UITableView掌握点 设置UITableView的dataSource.delegate UITableView多组数据和单组数据的展示 UITableViewCell的常见属性 UITableView的性能优化(cell的循环利用) 自定义Cell 如何展示数据 UITableView需要一个数据源(dataSource)来显示数据 UITableView会向数据源查询一共有多少行数据以及每一行显示什么数据等 没有设置数据源的UITableView只是个空壳 凡是遵守UITableVi

IOS开发基础常用控件简介

在IOS开发中,各类控件完美的解决了开发过程中界面与交互展现的问题,使得IOS产品界面更加灵活实用,IOS常用控件的介绍如下. 1.窗口 UIWindow iPhone的规则是一个窗口,多个视图,窗口是你在app显示出来你看到的最底层,他是固定不变的,基本上可以不怎么理会,但要知道每层是怎样的架构. 2.视图 UIView, 是用户构建界面的基础,所有的控件都是在这个页面上画出来的,你可以把它当成是一个画布,你可以通过UIView增加控件,并利用控件和用户进行交互和传递数据. 窗口和视图是最基本

iOS 开发 ZFUI framework控件,使布局更简单

来自:http://www.jianshu.com/p/bcf86b170d9c 前言 为什么会写这个?因为在iOS开发中,界面的布局一直没有Android布局有那么多的方法和优势,我个人开发都是纯代码,Masonry这个框架我在开发中也是不用的,一个是代码布局的时候,代码量比较多,另外好像在iOS10 布局有问题,网上也有些解决的方法了. 所以就想能自定义一些UI控件,使布局更加简单 实现思路 可以像Android的wrap_content一样,是UILabel 可以根据内容来展示控件的宽高

iOS开发基础-UITableView控件简单介绍

 UITableView 继承自 UIScrollView ,用于实现表格数据展示,支持垂直滚动.  UITableView 需要一个数据源来显示数据,并向数据源查询一共有多少行数据以及每一行显示什么内容等.凡是遵守 UITableViewDataSource 协议的Objc对象,都可以是 UITableView 的数据源.  - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView  返回共有多少组数据.  - (NSI

iOS开发导航栏控件的作用

一,在iOS开发过程中针对一些导航栏上需要自定义视图的情况,有时候需要用系统自带的处理,有些时候需要自定义一些视图并把视图添加上去,这时候主要是它们的位置有些许差别,下面简单写下demo: 1,用导航栏系统自带的视图处理: 1 //1 中间的图片 2 UIImageView *imageBarView = [[UIImageView alloc] initWithFrame:CGRectMake(kScreenWidth / 2.f - 40.f, 20.f, 80, 30)]; 3 image

iOS开发中UIDatePicker控件的使用方法简介

iOS上的选择时间日期的控件是这样的,左边是时间和日期混合,右边是单纯的日期模式. 您可以选择自己需要的模式,Time, Date,Date and Time  , Count Down Timer四种模式. 本篇文章简单介绍下PickerDate控件的使用1.新建一个Singe View Application,命名为DatePickDemo,其他设置如图 2.放置控件打开ViewController.xib,拖拽一个DatePicker控件放到界面上,再拖拽一个Button控件放到界面上,双