自動布局4-UITableViewCell自動高度與高度變化

自動布局系列的代碼可見工程:https://github.com/noahls/AutoLayoutDemo

UITableView是iOS中最常用的控件之一。根據(jù)UITableViewCell的內(nèi)容確定其高度是非常常見的需求。

iOS8之后蘋果提供了Self Sizing Cell的機制讓開發(fā)者能夠簡單地實現(xiàn)這一需求。

靜態(tài)Self Sizing Cell

最基本的需求,只要靜態(tài)地根據(jù)cell的內(nèi)容來確定其高度。其內(nèi)容不會變化。

有三點要求

首先在初始化tableView以后加上一下代碼:

tableView.estimatedRowHeight = 44.0;
tableView.rowHeight = UITableViewAutomaticDimension;

其次是不要重寫UITableViewDataSource中的

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

最后保證cell的contentView內(nèi)部的約束集合能夠準(zhǔn)確地計算出cell的高度。

簡答介紹一下這些API的作用:

  1. estimatedRowHeight:由于UITableView是UIScrollView的子類,所以也要確定它的contentSize。在繪制cell之前都是要先確定好tableview的contentSize的高度(寬度是確定的),所以計算高度的heightForRowAtIndexPath API都是在cellForIndexPath之前。那么如果我們要根據(jù)內(nèi)容來計算高度的話,就要先初始化cell的內(nèi)容才可以。那么此時tableview的contentSize的高度就無法確定,怎么解決呢?就是用estimatedRowHeight乘cell的數(shù)量來初步計算contentSize的高度。然后再根據(jù)實際計算后的高度調(diào)整contentSize。
  2. UITableViewAutomaticDimension:這實際上是一個Float類型的常量,沒有實際意義,只是告訴系統(tǒng)cell的高度需要計算。

可變高度cell

在有些情況下,我們需要展開cell來展示更多的內(nèi)容。

假設(shè)有這樣的需求:要寫一個cell,cell內(nèi)有一個簡介的label。簡介默認只占一行,但是要提供一個展開按鈕,點擊按鈕可以展示全部簡介內(nèi)容。

首先要滿足上面的條件,然后設(shè)置好約束:

    _increaseLabel = [[UILabel alloc] init];
    [self.contentView addSubview:_increaseLabel];
    
    [_increaseLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.contentView).offset(16);
        make.top.equalTo(self.contentView).offset(16);
        make.right.lessThanOrEqualTo(self.contentView).offset(-16);
    }];
    
    _showMoreBtn = [[UIButton alloc] init];
    [self.contentView addSubview:_showMoreBtn];
    [_showMoreBtn mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.contentView);
        make.bottom.equalTo(self.contentView).offset(-16);
        make.top.equalTo(_increaseLabel.mas_bottom).offset(8);
    }];
    [_showMoreBtn setTitle:@"展開" forState:UIControlStateNormal];
    [_showMoreBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [_showMoreBtn addTarget:self action:@selector(showMore:) forControlEvents:UIControlEventTouchUpInside];

然后在showMore函數(shù)里面做動畫處理:

    _isExpanded = !_isExpanded;
    if (_isExpanded) {
        [_showMoreBtn setTitle:@"收起" forState:UIControlStateNormal];
        _increaseLabel.numberOfLines = 0;
    }else{
        [_showMoreBtn setTitle:@"展開" forState:UIControlStateNormal];
        _increaseLabel.numberOfLines = 1;
    }
    
    if (_handleIncrease) {
        _handleIncrease();
    }

在這里,只要將label的numberOfLines屬性設(shè)置成1或者0(多行)就可以變更了。關(guān)鍵是_handleIncrease(),這是一個從controller中傳過來的block,因為最終還是得依靠刷新tableview來進行高度的變更,在tableView中的dataSource中:

        IncreaseLabelCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([IncreaseLabelCell class])];
        cell.increaseLabel.text = longText;
        cell.handleIncrease = ^() {
            [self.tableView beginUpdates];
            [self.tableView endUpdates];
//            [self.tableView reloadData];
        };
        return cell;

