代码改变世界

iOS学习之路十三(动态调整UITableViewCell的高度)

2013-09-08 09:20  Lves Li  阅读(2353)  评论(0编辑  收藏  举报

大概你第一眼看来,动态调整高度是一件不容易的事情,而且打算解决它的第一个想法往往是不正确的。在这篇文章中我将展示如何使图表单元格的高度能根据里面文本内容来动态改变,同时又不必子类化UITableViewCell。你当然可以通过子类化它来实现,但是这样做会使得代码复杂因为设置高度是在图表本身的实例上而不是对单元格操作。下面你将会看到这其实是一件轻而易举的事情。对于图表来说能够动态调整高度是件很有意义的事情,我首先想到的需要这个功能的是当显示一列长度会变化的文本列表时,如果文本内容较少,它或许能够适合正常的单元格label,但是如果文本变长,就不得不重新设置单元格大小以便于显示全部的文本内容。我总结了重新设置单元格大小的主要步骤如下:

1 创建并添加一个UILabel作为单元格cell的子视图;
2 在UITableView的委托方法: (CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath: (NSIndexPath *) indexPath中计算高度
3 在UITableView的委托方法: (UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath: (NSIndexPath *) indexPath中计算UILabel的框大小。

下面我要详细介绍这些步骤,首先看一下程序输出截图:

在普通的图表中,你可以简单地用下面的方法设置单元格内label的文本内容:

[[cell textLabel] setText:@"Text for the current cell here."];

也许你认为这样做就可以完全控制UILabel了,但是我发现我的任何要改变UILabel框大小的尝试都失败了,因此这并不是实现动态调整大小的一个好的候选方案。

我们需要设计一个UILabel然后把它添加到单元格的内容视图中。要实现它需要调用-cellForRowAtIndexPath,大致内容如下所示:

 

  1. - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath  
  2. {  
  3.   UITableViewCell *cell;  
  4.   UILabel *label = nil;  
  5.    
  6.   cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];  
  7.   if (cell == nil)  
  8.   {  
  9.     cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];  
  10.    
  11.     label = [[UILabel alloc] initWithFrame:CGRectZero];  
  12.     [label setLineBreakMode:UILineBreakModeWordWrap];  
  13.     [label setMinimumFontSize:FONT_SIZE];  
  14.     [label setNumberOfLines:0];  
  15.     [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];  
  16.     [label setTag:1];  
  17.    
  18.     [[cell contentView] addSubview:label];  
  19.   }  
  20. }  

 

 

这并不是完整的代码因为我们仅仅在创建单元格的时候初始化它的label,这段代码对应调用-dequeueReusableCellWithIdentifier之后的判断模块if(cell == nil)。
在这里我想强调两点:第一个,我们可以注意到label有一个标签与其对应,因为调用了-setTag:1。当cell不等于nil时这个标签可以用到。第二点,我们通过调用[[cell contentView] addSubview:label]来将label添加到单元格的内容视图中,这个只是在label初始化的时候用到。每调用这个函数都会添加label到子视图序列中。下面我们会将这段代码补充完整,但之前先让我们看一下如何设置cell的高度。

计算cell的高度

在一个复杂的cell中,计算高度可能比较困难,但是你只需要关心那些高度会变化的部件就可以了。在我的例子中,唯一需要处理的就是添加到单元格中的label。我们根据文本的大小来计算cell 的高度,而文本的大小取决于文本的长度和文本字体。NSString类提供了函数-sizeWithFont来方便我们获取cell 的大小。下面的代码介绍了函数-heightForRowAtIndexPath:

  1. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;  
  2. {  
  3.   NSString *text = [items objectAtIndex:[indexPath row]];  
  4.    
  5.   CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);  
  6.    
  7.   CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];  
  8.    
  9.   CGFloat height = MAX(size.height, 44.0f);  
  10.    
  11.   return height + (CELL_CONTENT_MARGIN * 2);  
  12. }  

 

你会注意到我们用到了几个常量来计算cell 的大小,它们的定义如下所示:

#define FONT_SIZE 14.0f
#define CELL_CONTENT_WIDTH 320.0f
#define CELL_CONTENT_MARGIN 10.0f

常量CELL_CONTENT_WIDTH是整个cell的宽度。CELL_CONTENT_MARGIN是我们定义的页边空白,FONT_SIZE是我们采用文本的字体大小。

首先我们要创建一个内容宽度的约束条件。CGSizeMake的第一个参量是总共的内容宽度减去两个页边空白。因为左边和右边各有一个页边空白。第二个参数是我们提供的最大数值。这个约束条件在后面的函数-sizeWithFont中将会用到。在-sizeWithFont中我们设置为UILineBreakModeWordWrap来获取在允许自动换行的情况和上面提到的约束条件下正确的大小。最后我们使用MAX宏设置cell的高度,并且保证cell 的高度不会小于44个像素,因为它返回size.height和44两个数中的最大值。最后,我们将上下的页边空白考虑进去得到最后的结果。

