去年項目改版前有個頁面需要一個二級列表,本來想做出一個多級列表demo出來,但是當時因為時間比較趕就沒有施行。最近正好有同學問起來,就抽時間來完成下這個拖了這么久的想法。
先放一下粗略的效果圖(主要提供一下思路,如果使用的話得根據需求自己來進行修改了)和鏈接DEMO鏈接:
當時寫二級列表的時候使用的是UITableView的headView和分組來實現(xiàn)的,但是因為這次想要實現(xiàn)的是一個多級列表,并不確定是多少級,可能是三級、四級甚至是十級(當然移動端不太可能出現(xiàn)這么多級的列表),想來想去使用headView好像都滿足不了這個需求(可能是我的思維局限)。思考了一下覺得不使用headView,只要處理好模型的結構和源數據的管理問題,應該是可以實現(xiàn)的。
下面來說一下大概的思路:
首先創(chuàng)建UITableView必不可少,在不使用headView的情況下數據的結構就顯得尤為重要。
多級列表大概的數據結構應該是這樣的:
由圖中不難看出,每一個節(jié)點都有名字,每一個節(jié)點都有可能會包含若干的子節(jié)點,并且每個節(jié)點會有兩個:狀態(tài)展開或者關閉(當然這是最基本的,可能實際上會需要更多的屬性)。我們根據這些必須屬性來建立model,代碼如下
@interface TableViewModel : NSObject
//數據名稱
@property (copy, nonatomic) NSString *name;
//狀態(tài) YES展開、NO收起
@property (assign, nonatomic) BOOL open;
//子節(jié)點
@property (strong, nonatomic) NSMutableArray *array;
@end
有了最基本的model以后,我們先來構造一組四層的假數據(用遞歸也可以,這里我就偷懶硬寫了四層)。
//構造模擬數據
- (void)initData {
//構造父節(jié)點
for (int i = 0; i<5; i++) {
TableViewModel *model = [[TableViewModel alloc]init];
model.name = [NSString stringWithFormat:@"%i",i];
model.array = [NSMutableArray array];
//構造子節(jié)點
for (int j = 6; j<10; j++) {
TableViewModel *childModel = [[TableViewModel alloc]init];
childModel.name = [NSString stringWithFormat:@" %i",j];
childModel.array = [NSMutableArray array];
//構造孫子節(jié)點
for (int k = 11; k<14; k++) {
TableViewModel *grandsonModel = [[TableViewModel alloc]init];
grandsonModel.name = [NSString stringWithFormat:@" %i",k];
grandsonModel.array = [NSMutableArray array];
//構造曾孫節(jié)點
for (int l = 15; l<17; l++) {
TableViewModel *grandsonSonModel = [[TableViewModel alloc]init];
grandsonSonModel.name = [NSString stringWithFormat:@" %i",l];
//構造曾孫節(jié)點
[grandsonModel.array addObject:grandsonSonModel];
}
//孫子節(jié)點
[childModel.array addObject:grandsonModel];
}
//子節(jié)點
[model.array addObject:childModel];
}
//父節(jié)點
[self.countArray addObject:model];
}
}
構造完成后我們可以得到一個數組,這個數組里面的結構是這樣的:
數據構造完成以后的任務就是顯示了,那么怎么才能把這些數據按照我們要求的形式展現(xiàn)出來呢?
在不使用headView的情況下,tableView基本上就相當于一個分組。那么在控制器中建立一個數組,這個數組用來存儲將要顯示在tableView上的數據。只需管理這個數組中元素的個數與順序就可以實現(xiàn)我們的需求。
接著創(chuàng)建一個可變數組用來管理要顯示的數據。
@property (strong, nonatomic) NSMutableArray *countArray;
可以注意到,在構建數據的時候我首先將父節(jié)點加入到了這個數組中。將這些節(jié)點加入數組中后,將countArray設為UITableView的數據源,此時顯示為父節(jié)點。有了基本的思路,接下來就要開始考慮點擊展開和點擊收起需要怎么實現(xiàn)了。
我們先來考慮點擊展開的問題:
當點擊tableView時,我們需要獲得當前點擊項下所有的未展開節(jié)點、已展開節(jié)點、以及展開節(jié)點的子節(jié)點。因為不曉得該節(jié)點下存在多少層子節(jié)點所以這里需要用到遞歸來取出子節(jié)點(有點像遍歷鏈表的感覺),取出子節(jié)點之后相應的按照順序(這里創(chuàng)建了屬性rowCount來記錄點擊行的下標,文章最后會說明為什么不使用參數傳入。)加入到countArray中,然后刷新tableView將節(jié)點顯示出來。代碼如下:
//展開所有子節(jié)點
- (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];
}
}
}
有了展開,那么接下來就需要寫關閉了。點擊關閉時,同樣的需要將所有的子節(jié)點全部從countArray中刪除,具體方法和思路跟點擊展開基本相同。代碼如下:
//收起所有子節(jié)點
- (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];
}
}
至此,多級列表就完成了。
下面是同學要的另外一個小的附加功能,也順便來介紹下實現(xiàn)思路。
大家應該看到了,剛開始放的圖上面還有一個紅色的浮窗,下面來介紹下這個浮窗的實現(xiàn)思路。
同學給的紅色懸浮框的需求那,是當列表滑動時將將要顯示的并且是展開狀態(tài)分組的根節(jié)點,顯示為懸浮框展示出來。那么首先通過看UITableViewDelegate中的API,發(fā)現(xiàn)里面有兩個可以拿來使用的方法。
//獲取到將要顯示的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。那么我們就需要監(jiān)聽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展開狀態(tài)時,將它顯示出來即可。代碼如下:
TableViewModel *model = [self.countArray objectAtIndex:indexPath.row];
if(model.open) {
self.label.hidden = NO;
self.label.text = model.name
}
當用戶向下滑動時,頂部cell在不斷的顯示出來,我們這個時候需要倒序遍歷模型數組,拿到最近的一個展開狀態(tài)的model,將它顯示出來即可。如果遍歷結束沒有找到展開狀態(tài)的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;
此時,基本已經完工了。但是如果去使用的時候會發(fā)現(xiàn),懸浮框顯示狀態(tài)時,再點擊展開列表,那么懸浮框上的數字會發(fā)生改變。原因是因為每次點擊展開后,我會將數據相應的加入相應的數組然后刷新tableView,這個時候是會進入上面兩個tableView的代理方法的,所以就會出現(xiàn)顯示錯亂的問題。此時,變量state就又起到了作用,只需在每次刷新時重置狀態(tài),并且在相應位置加上判斷即可避免這種情況。同樣的收起tableView的時候,只需在scrollViewDidScroll中進行判斷即可。
如有錯誤歡迎大家指正。