【问题标题】:Dynamic UILabel Heights/Widths in UITableViewCell in all Orientations所有方向的 UITableViewCell 中的动态 UILabel 高度/宽度
【发布时间】:2012-04-20 01:25:53
【问题描述】:

我的问题基本上归结为在 UITableCell 中支持 UILabel(我想是其他元素)的动态高度的最佳方法,以及正确调整标签宽度/高度和单元格的大小旋转时的高度。

我知道如何获得 UILabel 的预期高度,并相应地调整高度,但是当你也支持横向时,事情似乎变得相当棘手。

我想这可能会走layoutSubviews 的路线,但我不确定你如何将它与需要计算单元格高度的表格结合起来。 Another interesting post 在初始化时手动设置框架,以确保他们根据已知数量进行计算,但这仅解决了部分问题。

这是我正在处理的图像,红色箭头指向动态高度标签,蓝色箭头指向将改变高度的单元格。

我已经设法让它正常工作,但不确定它是否是正确的方法。

以下是我学到的一些东西:

  • cellForRowAtIndexPath 中的 cell.frame 始终在纵向模式下给出其大小。 (即对于 iphone 应用程序,它总是报告 320)。
  • 将原型单元格存储在属性中以供参考存在同样的问题,始终处于纵向模式
  • 自动调整标签宽度的掩码在此用例中似乎没有用,实际上会导致计算的旋转高度出现问题
  • cellForRowAtIndexPath 中的 tableView.frame 以正确的方向给出了它的大小
  • 您需要轮流致电[tableView reloadData]。我找不到任何其他方法来更新单元格和标签高度

以下是我为使这项工作为我所采取的步骤:

  1. 禁用 UILabel 上的任何自动调整大小蒙版,因为它们会因某种原因干扰获得正确的标签高度
  2. 在viewDidLoad 上,我抓取了对每个标签字体的引用,以及与纵向中的 tableView 相比标签宽度百分比的浮点数

    CWProductDetailDescriptionCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
    self.descriptionLabelWidthPercentage = cell.descriptionLabel.frame.size.width / 320;
    self.descriptionLabelFont = cell.descriptionLabel.font;
    
  3. 在heightForRowAtIndexPath 中,我使用 tableView 宽度和我已经抓取的百分比来计算单元格高度:

    case TableSectionDescription:
        CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
        CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:self.descriptionLabelFont];
        return newLabelFrameSize.height + kTextCellPadding;
    
  4. 在cellForRowAtIndexPath 中我为标签框架计算框架并更新它

    cell = [tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.text = self.product.descriptionText;
    CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
    CGFloat labelWidth = self.tableView.frame.size.width * self.descriptionLabelWidthPercentage;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(labelWidth, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, labelWidth, newLabelFrameSize.height);
    
  5. 在didRotateFromInterfaceOrientation 你需要[self.tableView reloadData]

这个方法的问题:

  1. 您无法利用 might set in IB 自动调整大小的蒙版和边距
    • 直到您在cellForRowAtIndexPath 中归还单元格后,它们似乎才触发(不确定这是否完全正确)
    • 他们以某种方式弄乱了高度计算,并且您的标签在横向中最终是 taller 而不是 desired。
  2. 如果您的标签宽度在 portrait 和 landscape 中的百分比不相等,则您需要知道这两个百分比
  3. 您需要知道/计算您的标签宽度百分比。理想情况下,您可以在自动调整大小的蒙版完成其操作后即时计算这些。
  4. 只是感觉很笨拙。我感觉在带有缩进的编辑模式下我会遇到更多的麻烦。

所以。这样做的正确方法是什么?我见过很多这样的问题,但没有一个能完全解决这个问题。

【问题讨论】:

  • 它的 BS 苹果造成了这样的问题
  • 我在纯纵向视图中遇到了这些问题。使用具有动态高度的 Storyboard 中的动态原型(例如,以适应各种高度 cmets)会导致视图超出我在 heightForRowAtIndexPath 中告诉它的范围。如果我在 willDisplayCell 中禁用自动调整大小和布局,则 Bob Spryn 下面的回答对我来说大致适用。很想提交一个错误,但不确定如何完全解释它。

标签: ios uikit


【解决方案1】:

显然,我需要写下我的大问题并从电脑上休息一下才能弄清楚。

所以这是解决方案:

动态计算 UILabel 高度的主要步骤,并利用自动调整掩码:

  1. 为每个需要调整高度的单元保留一个原型单元
  2. 在heightForRowAtIndexPath
    • 根据您所在的方向调整参考单元格宽度
    • 然后您就可以可靠地使用包含的 UILabel 框架宽度来计算高度
  3. 除了初始设置外,不要在cellForRowAtIndexPath 中进行任何帧调整
    • UITableViewCells 会在调用后立即执行所有布局内容,因此您的努力充其量是徒劳的,最坏的情况是它们会搞砸
  4. 在willDisplayCell forRowAtIndexPath 中进行框架调整
    • 我很生气,我忘了这件事
    • 它发生在所有 UITableViewCell 内部布局之后,就在它绘制之前
    • 带有自动调整大小蒙版的标签已经具有可用于计算的正确宽度

在 viewDidLoad 中抓取需要调整高度的单元格的引用

请注意,这些是保留/强 (ARC),并且从未添加到表本身

self.descriptionReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"DescriptionCell"];
self.attributeReferenceCell = [self.tableView dequeueReusableCellWithIdentifier:@"AttributeCell"];