为了使得读者形象化的了解页边空白,下面一个截图可以看出有一个边界环绕着label。调用[[label layer] setBorderWidth:2.0f]可以显示该边界从而方便我们看到页边空白。

计算并设置UILabel框大小

在前面我们用来计算高度的方法也是我们用来设置UILabel框大小的方法。下面将-cellForRowAtIndexPath代码补充完整:

  1. - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath  
  2. {  
  3.   UITableViewCell *cell;  
  4.   UILabel *label = nil;  
  5.    
  6.   cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];  
  7.   if (cell == nil)  
  8.   {  
  9.     cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];  
  10.    
  11.     label = [[UILabel alloc] initWithFrame:CGRectZero];  
  12.     [label setLineBreakMode:UILineBreakModeWordWrap];  
  13.     [label setMinimumFontSize:FONT_SIZE];  
  14.     [label setNumberOfLines:0];  
  15.     [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];  
  16.     [label setTag:1];  
  17.    
  18.     [[label layer] setBorderWidth:2.0f];  
  19.    
  20.     [[cell contentView] addSubview:label];  
  21.    
  22.   }  
  23.   NSString *text = [items objectAtIndex:[indexPath row]];  
  24.    
  25.   CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);  
  26.    
  27.   CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];  
  28.    
  29.   if (!label)  
  30.     label = (UILabel*)[cell viewWithTag:1];  
  31.    
  32.   [label setText:text];  
  33.   [label setFrame:CGRectMake(CELL_CONTENT_MARGIN, CELL_CONTENT_MARGIN, CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), MAX(size.height, 44.0f))];  
  34.    
  35.   return cell;  
  36. }  

 

要注意if(cell == nil)模块是初始化代码,只在cell创建的时候运行一次。该模块外部代码每次都会执行只要在每次数据更新或者窗口拖拽之后调用了-cellForRowAtIndexPath。

也就是说,每次都需要设置label中文本内容以及设置label外框大小。注意如果label处于未初始化状态,我们需要通过调用[cell viewWithTag:1]来获取UILabel的句柄。这段代码跟前面计算高度的代码基本相同。

总结

动态计算单元格cell的高度真的并不困难。如果你有一个很复杂的cell,你只需要根据内容宽度和特定文本字体的大小来确定cell的高度。如果你不清楚你的外框显示在什么地方,只需要通过调用[[view layer] setBorderWidth:2.0f]来使外框显示即可。这会有助于你了解绘图过程以及更快地在更深的层次理解绘图显示的问题。

演示工程文件:DynamicHeights Demo Project

作者:Matt Long
原文链接:http://www.cimgf.com/2009/09/23/uitableviewcell-dynamic-height/
原文:
UITableViewCell Dynamic Height
At first glance setting a height dynamically for table view cells seems a little daunting and the first most obvious answers that come to mind are not necessarily correct. In this post I will show you how to set your table view cell heights dynamically based upon the text content without subclassing UITableViewCell. You can subclass it, however, doing so does not make the code much cleaner as setting the height is done in your delegate for the table view itself rather than the cell anyhow. Read on to see what you need to know to make dynamic cell height sizing a breeze. There are probably numerous reasons why you might want dynamic heights for your table view cells, but the one I’ve run into most is the need to resize because I am displaying lists of text objects with varying lengths. When the text is short, it might fit in the normal cell label, however, if the text gets longer, you will want to resize the cell so that you can display the complete content. I’ve distilled the process of resizing table cells to a few rules of thumb. Here they are:

  • Create, configure, and add a UILabel as a subview of the contentView in the cell.
  • Calculate the height in the UITableView delegate method, – (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
  • Calculate the frame for the UILabel in the UITableView delegate method, – (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

I am going to cover each of these rules in detail, but take a look at the output of the example project in the screenshot.

In simpler table view based applications, you can simply set the text of the table view cell’s text label like this:

[[cell textLabel] setText:@"Text for the current cell here."];

Doing so might make you think that you can manipulate the UILabel that the cell uses, however, I’ve found my attempts to change the UILabel’s frame get ignored completely, so it is not a good candidate for use with our dynamic resizing code.

Instead what we need to do is programatically create a UILabel and add it to the cell’s content view. Do this in the call to -cellForRowAtIndexPath. Use something like the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell;
  UILabel *label = nil;
 
  cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];
  if (cell == nil)
  {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];
 
    label = [[UILabel alloc] initWithFrame:CGRectZero];
    [label setLineBreakMode:UILineBreakModeWordWrap];
    [label setMinimumFontSize:FONT_SIZE];
    [label setNumberOfLines:0];
    [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];
    [label setTag:1];
 
    [[cell contentView] addSubview:label];
  }
}

