IOS研究成果——在autolayout 如何实现 Cell 的高度自适应

时间:2021-11-13 22:19:07

Hello,小编又来了,距离上次写博客都有一段日子了,主要是这个月一直在赶新项目,项目推了又改改了又推,需求被改无数,泪奔。。。但是在做项目的时候发现了好多小细节,等项目做完了就拿来给大家看看,好了,不再多废话了,回归我们今天的主题。

其势 Autolayout 技术再 IOS6的时候就已经开始有了,但是大多数人还是习惯不了这个看起来奇特的东西,而且官方文档做到根本不是人看的,但是现在越来越多的人开始使用 autolayout 了,为什么?因为好用呀,就是这么简单,以前辛辛苦苦算 frame 值,现在大家就只需要抛开他,拥抱我们的 autolayout 就可以了,当然也有一些特殊情况,这里先不说,那么,我们在用 autolayout 的时候会碰到一个问题,怎么让我们的 cell 自适应呢?大家都知道,以前我们让 cell 自适应的时候,都是通过在模型赋值的时候计算出内容的 frame 值,也就是我们经常说的 frameModel,但是这个方法很沉郁,辛辛苦苦拿内容过去慢慢计算,稍微算法不好的同学就在这里跪了,那么 autolayout 完全就帮到你了,因为 label 的高度都是自适应的,非常好用,在说这个技术之前希望之前不太懂 autolayout 的同学恶补一下。。。。毕竟时代在进步,技术不断更新,在这里推荐大家使用一个第三方进行更好的运用 autolayout,到 GitHub 搜索Masonry这个绝对的代码 autolayout 布局的神器。当 你布局发生问题时还会有很好的提醒让你发现的那里出问题了。点击这里

先说一下思想:
1.启用估计行高(IOS8)下的实现,当你开启此方法再要 autolayout 布局的时候,自适应已经开启了,但是这只是使用与 IOS8以上的系统,当运行到 IOS7的时候,就会崩溃了


    self.tableView.rowHeight = UITableViewAutomaticDimension;
    self.tableView.estimatedRowHeight = 44.0; // 设置为一个接近“平均”行高的值,用来估算修正tableView滚动条的高度

这样做是为了让tableView 上显示的 cell 临时的估计一个行高,主要使用来修改 tableView 右侧滚动条的大小,告诉它我下面还有多少内容,你自己看着办。但是一旦当 cell 出现在屏幕的时候这个值就会重新被计算,所以有时候我们设置的估算只比较少的时候,会出现一个滚动条跳跃的情况,这个会出现在估算一个 cell 的高度跟实际 cell 的高度相差比较大的时候出现,这个时候,你才应当实现tableView:estimatedHeightForRowAtIndexPath:方法,为每一行返回一个更精确的估算值。

IOS7下支持的方法(需要自己实现 cell 尺寸的自适应功能)

2.1创建一个用作缓存的实例,cacheCells
当然你要为在这个 tableView 显示的每一种 cell 都必须给一个重用的标识符,因为每一种cell 的布局不同,被估算出来的高度也会不相同的,这些 cell 完全是用作高度计算的,绝对不会被用作tableView:cellForRowAtIndexPath:方法的返回值以实际呈现在屏幕上。)接着,这个cell的内容(例如,文本、图片等等)还必须和会被显示在table view中的内容完全一致。

2.2使用估算行高
当你一加载程序进来的时候,你的 tableView 中已经有几十行 Cell,你会发现自动布局约束的解决方式在第一次加载table view的时候会迅速地卡住主线程。这是因为tableView在第一次加载中会调用 tableView:heightForRowAtIndexPath:方法(主要就是为了计算滚动条的大小)
所以在 IOS7中,你绝对可以使用estimatedRowHeight,让滚动条拥有一个估算的值,让不在屏幕中的 cell 告诉滚动条,我应该有多高,滚动条就会根据估算值而显示大小,然而,这些 cell 都会在显示在屏幕的时候计算出真实高度,那么滚动条也跟着改变。
上面也说过,如果使用估算值的话,当 cell 的高度与被估算的高度严重不匹配的时候,滚动条会有跳跃现象,这个时候,你才应当实现tableView:estimatedHeightForRowAtIndexPath:方法,为每一行返回一个更精确的估算值。

  1. 缓存行高(如果需要)
    如果上面提到的你都做了,但是tableView:heightForRowAtIndexPath:的性能仍然慢的不可接受。非常不幸,你需要给行高做一些缓存(这是苹果的工程师们给出的改进建议)。大体的思路是,第一次计算时让自动布局引擎解析约束条件,然后将计算出的行高缓存起来,以后所有对该cell的高度的请求都返回缓存值。当然,关键还要确保任何会导致cell高度变化的情况发生时你都清除了缓存的行高——这通常发生在cell的内容变化时或其他重大事件发生时(比如用户调节了动态类型文本大小(Dynamic Type text size)的滑动条),当然,这是比较底层的做法,目前以小编的功力还没法做到这一点,但是 GitHub 上有一些第三方已经做出来了,等有空小编再发出来跟大家探讨一下

以下是 IOS7下实现 Cell 自适应高度的代码

