自動布局系列的代碼可見工程: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的作用:
- 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。
- 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ù)了。。。