【前言】
在使用華爾街見聞 app 時,看到它的 tableVeiw 上的 cell 具有很好的展開與收縮功能。於是自己想了一下實現,感覺應該挺簡單的,於是心癢癢寫個 demo 實現一波。華爾街見聞 app 上的效果如下:
【本 demo 實現的效果圖】
【思路】
由它的效果圖可以觀察出,cell 上默認顯示文字多於 4 行時省略,點擊時文字全部展現,cell 也同時適應文字的高度。
1. label 行數可以用 numberOfLines 屬性來控制,改變它就可以改變文字的高度。
2. cell 的高度需要變化,但是如果根據文字收縮時算一下高度,展開時又給 cell 一個高度這樣會很麻煩,因為這樣要去精確計算出文字高度,上下間距。所以這里我想到的是利用 tableView 估算 cell 高度的機制,cell 內的文字用 label 的約束將 cell "撐滿"。
3. 用一個 model 對應一個 cell 來防止復用數據錯亂,然后展開與收縮用 tableView 的刷新動畫就好了。
4. 實際實現過程中遇到一些小坑,重點在處理動畫流暢思路上。
【代碼實現】
1. 開啟 tableView 估算機制
- (HomeView *)homeView { if (!_homeView) { _homeView = [[HomeView alloc] init]; _homeView.backgroundColor = [UIColor whiteColor]; _homeView.tableView.dataSource = self; _homeView.tableView.delegate = self; _homeView.tableView.estimatedRowHeight = 80; [_homeView.tableView registerClass:[HomeCell class] forCellReuseIdentifier:NSStringFromClass([HomeCell class])]; [self.view addSubview:_homeView]; } return _homeView; }
2. label 默認 numberOfLines 設置
- (UILabel *)contentL { if (!_contentL) { _contentL = [[UILabel alloc] init]; _contentL.frame = CGRectMake(0, 0, self.contentView.bounds.size.width, 0); _contentL.lineBreakMode = NSLineBreakByTruncatingTail; _contentL.numberOfLines = 4; [self.contentView addSubview:_contentL]; } return _contentL; }
3. cell 數據設置 (通過 model 內 isOpen 來防止 cell 復用數據發生錯亂)
- (void)setModel:(HomeModel *)model { _model = model; self.contentL.text = model.title; if (model.isOpen) { self.contentL.numberOfLines = NSIntegerMax; } else { self.contentL.numberOfLines = 4; } }
4. 點擊時 tableView 動畫刷新
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { HomeCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.model.isOpen = !cell.model.isOpen; [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; }
【細節優化】
上面的實現看似美好且不費力,但實際多滑兩下,多點一些不同高度的 cell,會發現它的展開動畫明顯不流暢,會出現一點抖動狀況。如下:
看到這里,展開和收縮動畫在這個 cell 上簡直抽風了,有種突然截斷了的感覺。這是肯定不能忍的。
優化分析:
tableView 動畫不會有問題,是很正常的刷新對應的 cell。唯一可能出問題的就是 cell 的估算機制。因為 cell 我們默認給的高度是 80,所以當 cell 實際高度與默認高度相差太大時,就會出現這種問題了。
解決方案:
我們還是采用 cell 高度的估算機制,但要讓系統盡可能估計得准確,減少它調整高度帶來的動畫不流暢。那就是利用 tableView 返回估算高度的代理方法,我們先給它算好一個高度,並且這個高度肯定基本准確。(注意: 提前在拿到 model 數據時就算好 cell 高度存放在 model 中,而不是在 tableView 代理方法中去計算 cell 高度,因為代理方法調用太頻繁,那里做一些簡單取值操作就好,不然當數據量大時影響 tableView 滑動性能。)
1. 在 model 獲取數據時就算好 cell 數據的高度。在 HomeModel 加一個 cellHeight 屬性,並重寫 setTitle: 方法。如下:
- (void)setTitle:(NSString *)title { _title = title; CGFloat w = [UIScreen mainScreen].bounds.size.width - 24; CGFloat h = MAXFLOAT; NSDictionary *dict = @{NSFontAttributeName: [UIFont systemFontOfSize:16]}; CGFloat height = [title boundingRectWithSize:CGSizeMake(w, h) options:NSStringDrawingUsesLineFragmentOrigin attributes:dict context:nil].size.height; _cellHeight = height + 24; }
2. 在控制器中加上 tableView 返回估算高度的代理方法設置如下:
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { HomeModel *model = self.testDataArray[indexPath.row]; return model.cellHeight; }
這樣再運行,無論怎么滑怎么點,都十分完美。😀
注意: tableView 返回估算高度方法里雖然我們返回了准確的高度,但 cell 最終的高度並不是以估算高度為准,而是取決於 cell 內約束,即 cell 是被約束撐滿的。
所以 cell 的實際高度取決於設置數據時的里面 label.numberOfLines。這里返回的高度讓系統減少估算的調整,所以這樣動畫就流暢了。
【demo地址】
碼雲: https://gitee.com/LiJinShi/SpreadCellDemo