在heightForRowAtIndexPath 中计算可变高度单元格

我首先更新了我的参考单元格的宽度,以便它布置其子视图 (UILabels),然后我可以将其用于计算。然后我使用更新的标签宽度来确定我的标签的高度,并添加任何必要的单元格填充来计算我的单元格高度。 (请记住,仅当您的单元格随标签更改高度时才需要进行这些计算)。

UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
float width = UIDeviceOrientationIsPortrait(orientation) ? 320 : 480;

case TableSectionDescription:
    CGRect oldFrame = self.descriptionReferenceCell.frame;
    self.descriptionReferenceCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, width, oldFrame.size.height);
    CGFloat referenceWidth = self.descriptionReferenceCell.descriptionLabel.frame.size.width;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(referenceWidth, MAXFLOAT) AndFont:self.descriptionReferenceCell.descriptionLabel.font];
    return newLabelFrameSize.height + kTextCellPadding;

在cellForRowAtIndexPath不要做任何依赖方向的动态布局相关更新

在willDisplayCell forRowAtIndexPath 中自行更新标签高度

由于表格单元格已执行layoutSubviews,标签已经具有正确的宽度,我用它来计算标签的确切高度。我可以安全地更新我的标签高度。

case TableSectionDescription:
    CGRect oldFrame = ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame;
    CGSize newLabelFrameSize = [self sizeForString:self.product.descriptionText WithConstraint:CGSizeMake(oldFrame.size.width, MAXFLOAT) AndFont:((CWProductDetailDescriptionCell *)cell).descriptionLabel.font];
    ((CWProductDetailDescriptionCell *)cell).descriptionLabel.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.size.width, newLabelFrameSize.height);
    break;

在旋转时重新加载数据

如果您不重新加载数据,您的可见单元格将不会更新其高度和标签。

-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
    [self.tableView reloadData];
}

唯一要指出的另一件事是,我正在使用一个辅助类来计算仅包含基本调用的标签高度。真的它不再节省任何代码了(我曾经有其他的东西在那里),所以直接使用你的字符串的正常方法。

- (CGSize) sizeForString:(NSString*)string WithConstraint:(CGSize)constraint AndFont:(UIFont *)font {
    CGSize labelSize = [string sizeWithFont:font
                          constrainedToSize:constraint 
                              lineBreakMode:UILineBreakModeWordWrap];
    return labelSize;
}

【讨论】:

  • 我可能需要做更多的工作来支持编辑模式。一旦我这样做了,我会添加到这个。
  • 顺便说一句,你认为你真的需要在 didRotateFromInterfaceOrientation 中重新加载数据吗?最坏的情况,您可以只调整帧(类似于您在 willDisplayCell 中的内容),而不是调用 reload,它会重新调用 cellForRowAtIndexPath(即重新创建/重新排列所有可见单元格)。或者,既然您向我指出您可以在 willDisplayCell 中成功使用自动调整大小的蒙版,我发现我不再需要对 didRotateFromInterfaceOrientation 执行任何操作(只要您的 heightForRowAtIndexPath 正确计算您的可变高度)。
  • 是的,看来我愿意。虽然单元格高度可能会自动正确更新,但我需要调整标签高度的大小。所以它要么重新加载数据,要么让单元格本身足够聪明(我认为通过覆盖 layoutSubviews)来更新它们的标签高度。
  • 仅供参考,我发现在单元格中覆盖layoutSubviews 是调整标签本身大小的最有效方法。如果您在 [super layoutSubviews] 之后进行更改,则所有父级调整大小已经发生,您可以做任何您需要的事情。
  • 非常彻底。谢谢。然而,创建对细胞的强引用会破坏系统的细胞回收策略。覆盖 layoutSubviews 通常是建议的方法。
