去年项目改版前有个页面需要一个二级列表,本来想做出一个多级列表demo出来,但是当时因为时间比较赶就没有施行。最近正好有同学问起来,就抽时间来完成下这个拖了这么久的想法。
先放一下粗略的效果图(主要提供一下思路,如果使用的话得根据需求自己来进行修改了)和链接DEMO链接:
当时写二级列表的时候使用的是UITableView的headView和分组来实现的,但是因为这次想要实现的是一个多级列表,并不确定是多少级,可能是三级、四级甚至是十级(当然移动端不太可能出现这么多级的列表),想来想去使用headView好像都满足不了这个需求(可能是我的思维局限)。思考了一下觉得不使用headView,只要处理好模型的结构和源数据的管理问题,应该是可以实现的。
下面来说一下大概的思路:
首先创建UITableView必不可少,在不使用headView的情况下数据的结构就显得尤为重要。
多级列表大概的数据结构应该是这样的:
由图中不难看出,每一个节点都有名字,每一个节点都有可能会包含若干的子节点,并且每个节点会有两个:状态展开或者关闭(当然这是最基本的,可能实际上会需要更多的属性)。我们根据这些必须属性来建立model,代码如下
@interface TableViewModel : NSObject
//数据名称
@property (copy, nonatomic) NSString *name;
//状态 YES展开、NO收起
@property (assign, nonatomic) BOOL open;
//子节点
@property (strong, nonatomic) NSMutableArray *array;
@end
有了最基本的model以后,我们先来构造一组四层的假数据(用递归也可以,这里我就偷懒硬写了四层)。
//构造模拟数据
- (void)initData {
//构造父节点
for (int i = 0; i<5; i++) {
TableViewModel *model = [[TableViewModel alloc]init];
model.name = [NSString stringWithFormat:@"%i",i];
model.array = [NSMutableArray array];
//构造子节点
for (int j = 6; j<10; j++) {
TableViewModel *childModel = [[TableViewModel alloc]init];
childModel.name = [NSString stringWithFormat:@" %i",j];
childModel.array = [NSMutableArray array];
//构造孙子节点
for (int k = 11; k<14; k++) {
TableViewModel *grandsonModel = [[TableViewModel alloc]init];
grandsonModel.name = [NSString stringWithFormat:@" %i",k];
grandsonModel.array = [NSMutableArray array];
//构造曾孙节点
for (int l = 15; l<17; l++) {
TableViewModel *grandsonSonModel = [[TableViewModel alloc]init];
grandsonSonModel.name = [NSString stringWithFormat:@" %i",l];
//构造曾孙节点
[grandsonModel.array addObject:grandsonSonModel];
}
//孙子节点
[childModel.array addObject:grandsonModel];
}
//子节点
[model.array addObject:childModel];
}
//父节点
[self.countArray addObject:model];
}
}
构造完成后我们可以得到一个数组,这个数组里面的结构是这样的:
数据构造完成以后的任务就是显示了,那么怎么才能把这些数据按照我们要求的形式展现出来呢?
在不使用headView的情况下,tableView基本上就相当于一个分组。那么在控制器中建立一个数组,这个数组用来存储将要显示在tableView上的数据。只需管理这个数组中元素的个数与顺序就可以实现我们的需求。
接着创建一个可变数组用来管理要显示的数据。
@property (strong, nonatomic) NSMutableArray *countArray;
可以注意到,在构建数据的时候我首先将父节点加入到了这个数组中。将这些节点加入数组中后,将countArray设为UITableView的数据源,此时显示为父节点。有了基本的思路,接下来就要开始考虑点击展开和点击收起需要怎么实现了。
我们先来考虑点击展开的问题:
当点击tableView时,我们需要获得当前点击项下所有的未展开节点、已展开节点、以及展开节点的子节点。因为不晓得该节点下存在多少层子节点所以这里需要用到递归来取出子节点(有点像遍历链表的感觉),取出子节点之后相应的按照顺序(这里创建了属性rowCount来记录点击行的下标,文章最后会说明为什么不使用参数传入。)加入到countArray中,然后刷新tableView将节点显示出来。代码如下:
//展开所有子节点
- (void)insertData:(NSMutableArray *)array {
for (int i = 0; i<array.count; i++) {
TableViewModel *model = [array objectAtIndex:i];
self.rowCount++;
[self.countArray insertObject:model atIndex:self.rowCount];
if (model.array && model.open) {
[self insertData:model.array];
}
}
}
有了展开,那么接下来就需要写关闭了。点击关闭时,同样的需要将所有的子节点全部从countArray中删除,具体方法和思路跟点击展开基本相同。代码如下:
//收起所有子节点
- (void)deleteData:(NSMutableArray *)array {
for (int i = 0; i<array.count; i++) {
TableViewModel *model = [array objectAtIndex:i];
if (model.array) {
[self deleteData:model.array];
}
[self.countArray removeObject:model];
}
}
至此,多级列表就完成了。
下面是同学要的另外一个小的附加功能,也顺便来介绍下实现思路。
大家应该看到了,刚开始放的图上面还有一个红色的浮窗,下面来介绍下这个浮窗的实现思路。
同学给的红色悬浮框的需求那,是当列表滑动时将将要显示的并且是展开状态分组的根节点,显示为悬浮框展示出来。那么首先通过看UITableViewDelegate中的API,发现里面有两个可以拿来使用的方法。
//获取到将要显示的cell
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath ;
//获取到消失的cell
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath NS_AVAILABLE_IOS(6_0);
根据需求我们只需要屏幕上部cell的显示和消失情况,那么可以知道当用户向上滑动时,我们应该获取的为将要消失的cell;当用户将要向下滑动时,我们需要获取将要显示的cell。那么我们就需要监听tableView的滑动方向。
方法如下
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
//如果当前位移大于缓存位移,说明scrollView向上滑动
//state 0初始 1下 2上
if (scrollView.contentOffset.y > oldY) {
state = 2;
}else{
state = 1;
}
//将当前位移变成缓存位移
oldY = scrollView.contentOffset.y;
//当用户滑至顶部时,需要隐藏悬浮框
if (scrollView.contentOffset.y <= 0) {
self.label.hidden = YES;
}
}
当用户向上滑动时,顶部cell在不断消失,我们只需拿到消失的cell所对应的model,当model展开状态时,将它显示出来即可。代码如下:
TableViewModel *model = [self.countArray objectAtIndex:indexPath.row];
if(model.open) {
self.label.hidden = NO;
self.label.text = model.name
}
当用户向下滑动时,顶部cell在不断的显示出来,我们这个时候需要倒序遍历模型数组,拿到最近的一个展开状态的model,将它显示出来即可。如果遍历结束没有找到展开状态的model,那么将悬浮框隐藏。代码如下:
for (NSInteger i = indexPath.row ; i>=0; i--) {
TableViewModel *model = [self.countArray objectAtIndex:i];
if (model.open) {
self.label.hidden = NO;
self.label.text = model.name;
return;
}
}
self.label.hidden = YES;
此时,基本已经完工了。但是如果去使用的时候会发现,悬浮框显示状态时,再点击展开列表,那么悬浮框上的数字会发生改变。原因是因为每次点击展开后,我会将数据相应的加入相应的数组然后刷新tableView,这个时候是会进入上面两个tableView的代理方法的,所以就会出现显示错乱的问题。此时,变量state就又起到了作用,只需在每次刷新时重置状态,并且在相应位置加上判断即可避免这种情况。同样的收起tableView的时候,只需在scrollViewDidScroll中进行判断即可。
如有错误欢迎大家指正。