前言
其实早就准备写这篇文章了,但是一直没有系统去整理一下相关的demo,加上最近离职了,各种事情忙的有点郁闷,所以一直拖沓了下来。回家休息了一段时间想起来写了一半的demo,在还没找工作的这段空挡时间抽空完善了一下再写篇说明文档备忘一下。
需求背景
iOS的cell行高自适应是个非常常见的需求,也是一个非常简单的需求,之前我遇到过很多小伙伴不知道怎么来实现,在这里就一步步的来分析一下,供大家参考。
问题分析
其他的实现场景就不说了,我们现在来分析一下具体的需求,如图所示:
cell行高自适应.png
其实主要实现这几点就可以解决所谓的自适应行高的问题,下面我们就来逐步实现这个需求。
计算UITableViewCell的高度
说到计算高度,大家都不陌生,最简单常见的就是计算出每个子视图的高度累积起来返回我们所需要的cell高度,然后在UITableViewDelegate中调用:
- - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- return 666;
- }
或者高度固定的情况下直接
- self.tableView.rowHeight = 666;
但是这就要求我们需要提前拿到model中的数据来手动计算每个控件的高度,这样既麻烦又不能通用,所以在autolayout出来之后我们只要给cell的contentView的上下左右都添加了约束,系统就可以自动的帮我们实现高度的自适应,就是一定要保证cell的高度可以被子视图撑开就可以了,利用的是systemLayoutSizeFittingSize这个API;
iOS8之后就更简单了,直接使用:
- self.tableView.estimatedRowHeight = 666;
- self.tableView.rowHeight = UITableViewAutomaticDimension;
就可以了,其中estimatedRowHeight是预估高度,这里要注意delegate中的返回高度方法就不用在写了。
关于这方面的文章,UITableView+FDTemplateLayoutCel的作者写的一篇文章十分详细,建议先去了解一下(优化UITableViewCell高度计算的那些事)
但是这个方法实际上在有多个子视图的cell上滑动是很卡顿的,特别是在iOS8尤其是iOS10上卡顿尤为明显,这跟系统的算高机制有一定关系,具体可以看上面的文章,这里不再解释了。
如果脱离开autolayout来说,平时计算高度的话,最开始都是根据cell内子控件内容的高度来手动累加起来,但是这个方法每次都要去手动处理其中的算高逻辑,而且横竖屏切换的时候还要重新计算,在平时开发中就会浪费大量不必要的精力。所以后来我在项目中是通过调用layoutSubviews来获取到子控件的实际frame,这样就可以得到我们所需的cell高度值,如下代码所示:
- cell.frame = CGRectSetWidth(cell.frame, contentViewWidth);
- cell.contentView.frame = CGRectSetWidth(cell.contentView.frame, CGRectGetWidth(tableView.frame));
- [cell layoutIfNeeded];
- UIView *cellBottomView = nil;
- if (cell.FS_cellBottomView) {
- cellBottomView = cell.FS_cellBottomView;
- }else if (cell.FS_cellBottomViews && cell.FS_cellBottomViews.count > 0) {
- cellBottomView = cell.FS_cellBottomViews[0];
- for (UIView *view in cell.FS_cellBottomViews) {
- if (CGRectGetMaxY(view.frame) > CGRectGetMaxY(cellBottomView.frame)) {
- cellBottomView = view;
- }
- }
- }else {
- NSArray *contentViewSubViews = cell.contentView.subviews;
- if (contentViewSubViews.count == 0) {
- cellBottomView = cell.contentView;
- }else{
- cellBottomView = contentViewSubViews[0];
- for (UIView *view in contentViewSubViews) {
- if (CGRectGetMaxY(view.frame) > CGRectGetMaxY(cellBottomView.frame)) {
- cellBottomView = view;
- }
- }
- }
- }
- CGFloat cellHeight = CGRectGetMaxY(cellBottomView.frame) + bottomOffset;
其中的cellBottomView是位于cell***部的子视图,为了提高计算效率***传入,如果不确定哪个子视图在最下面,可以传入一个视图数组contentViewSubViews,详细使用方式可以查看demo。
缓存cell高度
高度计算出来后,正常来说我们的需求已经达到了,但是如果这个高度值每次滑动的时候由于cell的复用机制都会重新计算,若果这个cell的自定义样式很复杂,子视图太多,那么大量的计算一定会损耗性能而导致明显的卡顿,所以缓存机制就是个必要的措施,更何况苹果也建议这样做;
demo提供了两个计算行高的API:
- /**
- cell自动计算行高
- @param tableView tableView
- @param indexPath indexPath
- @param contentViewWidth cell内容宽度,不确定可传0
- @return cell高度
- */
- + (CGFloat)FSCellHeightForTableView:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath cellContentViewWidth:(CGFloat)contentViewWidth bottomOffset:(CGFloat)bottomOffset;
- /**
- cell自动计算行高优化版
- @param tableView tableView
- @param indexPath indexPath
- @param cacheKey 当前cell唯一标识符
- @param contentViewWidth cell内容宽度,不确定可传0
- @return cell高度
- */
- + (CGFloat)FSCellHeightForTableView:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath cacheKey:(NSString *)cacheKey cellContentViewWidth:(CGFloat)contentViewWidth bottomOffset:(CGFloat)bottomOffset;
***种使用数组来做缓存,传入对应cell的indexPath作为数组索引值;第二种则采用字典来缓存数据,要求传入一个唯一标识符cacheKey来区分;
两种方式都可以准确获得cell高度,***种实现更简洁,缺点就是数据源发生变化时,所有的缓存就会清空重新计算后缓存,比如reloadData的时候;第二种就是在前者的基础上添加一个区分不同cell的标识符,使用时还是建议使用第二种,不会清空缓存数据,轻量级页面没什么区别。总之两种方法都做了缓存数据的容错处理,支持以下方法:
- @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:)
兼容横竖屏
这个需求实现较为简单,就是横屏和竖屏分别采用两套缓存数据,互不影响,切换横竖屏的时候自动切换数据源。
- - (NSMutableArray *)indexCacheArrForCurrentOrientation
- {
- return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? self.indexCacheArr_Portrait: self.indexCacheArr_Landscape;
- }
***实现的效果如图所示: