iOS UITableView5种行高自适应方案的选择
下面将给出了5种Cell自适应高度的方案,并对比每种实现方案的流畅度。从UI最不流畅的一种开始,我们慢慢优化。通过观察屏幕的FPS来判断屏幕在操作时是否卡顿。关于对FPS的实时监测,使用了YYKit-Demo中FPS控件来实现。点击列表中不同Cell都会跳转相同的内容列表页。只不过每个Cell所对应的内容页面的Cell自适应高度的实现方式不同。
5种Cell高度自适应方案
1.Autolayout + AutomaticDimension
点击第一个Cell进入的页面完全由AutoLayout进行布局,Cell自适应的高度也不用我们自己计算,而是使用系统提供的解决方案UITableViewAutomaticDimension来解决。当然,使用UITableViewAutomaticDimension要依赖于你添加的约束,稍后会介绍到。这种实现方案用起来简单,不过UI流畅度方面不太理想。当TableView快速滑动时,就会出现严重的掉帧。亲测FPS最低值38!
545446-20160922151158059-1130164887.gif
#//第1步:设置预估值
self.tableView.estimatedRowHeight = 100.0;
#//第2步:返回UITableViewAutomaticDimension 自动调整约束,性能非常低
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
cell所有子控件和底部动态label的约束
动态label关键的约束,撑开cell
2.Autolayout + CountHeight
依然是采用AutoLayout的方式来对Cell的内容进行布局,不过Cell的高度我们是自己计算的,计算的过程是放在子线程中进行的,所以这种实现方式要优于第一种实现方式,亲测FPS最低值36!
手动计算行高,Autolayout布局
- (void)createDataSupport {
self.dataSupport = [[DataSupport alloc] init];
__weak typeof (self) weak_self = self;
[self.dataSupport setUpdataDataSourceBlock:^(NSMutableArray *dataSource) {
weak_self.dataSource = dataSource;
[weak_self.tableView reloadData];
}];
[self addTestData];
}
- (void)addTestData {
dispatch_queue_t concurrentQueue = dispatch_queue_create("zeluli.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
for (int i = 0; i < 50; i ++) {
dispatch_group_async(group, concurrentQueue, ^{
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self createTestModel];
dispatch_semaphore_signal(lock);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self updateDataSource];
});
}
- (void)createTestModel {
TestDataModel * model = [[TestDataModel alloc] init];
model.title = @"行歌";
NSDateFormatter *dataFormatter = [[NSDateFormatter alloc] init];
[dataFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
model.time = [dataFormatter stringFromDate:[NSDate date]];
NSString *imageName = [NSString stringWithFormat:@"%d.jpg", arc4random() % 6];
model.imageName =imageName;
NSInteger endIndex = arc4random() % contentText.length;
model.content = [contentText substringToIndex:endIndex];
model.textHeight = [self countTextHeight:model.content];
model.cellHeight = model.textHeight + 60;
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:model.content];
text.font = [UIFont systemFontOfSize:14];
text.lineSpacing = 3;
model.attributeContent = text;
model.attributeTitle = [[NSAttributedString alloc] initWithString:model.title];
model.attributeTime = [[NSAttributedString alloc] initWithString:model.time];
[self.dataSource addObject:model];
}
-(CGFloat)countTextHeight:(NSString *) text {
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:text];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineSpacing = 0;
UIFont *font = [UIFont systemFontOfSize:14];
[attributeString addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, text.length)];
[attributeString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
CGRect rect = [attributeString boundingRectWithSize:CGSizeMake(SCREEN_WIDTH - 30, CGFLOAT_MAX) options:options context:nil];
return rect.size.height + 40;
}
- (void)updateDataSource {
if (self.updateDataBlock != nil) {
self.updateDataBlock(self.dataSource);
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row < self.dataSource.count) {
TestDataModel *model = self.dataSource[indexPath.row];
return model.cellHeight;
}
return 100;
}
@implementation AutolayoutTableViewCell
- (void)configCellData:(TestDataModel *)model {//配置cell中的数据
[self.headerImageView setImage:[[ImageCache shareInstance] getCacheImage:model.imageName]];
[self.titleLable setText:model.title];
[self.timeLabel setText:model.time];
[self.contentLabel setText:model.content];
}
3.FrameLayout + CountHeight
为了进一步提高流畅度,我们采用了纯Frame布局,因为Autolayout最终还是会被转换成Frame进行布局的,所以我们就用Frame对整个Cell中的所有子控件进行布局。当然Cell高度及可变内容的高度,跟第2种方法一样都是在子线程中进行计算的,这优化的重要一步。这种实现方式还是比较流畅的,可以作为折中的方案.亲测FPS最低值42!
手动计算行高,frame布局
4.YYKit + CountHeight
545446-20160922151158059-1130164887.gif接下来我们继续进行优化,引入第三方UI组件YYKit。将Cell上的组件替换成YYKit所提供的组件。然后使用Frame进行布局,当然也是在子线程中对Cell的高度进行了计算。效果还是比较流畅的,但是还未达到完全不掉帧的效果。亲测FPS最低值53!
@property (strong, nonatomic) UIImageView *headerImageView;
@property (strong, nonatomic) YYLabel *titleLable;
@property (strong, nonatomic) YYLabel *timeLabel;
@property (strong, nonatomic) YYTextView *contentTextView;
5.AsyncDisplayKit + CountHeight
我们用Facebook提供的第三方库来进行基础组件的替换,将我们使用到的组件替换成AsyncDisplayKit相应的Note。这些Note是对系统组件的重组,对组件的显示进行了优化,让其渲染更为流畅,亲测FPS最低值59!
如果你对UI流畅度要求比较高的话,那么AsyncDisplayKit是一个比较好的选择。不过会严重依赖AsyncDisplayKit,如果AsyncDisplayKit停止维护了,后期对AsyncDisplayKit进行替换的话,工作量还是比较大的。因为这种布局框架不像网络框架,我们可以对网络框架的调用进行提取,网络层统一对外接口,很方便切换到其他网络请求库。但是像AsyncDisplayKit这种框架会散布于UI层的各个角落,封装提取不易,更不用说轻而易举的替换了。所以像这种页面的实现,个人还是偏向于Framelayout + CountHeight的方式来实现。
@property (strong, nonatomic) ASImageNode *headerImageNode;
@property (strong, nonatomic) ASTextNode *titleTextNode;
@property (strong, nonatomic) ASTextNode *timeTextNode;
@property (strong, nonatomic) ASTextNode *contentTextNode;
总结:
- 1、2方案对比,手动计算行高优于自适应行高
- 2、3方案对比,frame优于Autolayout
- 3、4、5方案, 根据实际情况进行选择,方案5最优,缺点同样明显,侵入性强
参考资料:https://www.cnblogs.com/ludashi/p/5895725.html
参考资料:https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/