static NSString *ID = @"Cell";
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    // 取出重用标示符对应的cell。
    // 注意,如果重用池(reuse pool)里面没有可用的cell,这个方法会初始化并返回一个全新的cell,
    // 因此不管怎样,此行代码过后,你会可以得到一个布局约束已经完全准备好,可以直接使用的cell。
    FSCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (!cell) {
        cell = [[FSCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
    }

    // 这里先做比较简单的赋值一个 text 的内容,然后改写 cell的 text 属性的 set 方法,根据内容设计 label 的大小
    NSString *text = self.dataSource[indexPath.row];
    cell.text = text;

    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //在开发中可能会碰到不同的 cell 布局,那么我们就需要的设置不同的重用标识符reuseIdentifier,我们这里设置每种重用的 cell ,做一个缓存
// NSString *reuseIdentifier = [self.offscreenCells

     // 从cell字典中取出重用标示符对应的cell。如果没有,就创建一个新的然后存储在字典里面。
     // 警告:不要调用table view的dequeueReusableCellWithIdentifier:方法,因为这会导致cell被创建了但是又未曾被tableView:cellForRowAtIndexPath:方法返回,会造成内存泄露!

    // 个人理解:一开始程序近来的时候是先走heightForRowAtIndexPath:方法,然后再走cellForRowAtIndexPath:方法,如果此时用了table view的dequeueReusableCellWithIdentifier:方法,但是 cell 并没有存在与缓存中,此时创建 cell 后,cell tableView:cellForRowAtIndexPath:并没有在被使用到的时候会造成内存泄露
    FSCell *cell = [self.offscreenCells objectForKey:ID];
    if (!cell) {
        cell = [[FSCell alloc]init];
        [self.offscreenCells setObject:cell forKey:ID];
    }

    // 赋值内容
    NSString *text = self.dataSource[indexPath.row];
    cell.text = text;

 // 确保cell的布局约束被设置好了,因为它可能刚刚才被创建好。
    // 使用下面两行代码,前提是假设你已经在cell的updateConstraints方法中设置好了约束:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraints];

    // 将cell的宽度设置为和tableView的宽度一样宽。 
    // 这点很重要。
    // 如果cell的高度取决于table view的宽度(例如,多行的UILabel通过单词换行等方式),
    // 那么这使得对于不同宽度的table view,我们都可以基于其宽度而得到cell的正确高度。 
    // 但是,我们不需要在-[tableView:cellForRowAtIndexPath]方法中做相同的处理,
    // 因为,cell被用到table view中时,这是自动完成的。
    // 也要注意,一些情况下,cell的最终宽度可能不等于table view的宽度。
    // 例如当table view的右边显示了section index的时候,必须要减去这个宽度。
    cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

  // 触发cell的布局过程,会基于布局约束计算所有视图的frame。
   // (注意,你必须要在cell的-[layoutSubviews]方法中给多行的UILabel设置好preferredMaxLayoutWidth值;
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

     // 得到cell的contentView需要的真实高度
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

 // 要为cell的分割线加上额外的1pt高度。因为分隔线是被加在cell底边和contentView底边之间的。
    height += 1.0f;

     NSLog(@"%p",cell);
    return height;
}
// 注意:除非行高极端变化并且你已经明显的觉察到了滚动时滚动条的“跳跃”现象,你才需要实现此方法;否则,直接用tableView的estimatedRowHeight属性即可。
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 以必需的最小计算量,返回一个实际高度数量级之内的估算行高。
    // 例如:
    // 
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 250.0f;
    } else {
        return 40.0f;
    }
}

那么在 cell 的内部就是这么实现的

#import <UIKit/UIKit.h>

@interface FSCell : UITableViewCell

@property (nonatomic,copy) NSString *text;

@end
- (void)setText:(NSString *)text
{
    _text = text;

    [self.contentView addSubview:self.Label];

    self.Label.text = text;

    self.Label.font = [UIFont systemFontOfSize:20];

}
- (void)layoutSubviews
{
    [super layoutSubviews];

    // 必须设置好文字最大展示的宽度,这是必须的,autolayout 才能帮你排好计算好高度
    self.Label.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds) - 20;
}

- (void)updateConstraints
{
    [super updateConstraints];

    // updateConstraints 这个方法可能不止被调用一次,因此我们要避免添加相同布局约束,所有我们加一个布局完成的判断,让布局只布一次,以确保不见添加重复约束
    // 另外,我们需要更新一个布局也应该写到updateConstraints这个方法里面,但是要在判断语句之前,这样才能确保每次调用都能执行
    if (self.didSetUpConstraints) return;



    [self.Label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.top.left.bottom.mas_equalTo(UIEdgeInsetsMake(10, 10, 10, 10));
    }];

    self.didSetUpConstraints = YES;
}

- (UILabel *)Label
{
    if (!_Label) {
        _Label = [[UILabel alloc]init];
        _Label.numberOfLines = 0;
    }
    return _Label;
}
最后说一句,如果你觉得这篇博文很赞,请给我点个赞,如果有大神觉得小编有那里做的不对,或者那里需要优化,请指点出来,让大家一起进步,不为别的,只是为了让大家都能好好成长起来,么么哒