自动布局4-UITableViewCell自动高度与高度变化

自动布局系列的代码可见工程:https://github.com/noahls/AutoLayoutDemo

UITableView是iOS中最常用的控件之一。根据UITableViewCell的内容确定其高度是非常常见的需求。

iOS8之后苹果提供了Self Sizing Cell的机制让开发者能够简单地实现这一需求。

静态Self Sizing Cell

最基本的需求,只要静态地根据cell的内容来确定其高度。其内容不会变化。

有三点要求

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

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

其次是不要重写UITableViewDataSource中的

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

最后保证cell的contentView内部的约束集合能够准确地计算出cell的高度。

简答介绍一下这些API的作用:

  1. estimatedRowHeight:由于UITableView是UIScrollView的子类,所以也要确定它的contentSize。在绘制cell之前都是要先确定好tableview的contentSize的高度(宽度是确定的),所以计算高度的heightForRowAtIndexPath API都是在cellForIndexPath之前。那么如果我们要根据内容来计算高度的话,就要先初始化cell的内容才可以。那么此时tableview的contentSize的高度就无法确定,怎么解决呢?就是用estimatedRowHeight乘cell的数量来初步计算contentSize的高度。然后再根据实际计算后的高度调整contentSize。
  2. UITableViewAutomaticDimension:这实际上是一个Float类型的常量,没有实际意义,只是告诉系统cell的高度需要计算。

可变高度cell

在有些情况下,我们需要展开cell来展示更多的内容。

假设有这样的需求:要写一个cell,cell内有一个简介的label。简介默认只占一行,但是要提供一个展开按钮,点击按钮可以展示全部简介内容。

首先要满足上面的条件,然后设置好约束:

    _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函数里面做动画处理:

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

在这里,只要将label的numberOfLines属性设置成1或者0(多行)就可以变更了。关键是_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;

关键就在于beginUpdates和endUpdates这两个API,利用这两个API可以只刷新tableView的高度。亲测不一定会调用cellForIndexPath这个函数。所以如果有cell的属性变更就不能用这个API了。

相对于使用reloadData,beginUpdates和endUpdates结合使用可以在cell的高度变化时有一个动画效果,优化用户的体验。

约束变化导致高度变化

上面的例子中cell内部的约束是没有改变的,但是有些时候会遇到需要改变约束的情况。

假设cell中有两个标签,A和B。点击按钮时需要隐藏或者展示标签B。这个时候约束就要根据是否展开改变了。

- (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的约束是有冲突的。虽然在后面删除掉了,结果是正确的。但是警告是在约束建立的时候就会报的,为了避免误导,还是先删除约束比较好。

响应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的组合会发现没有任何变化,因为它不会去更新cell的内部。不一定执行setupSubViews方法。而使用reloadData会造成非常突兀的效果。而且也没有必要去刷新所有的cell。只要重新加载当前的cell就好了。并且还有动画效果的选项,可以让动态变化非常流畅。

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

推荐阅读更多精彩内容