tableview 多层展开与收起demo

规则要求:

  • tableview 有多层,类似于xcode文件目录的层级关系,每一个最开始展示的层姑且称之为根目录吧,并且,每个根目录下的层数不定。
  • 与文件目录类似,每个目录下可以有不同层级的目录同时展开,但是同一层次中只有一层是展开的,即要展开B层次的某一层,则需要收起B层次所有其他的层级。
  • 最底层是一个个文件,不能再展开(这里在业务逻辑上用处是:跳转到不同的页面)。

想法:

  • 整个界面是一个tableview,层级关系用cell中的label的位置展现,而tableview的数据源是一个一维数组_resultArray,其中,展开是在特定位置插入数据,收起是在特定位置删除数据。
  • 每一层目录存储着下一层的引用,就是包含了下一层的全部数据。展开该层的时候就是将下一层的数据加入_resultArray,收起该层时,是将该层的所有下层的数据从_resultArray中删除。

数据存储

//每个目录的数据结构如下:
@interface OpenTest : NSObject
@property (copy, nonatomic) NSString *title;    //非首层展示的标题
@property (assign, nonatomic) NSInteger level;  //决定偏移量大小
@property (copy, nonatomic) NSString *openUrl;  //最后一层跳转的规则
@property (copy, nonatomic) NSMutableArray *detailArray; //下一层的数据
@property (assign, nonatomic) BOOL isOpen;        //是否要展开
@property (copy, nonatomic) NSString *imageName;  //首层的图片
@end

其中,因为detailArray中存储的对象也应该是OpenTest, 所以需要在OpenTest.m中借助MJExtension (在github上下载并加入到项目中)进行显式转化。

#import "OpenTest.h"
@implementation OpenTest
- (instancetype)init {
    self = [super init];
    if (self) {
        [OpenTest mj_setupObjectClassInArray:^NSDictionary *{
            return @{
                     @"detailArray" : [OpenTest class]
                     };
        }];
    }
    return self;
}
@end

数据处理

开始建造源数据dataArray,该数组可明确层级关系,并且得到展示数组_resultArray。建造过程如下:

- (void)initData {
_dataArray = [NSMutableArray new];
_resultArray = [NSMutableArray new];

NSMutableArray *secondArray1 = [NSMutableArray new];
NSMutableArray *threeArray1 = [NSMutableArray new];
NSMutableArray *fourArray1 = [NSMutableArray new];

NSArray *FirstTitleArray = @[@"FirstTitle1", @"FirstTitle2", @"FirstTitle3", @"FirstTitle4", @"FirstTitle5", @"FirstTitle6", @"FirstTitle7", @"FirstTitle8", @"FirstTitle9", @"FirstTitle10"];
NSArray *SecondTitleArray = @[@"SecondTitle1", @"SecondTitle2", @"SecondTitle3"];
NSArray *ThreeTitleArray = @[@"ThreeTitle1", @"ThreeTitle2", @"ThreeTitle3", @"ThreeTitle4"];
NSArray *FourTitleArray = @[@"FourTitle1", @"FourTitle2", @"FourTitle3"];
NSArray *imageArray = @[@"scroller1", @"scroller2", @"scroller3", @"scroller4", @"scroller5", @"scroller6", @"scroller7", @"scroller8", @"scroller9", @"scroller10"];

//第四层数据
for (int i = 0; i < FourTitleArray.count; i++) {
    OpenTest *model = [[OpenTest alloc] init];
    model.title = FourTitleArray[i];
    model.level = 3;
    model.isOpen = NO;
    
    [fourArray1 addObject:model];
}

//第三层数据
for (int i = 0; i < ThreeTitleArray.count; i++) {
    OpenTest *model = [[OpenTest alloc] init];
    model.title = ThreeTitleArray[i];
    model.level = 2;
    model.isOpen = NO;
    model.detailArray = fourArray1;
    
    [threeArray1 addObject:model];
}

//第二层数据
for (int i = 0; i < SecondTitleArray.count; i++) {
    OpenTest *model = [[OpenTest alloc] init];
    model.title = SecondTitleArray[i];
    model.level = 1;
    model.isOpen = NO;
    model.detailArray = [threeArray1 mutableCopy];
    
    [secondArray1 addObject:model];
}

//第一层数据
for (int i = 0; i < FirstTitleArray.count; i++) {
    OpenTest *model = [[OpenTest alloc] init];
    model.title = FirstTitleArray[i];
    model.level = 0;
    model.isOpen = NO;
    model.detailArray = [secondArray1 mutableCopy];
    model.imageName = imageArray[i];
    
    [_dataArray addObject:model];
}

//处理源数据,获得展示数组_resultArray
[self dealWithDataArray:_dataArray];
}

/**
 将源数据数组处理成要展示的一维数组,最开始是展示首层的所有的数据
 @param dataArray 源数据数组
 */