【解决方案2】:

我同意您的大部分意见。不过有几点想法:

你说:

cellForRowAtIndexPath 中的 cell.frame 总是给出它的大小 人像模式。 (即对于 iphone 应用程序,它总是报告 320)。

这不是我的经验。在我的tableView:cellForRowAtIndexPath 中,我只是输入了一个

NSLog(@"Cell frame.size.width=%f", cell.frame.size.width);

它会根据方向报告 320 或 480。所以,将我的帧计算逻辑移到 willDisplayCell 中,我没有看到任何好处。

你说:

在heightForRowAtIndexPath 中,我使用 tableView 宽度和我已经抓取的百分比:

是的,你可以这样做。如果您在单元格右侧的描述可能真的很长,您可能希望将单元格左侧的标签保持固定宽度,并利用更宽的屏幕来真正最大化您的长描述单元格的右侧。但是,如果您经常在右侧有较短的描述,则根据屏幕宽度的目标百分比重新对齐是个好主意。就个人而言,我可能不会根据百分比转换像素,而是使用固定比率(例如,左侧的标签总是占宽度的 33%,右侧的标签总是占单元格的 66%,它可能会简化百分比逻辑,虽然它在功能上是等价的)。

你说:

您无法利用您可能设置的自动调整大小的蒙版和边距 IB

是的,我从未尝试过(在此之前,我看到了与您描述的相同的行为),但即使它确实有效,这也是一个坏主意,因为您确实需要根据环绕您的单词重新计算高度'正在使用,所以虽然它可能会让你接近,但无论如何你仍然需要重新计算高度才能让它正确。 (坦率地说,我只是希望我可以说“根据标签的字体、宽度和文本自动调整高度,但这并不存在。)不过,我同意你的观点,它不能更好地工作很奇怪。自动调整大小应该更优雅地处理这个问题。

你说:

如果您不这样做,您的可见单元格将不会更新它们的高度和标签 重新加载数据。

然后您建议调用reloadData。但是didRotateFromInterfaceOrientation 不是太晚了,因为我看到我的肖像 UILabel 在它进入风景后立即闪现,但在此处调用的风景重绘发生之前。我不想继承 UITableViewCell 并覆盖drawRect,但我不确定在横向再次绘制 UILabel 帧之前如何重新布局它们。我最初尝试将其移至 willRotateToInterfaceOrientation 并没有成功,但也许我没有在那里做某事。

【讨论】:

  • 奇怪的是,当您旋转时会重新计算单元格高度,但 willDisplay 单元格不是。好像我错过了一些东西......似乎我应该能够在重新查询 tableview 单元格高度的同时调整我的 UILabel 框架(但不调用 reloadData),但我不知道如何。
  • 你不能。关于你提到的一些事情。 cellForRowAtIndexPath cell.frame.size 可能会为您在旋转后创建的单元格提供正确的宽度,但当前显示的单元格可能还没有。即使它实际上确实为您提供了正确的宽度,因为在 cellForRowAtIndexPath 调用之后它不会布局其内容(layoutSubviews、autoresize 等),您也不想在那里进行布局。这是来自文档。
  • 另外,您不希望在 willRotateToInterfaceOrientation 中进行任何计算,因为它们仍将基于当前方向和大小。看看我上面的答案,它工作得非常好,允许我使用 IB 并具有灵活的高度和宽度。我认为我的解决方案的替代方法是覆盖单元子类中的 layoutSubviews,并在那里处理调整框架。
  • 我对没有在 cellForRowAtIndexPath 中布置视图的评论感到惊讶,因为这是 Table View Programming Guide 所做的,这是我一直为自定义单元格所做的,并且它有效。你能指出我们在文档中的哪个地方说不要这样做吗?
  • 通过一些实验,我发现在willDisplayCell 中设置自动调整大小掩码绝对可以消除您在原始问题中提到的调整大小错误。
【解决方案3】:

谢谢,这很有帮助。不过,我不想硬编码任何宽度。我花了很长时间研究这个问题,这就是我发现的。

