自动布局系列的代码可见工程: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的作用:
- estimatedRowHeight:由于UITableView是UIScrollView的子类,所以也要确定它的contentSize。在绘制cell之前都是要先确定好tableview的contentSize的高度(宽度是确定的),所以计算高度的heightForRowAtIndexPath API都是在cellForIndexPath之前。那么如果我们要根据内容来计算高度的话,就要先初始化cell的内容才可以。那么此时tableview的contentSize的高度就无法确定,怎么解决呢?就是用estimatedRowHeight乘cell的数量来初步计算contentSize的高度。然后再根据实际计算后的高度调整contentSize。
- 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动画,只好传一张图充充数了。。。