美文网首页iOS实战专题:临时区程序员
自动布局4-UITableViewCell自动高度与高度变化

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

作者: 流水_事 | 来源:发表于2017-04-15 15:13 被阅读401次

    自动布局系列的代码可见工程: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

    相关文章

      网友评论

        本文标题:自动布局4-UITableViewCell自动高度与高度变化

        本文链接:https://www.haomeiwen.com/subject/gmgnattx.html