基本上,有一个先有鸡还是先有蛋的问题,表格视图需要知道单元格的高度才能进行自己的布局,但是单元格需要在它们之前配置为包含表格视图的几何形状高度可以确定。

打破这种循环依赖的一种方法是将宽度硬编码为 320 或 480,这是本页其他地方的建议。不过,我不喜欢硬编码大小的想法。

另一个想法是使用原型单元预先计算所有高度。我试过这个,但仍然遇到计算错误的高度,因为创建UITableViewCell 只会留下一个未针对表格视图的实际样式和大小配置的单元格。显然没有办法手动为特定的表格视图配置单元格;你必须让UITableView去做。

我确定的策略是让表格视图像往常一样做所有事情,包括创建和配置自己的单元格,然后一旦设置了所有单元格,计算实际的行高并刷新表格视图。 缺点是表格视图被加载了两次(一次使用默认高度,另一次使用适当的高度),但优点是这应该是一个相当干净和健壮的解决方案。


此示例假定您有一个实例变量 _cellHeights,它是一个数组数组,用于保存每个表格视图部分的计算行高:

@interface MyViewController () {
    NSArray *_cellHeights; // array of arrays (for each table view section) of numbers (row heights)
}

编写一个方法,根据当前单元格内容计算所有单元格高度:

- (void)_recalculateCellHeights
{
    NSMutableArray *cellHeights = [[NSMutableArray alloc] init];

    for (NSUInteger section=0; section<[self.tableView numberOfSections]; section++) {
        NSMutableArray *sectionCellHeights = [[NSMutableArray alloc] init];

        for (NSUInteger row=0; row<[self.tableView numberOfRowsInSection:section]; row++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

            // Set cell's width to match table view's width
            CGRect cellFrame = cell.frame;
            cellFrame.size.width = CGRectGetWidth(self.tableView.frame);
            cell.frame = cellFrame;

            CGFloat cellHeight = self.tableView.rowHeight; // default

            if (cell.textLabel.numberOfLines == 0 || (cell.detailTextLabel && cell.detailTextLabel.numberOfLines == 0)) {
                [cell layoutIfNeeded]; // this is necessary on iOS 7 to give the cell's text labels non-zero frames

                // Calculate the height of the cell based on the text
                [cell.textLabel sizeToFit];
                [cell.detailTextLabel sizeToFit];

                cellHeight = CGRectGetHeight(cell.textLabel.frame) + CGRectGetHeight(cell.detailTextLabel.frame);

                // This is the best I can come up with for this :(
                if (self.tableView.separatorStyle != UITableViewCellSeparatorStyleNone) {
                    cellHeight += 1.0;
                }
            }

            [sectionCellHeights addObject:[NSNumber numberWithFloat:cellHeight]];
        }

        [cellHeights addObject:sectionCellHeights];
    }

    _cellHeights = cellHeights;
}

在-tableView:cellForRowAtIndexPath: 中,像往常一样创建一个单元格并填写 textLabel,而不执行任何布局(正如 Bob 在本页其他地方提到的那样:“UITableViewCells 在调用此操作后立即执行所有布局内容,因此您的努力充其量是徒劳的,最坏的情况是他们把事情搞砸了”)。重要的是要将标签的numberOfLines 属性设置为0,以允许动态高度。

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

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

        // Assign any fonts or other adjustments which may affect height here

        // Enable multiple line support so textLabel can grow vertically
        cell.textLabel.numberOfLines = 0;
    }

    // Configure the cell...
    cell.textLabel.text = [self.texts objectAtIndex:indexPath.section];

    return cell;
}

在-tableView:heightForRowAtIndexPath: 中,返回预先计算的单元格高度:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (_cellHeights) {
        NSArray *sectionCellHeights = [_cellHeights objectAtIndex:indexPath.section];
        return [[sectionCellHeights objectAtIndex:indexPath.row] floatValue];
    }

    return self.tableView.rowHeight; // default
}

在-viewDidAppear: 中,触发重新计算单元格高度并刷新表格。请注意,这必须发生在-viewDidAppear: 时间,而不是-viewWillAppear: 时间,因为在-viewWillAppear: 时间,尚未为表格视图的几何结构配置单元格。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self _recalculateCellHeights];
    [self.tableView reloadData];
}

在-didRotateFromInterfaceOrientation:,做同样的事情:

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [self _recalculateCellHeights];
    [self.tableView reloadData];
}

-tableView:willDisplayCell:forRowAtIndexPath: 不需要做任何特别的事情。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多