TableViewCell的收縮展開與動(dòng)態(tài)高度

關(guān)于TableViewCell的收縮展開與動(dòng)態(tài)高度的方法網(wǎng)上有很多,但本文主要運(yùn)用AutoLayout和更改Constraint priority的方式來(lái)實(shí)現(xiàn),另外包括了StackView動(dòng)態(tài)插入在TableViewCell里的運(yùn)用。

Cell收縮與展開

這是在實(shí)際項(xiàng)目中遇到一個(gè)需求。展示一個(gè)表格,每一行為一個(gè)項(xiàng)目的匯總行,點(diǎn)擊表格每一行時(shí)展開一系列細(xì)節(jié)小行,在此點(diǎn)擊收起該行,僅展示匯總行。如下圖所示。

Paste_Image.png

Paste_Image.png

Paste_Image.png

我們用TableView去實(shí)現(xiàn)該需求。在點(diǎn)擊cell時(shí)展開該cell, 再次點(diǎn)擊時(shí)收起。不同的cell展開高度依賴于該cell所展示的內(nèi)容。每一個(gè)行用StackView去實(shí)現(xiàn)。這里將用到嵌套的StackView和StackView動(dòng)態(tài)插入。另外我用了設(shè)置約束優(yōu)先級(jí)去解決Cell內(nèi)容顯示錯(cuò)位的問(wèn)題。

我們先實(shí)現(xiàn)Cell的展開與收縮。

  • 定義一個(gè)Dictionary來(lái)存儲(chǔ)每個(gè)Cell的展開或收縮狀態(tài)。keyIndexPath
@property (nonatomic, copy) NSMutableDictionary *expandedFlagOfRowDict;
  • 每次選擇cell后toggle展開狀態(tài)。實(shí)現(xiàn)TableView delegate方法。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    BMMerchantCell *cell = [tableView cellForRowAtIndexPath:indexPath];

    //toggle the expanded flag*
    self.expandedFlagOfRowDict[indexPath] = [NSNumber numberWithBool:![self.expandedFlagOfRowDict[indexPath] boolValue]];;
           
    [tableView beginUpdates];
    [tableView endUpdates];
}
  • 調(diào)用-beginUpdates 后 tableView delegate會(huì)回調(diào) heightForRowAtIndexPath方法。

-beginUpdates-endUpdates 必須組合使用。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    *//cell expanded height*
    if ([self.expandedFlagOfRowDict[indexPath] boolValue]) {
        return [self.heightForRowDict[indexPath] floatValue];
    }   
    *//cell collapsed height*
    return 44;
}
  • 由此實(shí)現(xiàn)了Cell的展開與收縮。

Cell動(dòng)態(tài)高度

上面代碼我們留意到有 self.heightForRowDict 這玩意兒,每個(gè)row展開的高度是不一樣的。它與self.expandedFlagOfRowDict 類似,我們定義一個(gè)字典來(lái)存儲(chǔ)每一個(gè)Cell的高度。該字典Key為IndexPath,value為高度。

@property (nonatomic, copy) NSMutableDictionary *heightForRowDict;

而每個(gè)Cell的高度由Cell對(duì)象本身確定。我們用IB去定義Cell原型。

Paste_Image.png

Cell收縮狀態(tài)時(shí),僅展示匯總一行(即Sum Stack View)。點(diǎn)擊展開時(shí),顯示細(xì)節(jié)行,細(xì)節(jié)行數(shù)不定。細(xì)節(jié)行嵌套到Detail Stack View這個(gè)大的StackView中。

細(xì)節(jié)行:(稱之為DetailItemStackView)

Paste_Image.png

可見(jiàn),在SumStackView高度已定,DetailStackView設(shè)定top與bottom約束,DetailItemStackView高度已定情況下,Cell的高度就由細(xì)節(jié)行的插入的多少?zèng)Q定了。

Note:與Label、Button等控件一樣,Stack View 也有自己的Intrinsic Size,在未設(shè)定約束時(shí)亦能確定自己的大小。其依賴于內(nèi)部subView的大小和space。在subView約束未定時(shí)其本身大小同依據(jù)于本身Intrinsic Size確定。這是一個(gè)嵌套遞歸的過(guò)程。

接下來(lái)就計(jì)算TableView Cell的高度。

  • 在收縮狀態(tài)時(shí),其高度等于SumStackView的高度。
  • 在展開狀態(tài)時(shí),其高度等于SumStackViewDetailStackView高度加兩者距離加DetailStackViewCell.Bottom距離。

考慮到解耦與性能優(yōu)化問(wèn)題,Cell的高度與內(nèi)容將不在Controller層計(jì)算。-tableView:heightForRowAtIndexPath: 中僅負(fù)責(zé)讀取高度值。
自定義Cell對(duì)象:

//MyCell.h
@interface MyCell : UITableViewCell

@property (weak, nonatomic) IBOutlet UIStackView *detailStackView;
@property (weak, nonatomic) IBOutlet UIStackView *sumStackView;

@property (weak, nonatomic) IBOutlet UILabel *cityLabel;
@property (weak, nonatomic) IBOutlet UILabel *sumLabel;
@property (weak, nonatomic) IBOutlet UILabel *vLabel;

@property (assign, nonatomic) CGFloat rowHeight;

@end

先把cell的高度都設(shè)為定值,運(yùn)行看看收縮與展開效果,假定rowHeight=100;

Paste_Image.png