關(guān)鍵就在于beginUpdates和endUpdates這兩個API,利用這兩個API可以只刷新tableView的高度。親測不一定會調(diào)用cellForIndexPath這個函數(shù)。所以如果有cell的屬性變更就不能用這個API了。

相對于使用reloadData,beginUpdates和endUpdates結(jié)合使用可以在cell的高度變化時有一個動畫效果,優(yōu)化用戶的體驗。

約束變化導(dǎo)致高度變化

上面的例子中cell內(nèi)部的約束是沒有改變的,但是有些時候會遇到需要改變約束的情況。

假設(shè)cell中有兩個標(biāo)簽,A和B。點擊按鈕時需要隱藏或者展示標(biāo)簽B。這個時候約束就要根據(jù)是否展開改變了。

- (void)setupSubViews{
    if (_isExpanded) {
        [_labelA mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.contentView).offset(16);
            make.top.equalTo(self.contentView).offset(16);
        }];
        
        [_changeBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.lessThanOrEqualTo(_labelA.mas_right).offset(8);
            make.right.equalTo(self.contentView).offset(-16);
            make.top.equalTo(_labelA);
        }];
        
        _labelB.hidden = NO;
        [_labelB mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(_labelA.mas_bottom).offset(8);
            make.left.equalTo(_labelA);
            make.right.lessThanOrEqualTo(self.contentView).offset(-50);
            make.bottom.equalTo(self.contentView).offset(-16);
        }];
        
        [_changeBtn setTitle:@"收起" forState:UIControlStateNormal];
    }else{
        [_labelB mas_remakeConstraints:^(MASConstraintMaker *make) {
            
        }];
        _labelB.hidden = YES;
        
        [_labelA mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.contentView).offset(16);
            make.top.equalTo(self.contentView).offset(16);
            make.bottom.equalTo(self.contentView).offset(-16);
        }];
        
        [_changeBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.lessThanOrEqualTo(_labelA.mas_right).offset(8);
            make.right.equalTo(self.contentView).offset(-16);
            make.top.equalTo(_labelA);
        }];
        
        [_changeBtn setTitle:@"展開" forState:UIControlStateNormal];
    }
}

這里需要注意一點就是原來的約束和新的約束可能會有沖突。這個時候要先去除沖突的約束再建立新的約束,否則Xcode會報約束沖突的警告。

例如在else分支中,如果將

    [_labelB mas_remakeConstraints:^(MASConstraintMaker *make) {}];

挪動到

        [_labelA mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self.contentView).offset(16);
            make.top.equalTo(self.contentView).offset(16);
            make.bottom.equalTo(self.contentView).offset(-16);
        }];

之后,那么就會報約束沖突的警告。因為B的約束還在并且B的約束加上更新后的A的約束是有沖突的。雖然在后面刪除掉了,結(jié)果是正確的。但是警告是在約束建立的時候就會報的,為了避免誤導(dǎo),還是先刪除約束比較好。

響應(yīng)button點擊時間的代碼如下:

- (void)change:(id)sender{
    if (_handleChange) {
        _handleChange();
    }
}

_handleChange也是從controller中傳遞過來的block。

在controller中要稍作變化:

ConstraintUpdateCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([ConstraintUpdateCell class]) forIndexPath:indexPath];
ConstraintUpdateCellModel *model = _cellModels[indexPath.row/2];
__weak typeof(self) weakSelf = self;
cell.handleChange = ^{
    model.isExpended = !model.isExpended;
    [weakSelf.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    //            [weakSelf.tableView beginUpdates];
    //            [weakSelf.tableView endUpdates];
    //            [weakSelf.tableView reloadData];
};
cell.isExpanded = model.isExpended;
[cell setupSubViews];
return cell;

使用beginUpdates/endUpdates的組合會發(fā)現(xiàn)沒有任何變化,因為它不會去更新cell的內(nèi)部。不一定執(zhí)行setupSubViews方法。而使用reloadData會造成非常突兀的效果。而且也沒有必要去刷新所有的cell。只要重新加載當(dāng)前的cell就好了。并且還有動畫效果的選項,可以讓動態(tài)變化非常流暢。

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

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

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