This is not the completed code as you’ll notice that we have initialized the label only when the cell needs created for the first time, that is if (cell == nil) after a call to -dequeueReusableCellWithIdentifier. There are two points I want to address in relation to this. First, notice the label has a tag associated with it after a call to -setTag:1. This will be used in the case where the cell is not equal to nil after a call to -dequeueReusableCellWithIdentifier. In that case we will need to get a handle to the label by calling [cell viewWithTag:1] which will return the view that we associated with that tag. Second, notice that we have added our label to the cell’s content view with a call to [[cell contentView] addSubview:label]. This is done when the label is initialized and should only be done once. Adding it each time this method is called will add the label again to the subviews array. We will come back to this code to finish it in a minute, but first let’s take a look at how we can set the height for our cell now that our label has been added.

Calculate the Cell Height

In a complex cell, your calculations could get a bit challenging, however, you only need to worry with the items that will change in height. In our example, the only item you need to deal with is the label that we added. We calculate the height of the cell by determining the size of the text based on the length of the text and the font we intend to use. The NSString class provides a method called -sizeWithFont that enables us to obtain this size. The following code show how we implement our call to -heightForRowAtIndexPath:

1
2
3
4
5
6
7
8
9
10
11
12
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
  NSString *text = [items objectAtIndex:[indexPath row]];
 
  CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);
 
  CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
 
  CGFloat height = MAX(size.height, 44.0f);
 
  return height + (CELL_CONTENT_MARGIN * 2);
}

You will notice that we have several constants we are using to calculate the size of our cell. These are defined as follows:

#define FONT_SIZE 14.0f
#define CELL_CONTENT_WIDTH 320.0f
#define CELL_CONTENT_MARGIN 10.0f

The constant CELL_CONTENT_WIDTH is the width of the entire cell. CELL_CONTENT_MARGIN is the margin we want to use all the way around the cell as the content inset, and of course FONT_SIZE is the size of the font we want to use for the label text.

The first place we use these is to create a constraint with the content width. Notice that CGSizeMake takes as its first parameter the total content width minus the margin times 2. This subtracts the margin from the left and the margin from the right from the total width to have the actual width of the label. The second parameter is just a maximum number we provide. The call to -sizeWithFont will set this to the actual height in the next line. This call to -sizeWithFont calculates the size according to the constant UILineBreakModeWordWrap which causes it to return the correct size for word wrap–which is why the width is important to get right. Next we set our height for the cell using a call to the MAX macro. This will ensure that our cell height will never be shorter than the default 44 pixels as MAX returns the larger of the two variables. Finally, we add our margin height back into the height for both top and bottom (hence x 2) and then return the result.

To help visualize how the margin is working, take a look at the following screenshot to see what each label looks like with a border around it. Turning this on with a call to [[label layer] setBorderWidth:2.0f] on the UILabel we added in the previous section makes it clear where the margins are.

Calculate the UILabel Frame and Set It

The same calculation we used to determine the height in the previous section is the code we use to set the frame for the UILabel we added in the beginning. To complete this tutorial we will finish out our implementation of -cellForRowAtIndexPath with the following code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell *cell;
  UILabel *label = nil;
 
  cell = [tv dequeueReusableCellWithIdentifier:@"Cell"];
  if (cell == nil)
  {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Cell"] autorelease];
 
    label = [[UILabel alloc] initWithFrame:CGRectZero];
    [label setLineBreakMode:UILineBreakModeWordWrap];
    [label setMinimumFontSize:FONT_SIZE];
    [label setNumberOfLines:0];
    [label setFont:[UIFont systemFontOfSize:FONT_SIZE]];
    [label setTag:1];
 
    [[label layer] setBorderWidth:2.0f];
 
    [[cell contentView] addSubview:label];
 
  }
  NSString *text = [items objectAtIndex:[indexPath row]];
 
  CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);
 
  CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
 
  if (!label)
    label = (UILabel*)[cell viewWithTag:1];
 
  [label setText:text];
  [label setFrame:CGRectMake(CELL_CONTENT_MARGIN, CELL_CONTENT_MARGIN, CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), MAX(size.height, 44.0f))];
 
  return cell;
}

Just remember that anything done within the if (cell == nil) block is initialization code and should only be done when the cell is first created. Anything done outside of the block will be used every time the -cellForRowAtIndexPath is called, which is any time the data gets reloaded or the view gets scrolled.

That being said, you will see that the only thing we do every time it gets called is setting the text of the current item and setting the label’s frame for the current item (lines 32 and 33). Notice that we got a handle to our UILabel by calling [cell viewWithTag:1] (lines 29 and 30) in the case where the label is nil in subsequent/non-initialization calls to this method. You will notice that our frame calculation code is exactly the same as what we used in the previous section to determine the row height.

Conclusion

Calculating dynamic cell heights is really not too hard. If you have a very complex cell, just remember that all you really need to calculate is the height based up a width that shouldn’t change and the size of the text of a certain font (unless of course you support both portrait and landscape modes–which makes things a little more challenging. I will, however, leave this as an exercise for the reader). If you find yourself wondering where your actual frame is displaying for a given view, just turn on the view border by calling [[view layer] setBorderWidth:2.0f]. This will help you see what is going on and give you the ability get to the bottom of your display problems quicker. Until next time.
DynamicHeights Demo Project