自動布局系列的代碼可見工程:https://github.com/noahls/AutoLayoutDemo
UITableView是iOS中最常用的控件之一。根據(jù)UITableViewCell的內容確定其高度是非常常見的需求。
iOS8之后蘋果提供了Self Sizing Cell的機制讓開發(fā)者能夠簡單地實現(xiàn)這一需求。
靜態(tài)Self Sizing Cell
最基本的需求烟很,只要靜態(tài)地根據(jù)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之前。那么如果我們要根據(jù)內容來計算高度的話赂蠢,就要先初始化cell的內容才可以绪穆。那么此時tableview的contentSize的高度就無法確定,怎么解決呢虱岂?就是用estimatedRowHeight乘cell的數(shù)量來初步計算contentSize的高度玖院。然后再根據(jù)實際計算后的高度調整contentSize。
- UITableViewAutomaticDimension:這實際上是一個Float類型的常量第岖,沒有實際意義难菌,只是告訴系統(tǒng)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函數(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屬性設置成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這個函數(shù)。所以如果有cell的屬性變更就不能用這個API了拍谐。
相對于使用reloadData烛缔,beginUpdates和endUpdates結合使用可以在cell的高度變化時有一個動畫效果,優(yōu)化用戶的體驗轩拨。
約束變化導致高度變化
上面的例子中cell內部的約束是沒有改變的践瓷,但是有些時候會遇到需要改變約束的情況。
假設cell中有兩個標簽,A和B亡蓉。點擊按鈕時需要隱藏或者展示標簽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的約束是有沖突的甲棍。雖然在后面刪除掉了简识,結果是正確的。但是警告是在約束建立的時候就會報的感猛,為了避免誤導七扰,還是先刪除約束比較好。
響應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的內部。不一定執(zhí)行setupSubViews方法咱士。而使用reloadData會造成非常突兀的效果立由。而且也沒有必要去刷新所有的cell照激。只要重新加載當前的cell就好了砾跃。并且還有動畫效果的選項,可以讓動態(tài)變化非常流暢腮鞍。
由于不知道怎么上傳gif動畫弛房,只好傳一張圖充充數(shù)了道盏。。。