- (void)dealWithDataArray:(NSMutableArray *)dataArray {
for (OpenTest *model in dataArray) {
    [_resultArray addObject:model];

    if (model.isOpen && model.detailArray.count > 0) {
        [self dealWithDataArray:model.detailArray];
    }
}
}

当首层没有展开数据时,点击首层展开第二层数据,比较容易实现,即在_resultArray添加下一层数据。添加数据方法如下:

/**
 在指定位置插入要展示的数据
 @param dataArray 数据数组
 @param row       需要插入的数组下标
 */
- (void)addObjectWithDataArray:(NSMutableArray *)dataArray row:(NSInteger)row {
for (int i = 0; i < dataArray.count; i++) {
    OpenTest *model = dataArray[i];
    model.isOpen = NO;
    [_resultArray insertObject:model atIndex:row];
    row += 1;
}
}

收起方法实现如下:

/**
 删除要收起的数据
 @param dataArray 数据
 @param count     统计删除数据的个数
 @return 删除数据的个数
 */
- (CGFloat)deleteObjectWithDataArray:(NSMutableArray *)dataArray count:(NSInteger)count {
for (OpenTest *model in dataArray) {
    count += 1;
    
    if (model.isOpen && model.detailArray.count > 0) {
        count = [self deleteObjectWithDataArray:model.detailArray count:count];
    }

    model.isOpen = NO;
    
    [_resultArray removeObject:model];
}

return count;
}

在已经展开的时候点击另外一个目录,要先收起再展开。因为每个层次只有一个目录是展开的,所以收起的时候,只需要跟同层次的目录数据比较,如果是已经打开的,则删除打开目录的所有子层。方法如下:

/**
 与点击同一层的数据比较,然后删除要收起的数据和插入要展开的数据
 @param model 点击的cell对应的model
 @param row   点击的在tableview的indexPath.row,也对应_resultArray的下标
 */
- (void)compareSameLevelWithModel:(OpenTest *)model row:(NSInteger)row {
NSInteger count = 0;
NSInteger index = 0;    //需要收起的起始位置
//如果直接用_resultArray,在for循环为完成之前,_resultArray会发生改变,使程序崩溃。
NSMutableArray *copyArray = [_resultArray mutableCopy];

for (int i = 0; i < copyArray.count; i++) {
    OpenTest *openModel = copyArray[i];
    if (openModel.level == model.level) {
        //同一个层次的比较
        if (openModel.isOpen) {
            //删除openModel所有的下一层
            count = [self deleteObjectWithDataArray:openModel.detailArray count:count];
            index = i;
            openModel.isOpen = NO;
            break;
        }
    }
}

//插入的位置在删除的位置的后面,则需要减去删除的数量。
if (row > index && row > count) {
    row -= count;
}

[self addObjectWithDataArray:model.detailArray row:row + 1];
}

界面

系统的tableviewcell 无法修改textLabel的位置,需要修改偏移量有两种方法:
1、继承UITableViewCell, 然后重写父类的方法 - layoutSubviews, 在该方法中修改textLabel的frame。
2、在cell.contentView 中添加一个label。

我这里使用的是第二种方法:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(15, 0, UI_SCREEN_WIDTH / 2, 32)];
        label.font = [UIFont systemFontOfSize:14];
        label.tag = LabelTag;

        [cell.contentView addSubview:label];
    }
    
    for (UIView *view in cell.contentView.subviews) {
        if (view.tag == LabelTag) {
            ((UILabel *)view).text = model.title;
            ((UILabel *)view).frame = CGRectMake(15 + (model.level - 1) * SpaceWidth , 0, UI_SCREEN_WIDTH / 2, 32);
        }
    }

最后在didSelectRowAtIndexPath方法中实现点击展开与收起,方法如下:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
OpenTest *model = _resultArray[row];

if (model.isOpen) {
    //原来是展开的,现在要收起,则删除model.detailArray存储的数据
    [self deleteObjectWithDataArray:model.detailArray count:0];
}
else {
    if (model.detailArray.count > 0) {
        //原来是收起的,现在要展开,则需要将同层次展开的收起,然后再展开
        [self compareSameLevelWithModel:model row:row];
    }
    else {
        //点击的是最后一层数据,跳转到别的界面
        NSLog(@"最后一层");
    }
}

model.isOpen = !model.isOpen;

//滑动到屏幕顶部
for (int i = 0; i < _resultArray.count; i++) {
    OpenTest *openModel = _resultArray[i];
    
    if (openModel.isOpen && openModel.level == 0) {
        //将点击的cell滑动到屏幕顶部
        NSIndexPath *selectedPath = [NSIndexPath indexPathForRow:i inSection:0];
        [tableView scrollToRowAtIndexPath:selectedPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
    }
}

[tableView reloadData];
}

效果

demo.png

将demo放到了百度云,喜欢的可以下载。
https://pan.baidu.com/s/1dFBrh9v

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容