UITableview中cell自适应高度

UITableview中cell自适应高度

由于UITableview中cell展示时,会先调用其代理方法中的,- tableView:heightForRowAtIndexPath: 方法,来确定该cell的高度,然后再调用tableview的datasource方法,来创建展示该cell。对于一些类似聊天界面的tableview,我们不能返回一个固定的高度,需要先根据model里面的数据来计算这个cell的高度。

第一种做法是根据data计算cell内视图的frame并手动布局界面。我一般的做法是在原来model的基础上再添加出一个frameModel层,作为隔离。因为model层只需要知道其本身内的数据,不需要具体了解具体展示其数据的cell的视图的布局规则。从另一个角度来说,同一个数据可能展示在不同的界面中,可能需要计算的frame也不同,所以不能把这个计算给写死在model层中。

这么做的优点是,
1.计算量比autolayout要小。由于autolayout的计算是根据界面的布局,屏幕尺寸来进行实时的计算,并将重新排布视图。而我们手动的计算,我们可以添加一个缓存,来确保具体的布局计算只有一次。由于可以减少计算量,所以界面的流畅程度会比autolayout快一点。

但是缺点也是存在的,
1.由于所有的计算需要我们手动来计算,代码的复杂度明显增加了。更加难维护了。因为需要另外抽出一个frameModel层,需要在这个对象里面手动添加计算的代码。如果需要自定义高度的cell很多的话,代码量上会增加很多。另外,这个frameModel需要知道cell中具体的视图布局规则,等于与cell直接耦合。如果以后需要修改cell的视图布局,则需要同时修改两处代码。

2.手动计算不能很好的支持横竖屏的切换。或者说,在写布局代码的时候,需要分别写入两套计算规则。而自动布局只需要根据界面的比例值来计算即可。

第二种做法使用autolayout来自动布局。在ios8的时候,推出了self-sizing的概念,即对于例如UILabel,UIButton本身的高度可以根据里面的数据内容来撑开,即会有一个自身的高度存在。因此我们可以根据这个特性,利用autolayout来给cell设置约束,然后给cell传入数据后,让cell来自动布局,然后cell就会被”撑开”,我们就可以直接得到cell被撑开后的height,这个height我们需要在代理中返回的数据。
具体可以调用的就是

1
2
3
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize {

}

FDTemplateLayoutCell框架基于这个概念来实现ios开发中UITableView中cell高度计算。

简单调用的步骤即,

1
2
3
4
5
6
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) {
// 配置 cell 的数据源,和 "cellForRow" 干的事一致,比如:
cell.entity = self.feedEntities[indexPath.row];
}];
}

简单查看了一下框架中具体实现代码,
首先这是一个UITableview的分类,所以可以直接以tableview的形式来调用,而不用创建其他的对象。

首先根据cell的identifer和runtime来给tableview持有一个cell对象数组。这个cell对象不用于具体的展示,只是用来方便计算cellHeight。

每当具体调用到代理方法返回高度的时候,我们就可以传入一个identifer得到一个已regist过的cell,然后给cell来传入model数据。cell得到数据后,我们手动调用其自动布局的方法,得到通过autolayout计算得出的cellheight

这么做的优点就是,在计算的时候不需要手动提前先计算cellHeight,也就不需要frameModel这个对象。代码更加简洁了。但是,每一次滑动tableview的时候,都会调用代理方法,我们需要重新计算一次cellHeight。而autolayout的计算量又远远大于手动计算,在流畅度上来说,体验远远差于手动计算。所以FDTemplateLayoutCell又提出了一个缓存的方法。可以根据indexPath或者key来实现数据的高度的缓存,如果第二次传入相同的key时,则直接返回缓存的数据,而不再重新计算。

key的简单缓存做法即使用mutableDictionary来实现即可。

讲一下使用indexpath来实现的缓存机制。
我们知道,在tableview中,刷新之后,可能原先indexpath对应的model即会出现新的变化,如果近简单的根据indexpath来进行数据缓存,则容易出现错位的情况。

主要的原理就是,通过分类覆写了UITableview的load方法,在load方法中,通过runtime将tableview的insert,reloadData等方法给hook成自己的方法。然后在替换的方法中手动管理indexpath变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

+ (void)load {
// All methods that trigger height cache's invalidation
SEL selectors[] = {
@selector(reloadData),
@selector(insertSections:withRowAnimation:),
@selector(deleteSections:withRowAnimation:),
@selector(reloadSections:withRowAnimation:),
@selector(moveSection:toSection:),
@selector(insertRowsAtIndexPaths:withRowAnimation:),
@selector(deleteRowsAtIndexPaths:withRowAnimation:),
@selector(reloadRowsAtIndexPaths:withRowAnimation:),
@selector(moveRowAtIndexPath:toIndexPath:)
};

for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

个人意见,在使用FDTemplateLayoutCell的使用确实可以简化我们在不同高度的cell计算。同时,为了避免多次重复的计算,应该开启缓存机制。更好的是通过key来实现缓存。即可以根据model中的各个data生成一个哈希值作为key来缓存height,当data发生变化的时候,其哈希值也会变化,即可以促使tableview来重新计算数据。