15

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

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

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

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

动态UILabel/单元格高度/宽度

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

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

  • cell.framecellForRowAtIndexPath总是在纵向模式下给出它的大小。(即对于 iphone 应用程序,它总是报告 320)。
  • 将原型单元格存储在属性中以供参考具有相同的问题,始终处于纵向模式
  • 在这个用例中,自动调整标签宽度的掩码似乎没用,实际上会导致计算的旋转高度出现问题
  • 中的 tableView.frame 以cellForRowAtIndexPath正确的方向给出其大小
  • 你需要调用[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. 您无法利用可能在 IB 中设置的自动调整大小掩码和边距
    • 直到您返回单元格之后,那些似乎才触发cellForRowAtIndexPath (不确定这是否完全正确)
    • 他们以某种方式弄乱了高度计算,并且您的标签最终景观中的期望高。
  2. 如果您的标签宽度在纵向横向中的百分比不相等,则您需要知道这两个百分比
  3. 您需要知道/计算您的标签宽度百分比。理想情况下,您可以在自动调整大小的蒙版完成其操作后即时计算这些。
  4. 只是感觉很笨拙。我有一种感觉,在带有缩进的编辑模式下我会遇到更多的麻烦。

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

4

3 回答 3

12

Apparently it took writing out my giant question and taking a break from the computer to figure it out.

So here's the solution:

The main steps to dynamically calculate UILabel heights on the fly, and take advantage of autoresizing masks:

  1. Keep a prototype cell for each cell that will need to have its height adjusted
  2. In heightForRowAtIndexPath
    • adjust your reference cell width for the orientation you are in
    • thenyou can then reliably use the contained UILabel frame widths for calculating height
  3. Don't do any frame adjustments in cellForRowAtIndexPath aside from initial setup
    • UITableViewCells perform all their layout stuff right after this is called, so your efforts at best are futile, at worst they screw things up
  4. Do your frame adjustments in willDisplayCell forRowAtIndexPath
    • I'm furious I forgot about this
    • It occurs after all the UITableViewCell internal layout and right before it draws
    • Your labels with autoresizing masks on them will already have correct widths you can use for calculations

In viewDidLoad grab references to cells that will need height adjustments

Note that these are retained/strong(ARC), and never added to the table itself

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

In heightForRowAtIndexPath do calculation for variable height cells

I first update the width of my reference cell so that it lays out its subviews (UILabels), which I can then use for calculations. Then I use the updated label width to determine the height of my labels, and add any necessary cell padding to calculate my cell height. (Remember these calculations are only necessary if your cell changes heights along with your label).

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;

In cellForRowAtIndexPath Don't do any dynamic layout related updates relying on orientation

In willDisplayCell forRowAtIndexPath Update the label heights themselves

Since the table cells have performed layoutSubviews, the labels already have the correct widths, which I use to calculate the exact height of the labels. I can safely update my label heights.

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;

Reload Data on rotate

Your visible cells won't update their heights and labels if you don't reload data.

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

The only other thing to point out is that I'm using a helper class for calculating the label height that just wraps the basic call. Really its not saving much of any code anymore (I used to have other stuff in there), so just use the normal method straight off your string.

- (CGSize) sizeForString:(NSString*)string WithConstraint:(CGSize)constraint AndFont:(UIFont *)font {
    CGSize labelSize = [string sizeWithFont:font
                          constrainedToSize:constraint 
                              lineBreakMode:UILineBreakModeWordWrap];
    return labelSize;
}
于 2012-04-20T04:16:22.277 回答
0

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

你说:

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 和 override drawRect,但我不确定在横向绘制 UILabel 帧之前如何重新布局它们。我最初尝试将其移至此willRotateToInterfaceOrientation并没有奏效,但也许我没有在那里做某事。

于 2012-04-20T05:19:18.393 回答
0

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

基本上,有一个先有鸡还是先有蛋的问题,表格视图需要知道单元格的高度才能进行自己的布局,但是单元格需要针对包含表格视图的几何形状进行配置,然后才能确定其高度决定。

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

另一个想法是使用原型单元预先计算所有高度。我尝试了这个,但仍然遇到计算错误的高度,因为创建 aUITableViewCell只会留下一个未针对表格视图的实际样式和大小配置的单元格。显然没有办法手动为特定的表格视图配置单元格;你必须让它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:.

于 2013-09-26T20:48:25.203 回答