運(yùn)行時(shí)可見(jiàn)編譯器報(bào)了一大堆約束沖突警告,界面Cell內(nèi)容顯示位置偏離,而IB界面設(shè)計(jì)并無(wú)約束警告。經(jīng)過(guò)本人探究后發(fā)現(xiàn),實(shí)際上是與運(yùn)行時(shí)的 -tableView:heightForRowAtIndexPath: 代理方法返回的高度相沖突。該高度H !== sumStackView.height + space + detailStackView.height + space; 故而沖突。而將其中一個(gè)space的約束優(yōu)先級(jí)調(diào)低至750,沖突解決。界面顯示正常。

Paste_Image.png

Note:由此得知,-tableView:heightForRowAtIndexPath: 返回高度值的約束優(yōu)先級(jí)實(shí)際上是介于7501000之間的(750<p<1000)。

Cell與StackView

至此Cell展開后顯示的是一個(gè)空的StackView,現(xiàn)在我們往Cell類里添加兩個(gè)方法內(nèi)容。

//MyCell.m

//Cell collapsed content
- (void)loadSumLineByDict:(NSDictionary *)dataDict {
    self.cityLabel.text = dataDict[@"cityDes"];
    self.sumLabel.text = [(NSNumber *)dataDict[@"sum"] stringValue];
}

//Cell expanded content
- (void)loadDetailLinesByDict:(NSDictionary *)dataDict {
    NSArray *detailList = (NSArray *)dataDict[@"detail"];
    
    //Insert sub stackViews. Performance consuming process.
    [detailList enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *mccTag = [(NSDictionary *)obj objectForKey:@"mccTag"];
        NSNumber *num = [(NSDictionary *)obj objectForKey:@"num"];
        DetailItemStackView *itemSV = [[[NSBundle mainBundle] loadNibNamed:@"DetailItemStackView" owner:nil options:nil] lastObject];
        itemSV.mccTagLabel.text = mccTag;
        itemSV.numLabel.text = [NSString stringWithFormat:@"%@", num];
        [self.detailStackView addArrangedSubview:itemSV];
    }];
    
    [self.detailStackView layoutIfNeeded];
    [self layoutIfNeeded];
    
    //Calculate the cell's height based on AutoLayout
    CGSize size = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    self.rowHeight = size.height;
}
  • 參數(shù)dataDict為數(shù)據(jù)模型;
  • -loadSumLineByDict:加載匯總行,即Cell收縮狀態(tài)時(shí)的展示行;
  • -loadDetailLinesByDict:加載細(xì)節(jié)行,即Cell展開時(shí)展示的內(nèi)容;該方法設(shè)計(jì)遍歷以及創(chuàng)建StackView對(duì)象,為耗性能的步驟。這個(gè)我們讓它有生之年只Run一次就夠了。
  • -layoutIfNeeded在stackView們都插入完畢后,更新布局。實(shí)現(xiàn)Runtime時(shí)的Autolayout;
  • -systemLayoutSizeFittingSize:方法返回滿足約束的大小,用以獲取Cell增加內(nèi)容后的高度值。

由此獲取cell的datasource方法如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *cellID = @"MyCellID";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];

    //load the collpased content
    [cell loadSumLineByDict:self.dataList[indexPath.row]];
    return cell;
}

-cellForRow:方法為高頻調(diào)用,因此此處盡量進(jìn)行輕量級(jí)的數(shù)據(jù)加載。此處僅加載Cell收縮狀態(tài)的內(nèi)容,而展開的內(nèi)容則在select時(shí)加載。didSelectRow方法就變成了這樣:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    MyCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    
    *//Check if the row had been loaded. If not,*
    if (![self.loadedFlagOfRowDict[indexPath] boolValue]) {
        
        *//Load detail lines and calculate the height for row*
        [cell loadDetailLinesByDict:self.dataList[indexPath.row]];
        
        *//store the height*
        self.heightForRowDict[indexPath] = @(cell.rowHeight + 1);
        
        *//set the loaded flag to true and it will end up true ever since*
        self.loadedFlagOfRowDict[indexPath] = @YES;
    }
    
    *//toggle the expanded flag*
    self.expandedFlagOfRowDict[indexPath] = [NSNumber numberWithBool:![self.expandedFlagOfRowDict[indexPath] boolValue]];;
    
    [tableView beginUpdates];
    [tableView endUpdates];
}
  • 此處加入了一個(gè)字典屬性來(lái)存儲(chǔ)是否加載的標(biāo)志位
@property (nonatomic, copy) NSMutableDictionary *loadedFlagOfRowDict;

依此判斷使得該Cell展開內(nèi)容有生之年僅加載一次。

  • 加載完內(nèi)容后計(jì)算高度,并存儲(chǔ)至_heightForRowDict中,在-tableView:heightForRowAtIndexPath:中讀取并展示實(shí)際高度。由此實(shí)現(xiàn)了TableViewCell 的收縮展開與動(dòng)態(tài)高度。
小結(jié):
  • -beginUpdates & -endUpdates 實(shí)現(xiàn)select Cell時(shí)收縮與展開。
  • -tableView:heightForRowAtIndexPath: 的約束優(yōu)先級(jí)。
  • -tableView:cellForRowAtIndexPath:-tableView:heightForRowAtIndexPath:為高頻調(diào)用方法,高耗能的操作盡量轉(zhuǎn)移到其它方法中,本例子中如Cell高度計(jì)算,內(nèi)容加載等。本例中cellForRow方法僅加載cell最基本的內(nèi)容。

下一篇講延續(xù)本篇內(nèi)容,探討TableView的性能優(yōu)化方面的問(wèn)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容