写点什么

在 MonoTouch 中自定义表格

2011 年 1 月 13 日

完整例子的源代码可以在这里下载。

为什么要定制表格?

表格在很多iPhone 应用程序中都是必需的UI 元素。虽然对于应用程序开发而言,这并非是一项新发明,鉴于设备尺寸等方面的限制,表格在iPhone 中的功能是非常固定的。

苹果在其SDK 中,直接内置了很多风格来让你定制表格。不过,在你最初创建表格的时候,它看起来非常简单。在没有进行任何定制的时候,你可以为表格选择两种基本风格,默认风格和分组风格:

在对表格中的单元格进行一点调整后,你就可以添加图标和说明文字:

你甚至能改变单元格的字体和颜色,然而,有时候这样还是不足够。如果你真的想完全改变基本的风格,创建一个复杂的UI,那么你必须创建自己的自定义单元格控件。下面的截图是定制后的效果,这个应用程序使用完全定制的单元格来显示内容:

幸好,苹果让我们有便捷的方法来完成这样的定制,就是允许我们创建自己的定制 UITableViewCell 控件,并把其用于UITableView 控件中。下面,让我们来逐步创建一个应用程序,学习如何利用UITableViewUITableViewCell 控件来创建上图所示效果的例子。

示例应用程序

那么接下来,我们就来创建应用程序。首先,打开 MonoDevelop,如下图所示:

接着,新建一个iPhone MonoTouch 项目。从“File”菜单中,选择“New Soluton”:

从C# iPhone 模板中选择“iPhone Window-based Project”,输入解决方案的名称“Example_CustomUITableViewCells”:

在项目特性对话框上点击“OK”,这些内容和我们要做的应用程序无关:

我们的项目现在创建好了,如下图所示:

通过在Solution Explorer 窗口里双击MainWindow.xib,以便在Interface Builder 中打开它,之后你会看到如下内容:

我们首先拖一个UITableView 控件到我们的MainWindow 中,效果如下:

我们需要在AppDelegate 类中添加一个outlet,以便我们能编程访问这个Table View。

在Library 窗口中,选择顶部的“Classes”标签页,接着在下拉列表中选择“Other Classes”。确保AppDelegate 已经选中,在Library 窗口的下半部点击“Outlets”标签页:

让我们来创建一个名为“tblMain”的outlet,通过点击“+”按钮,在名称列输入“tblMain”,你的outlet 应该类似下图的:

接着,把outlet 关联到我们之前添加的UITableView 上。在Document 窗口中选择AppDelegate,在Connections Inspector 窗口中,就会看到新创建的 tblMain outlet:

从连接管理器中把右侧的outlet 的圆点拖动到Document 窗口或Window Designer 的表格上,就可以把它们关联到一起:

现在,我们得到了一个关联过的表格,让我们回到MonoDevelop,编写一些代码。

在项目上按右键,在上下文菜单中选择“Add”,接着选择“New Folder”,把文件夹命名为“Code“。

接着创建一个命名为“BasicTableViewItem.cs”的类。即,在项目上再次按右键,在上下文菜单中选择“Add”,接着选择“New File”。命名类,并点击“New”:

在这个代码文件中,输入如下代码:

复制代码
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>class</span> <span>BasicTableViewItem</span>
{
<span>public</span> <span>string</span> Name { <span>get</span>; <span>set</span>; }
<span>public</span> <span>string</span> SubHeading { <span>get</span>; <span>set</span>; }
<span>public</span> <span>string</span> ImageName { <span>get</span>; <span>set</span>; }
<span>public</span> BasicTableViewItem ()
{
}
}
}

这个类相当简单,它表示了一个在单元格中的项目,并具有如下属性:

  • Name ——这个项目所显示的文本
  • SubHeading ——显示在项目名称下面的文本
  • ImageName ——这个项目所显示的图片名称

我们将在后面会看到如何使用这个类。

接着,在“Code”文件夹中,创建另外一个类,命名为“BasicTableViewItemGroup”,并输入如下代码:

复制代码
<span>using</span> System;
<span>using</span> System.Collections.Generic;
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>class</span> <span>BasicTableViewItemGroup</span>
{
<span>public</span> <span>string</span> Name { <span>get</span>; <span>set</span>; }
<span>public</span> <span>string</span> Footer { <span>get</span>; <span>set</span>; }
<span>public</span> <span>List</span><<span>BasicTableViewItem</span>> Items
{
<span>get</span> { <span>return</span> this._items; }
<span>set</span> { <span>this</span>._items = <span>value</span>; }
}
<span>protected</span> <span>List</span><<span>BasicTableViewItem</span>> _items = <span>new</span> <span>List</span><<span>BasicTableViewItem</span>>();
<span>public</span> BasicTableViewItemGroup ()
{
}
}
}

这也是一个相当简单的类。它用于项目分组的容器。具有如下属性:

  • Name ——分组的名称。通常显示在分组的顶部。
  • Footer ——显示在当前组中所有项目的底部文字
  • Items ——分组所包含的 BasicTableViewItem 的 List<> 集合。我们在其构造的时候实例化这个集合,以便在添加项目的时候无需手动去实例化它。

接着,再在同一个文件夹中创建另外一个类,命名为“BasicTableViewSource”,并输入如下代码:

复制代码
<span>using</span> System;
<span>using</span> System.Collections.Generic;
<span>using</span> MonoTouch.UIKit;
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>class</span> <span>BasicTableViewSource</span> : <span>UITableViewSource</span>
{
<span>protected</span> <span>List</span><<span>BasicTableViewItemGroup</span>> _tableItems;
<span>string</span> _cellIdentifier = <span>"BasicTableViewCell"</span>;
<span>public</span> BasicTableViewSource (<span>List</span><<span>BasicTableViewItemGroup</span>> items)
{
<span>this</span>._tableItems = items;
}
<span>public</span> <span>override</span> <span>int</span> NumberOfSections (<span>UITableView</span> tableView)
{
<span>return</span> this._tableItems.Count;
}
<span>public</span> <span>override</span> <span>int</span> RowsInSection (<span>UITableView</span> tableview, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Items.Count;
}
<span>public</span> <span>override</span> <span>string</span> TitleForHeader (<span>UITableView</span> tableView, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Name;
}
<span>public</span> <span>override</span> <span>string</span> TitleForFooter (<span>UITableView</span> tableView, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Footer;
}
<span>public</span> <span>override</span> <span>UITableViewCell</span> GetCell (<span>UITableView</span> tableView, MonoTouch.Foundation<span>NSIndexPath</span> indexPath)
{
<span>UITableViewCell</span> cell = tableView.DequeueReusableCell (<span>this</span>._cellIdentifier);
<span>if</span> (cell == <span>null</span>)
{
cell = <span>new</span> <span>UITableViewCell</span> (UITableViewCellStyle.Default, <span>this</span>._cellIdentifier);
}
<span>BasicTableViewItem</span> item = <span>this</span>._tableItems[indexPath.<span>Section</span>].Items[indexPath.<span>Row</span>];
cell.TextLabel.<span>Text</span> = item.Name;
<span>return</span> cell;
}
}
}

UITableViewSource 类负责处理我们数据的绑定,也处理用户和表格的交互。在我们的例子中,处理用户交互超出本篇文章的范围,不过我们依然需要完成数据绑定的功能。

这个类比其他两个稍微有点复杂,它的成员逐一说明如下:

  • 变量声明——我们声明了两个变量,**_tableItems,用于获取表格对象的本地变量,_cellIdentifier,** 在后面在重用表格单元格的时候提供一个关键字。
  • 构造器——构造器接受一个名为items 的参数,其类型是 **List。** 在我们的例子中,我们强制类和项目分组一起实例化,以便我们确信他们都是有效的。
  • NumberOfSections ——这个方法用于获取在表格中创建的分组数目。在我们的例子中,就返回_tableItems的分组数目。
  • RowsInSection ——这个方法用于获取表格中特定分组的项目数目。根据特定的分组索引,返回其包含项目的数量。
  • TitleForHeader ——这个方法返回某个分组标头的文本。就是返回当前分组对象BasicTableViewItemGroupName 属性。
  • TitleForFooter ——这个方法返回某个分组标脚的文本。类似TitleForHeader,不过这次是返回 Footer 属性。
  • GetCell ——这个方法根据特定分组和行返回实际的UITableCell 控件,这也是数据绑定大量工作发生的地方。首先,我们在相关的UITableView上调用DequeueReusableCell 。这样做主要是因为性能原因。因为 iPhone 限制了处理器的耗能,如果每次显示都必须创建新的UITableViewCell 的话,在遇到很长列表的时候可能会变得非常慢。为了解决这个问题,UITableView 控件中后台维护着一个UITableViewCell 控件缓存池,在单元格滚动出可视范围的时候,它会放入到这个缓存池中以备重用。这也就是_cellIdentifier变量的用武之处。

好,现在我们编写好了相关的类,就要开始添加一些图片,以便可以在项目上显示图标。

首先,在项目中类似之前创建“Code”文件夹那样,创建一个名为“Images”的文件夹。把如下的图片按照对应的名称逐一保存到你的硬盘上:

图片

名称

PawIcon.png

LightBulbIcon.png

PushPinIcon.png

TargetIcon.png

一旦保存好,就要把他们添加到项目中。在“Images”文件夹上点右键,选择“Add Files”。选择你保存好的图片,勾中“Override default build action”,在下拉列表中选择“Content”。这很重要。Content 是让你在运行时可以加载并使用它们的唯一构建动作。

你的解决方案现在应该如下所示:

为了确保你之前的步骤都是正确的,可以通过编译项目来检验。要进行编译,按住 Apple Key(苹果键),再按“B”键,或者从“Build”菜单中选择”Build All“。

一切正常的话,我们就准备好了相关的类和图片。让我们来编辑应用程序代码以使用它们!

在 Main.cs 上双击,显示出代码编辑器。在 AppDelegate 类中,添加如下代码行:

<span>BasicTableViewSource</span> _tableViewSource;This is a class variable to hold our BasicTableViewSource that we created earlier.

这是一个类变量,用来保存我们早期创建的BasicTableViewSource 实例。

接着,把如下方法复制到 AppDelegate 类中:

复制代码
<span>protected</span> <span>void</span> CreateTableItems ()
{
<span>List</span><<span>BasicTableViewItemGroup</span>> tableItems = <span>new</span> <span>List</span><<span>BasicTableViewItemGroup</span>> ();
<span>BasicTableViewItemGroup</span> tGroup;
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Birds"</span>, Footer = <span>"Birds have wings, and sometimes use them."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Crow"</span>, SubHeading = <span>"AKA, Raven."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Chicken"</span>, SubHeading = <span>"Males are called roosters."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Turkey"</span>, SubHeading = <span>"Eaten at thanksgiving."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Fish"</span>, Footer = <span>"Fish live in water. Mostly."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Trout"</span>, SubHeading = <span>"Rainbow is a popular kind."</span>, ImageName = <span>"TargetIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Salmon"</span>, SubHeading = <span>"Good sushi."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Cod"</span>, SubHeading = <span>"Flat fish."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Mammals"</span>, Footer = <span>"Mammals nurse their young."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Deer"</span>, SubHeading = <span>"Bambi."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Bats"</span>, SubHeading = <span>"Fly at night."</span>, ImageName = <span>"TargetIcon.png"</span> });
tableItems.Add (tGroup);
<span>this</span>._tableViewSource = <span>new</span> <span>BasicTableViewSource</span>(tableItems);
}

我们将调用这个方法来创建数据源,完成在表格中显示数据的过程。

最终,在AppDelegate 类中的FinishedLaunching 方法中,在调用window.MakeKeyAndVisible 之前加入如下代码。

复制代码
<span>this</span>.CreateTableItems ();
<span>this</span>.tblMain.Source = <span>this</span>._tableViewSource;

通过调用这个方法,我们就创建了一个表格数据源,并把数据源赋值给表格。

最终的 Main.cs 应该像这样:

复制代码
<span>using</span> System;
<span>using</span> System.Collections.Generic;
<span>using</span> System.Linq;
<span>using</span> MonoTouch.Foundation;
<span>using</span> MonoTouch.UIKit;
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>class</span> <span>Application</span>
{
<span>static</span> <span>void</span> Main (<span>string</span>[] args)
{
<span>UIApplication</span>.<span>Main</span> (args);
}
}
<span>public</span> <span>partial</span> <span>class</span> <span>AppDelegate</span> : <span>UIApplicationDelegate</span>
{
<span>BasicTableViewSource</span> _tableViewSource;
<span>public</span> <span>override</span> <span>bool</span> FinishedLaunching (<span>UIApplication</span> app, <span>NSDictionary</span> options)
{
<span>this</span>.CreateTableItems ();
<span>this</span>.tblMain.Source = <span>this</span>._tableViewSource;
window.MakeKeyAndVisible ();
<span>return</span> <span>true</span>;
}
<span>public</span> <span>override</span> <span>void</span> OnActivated (<span>UIApplication</span> application)
{
}
<span>protected</span> <span>void</span> CreateTableItems ()
{
<span>List</span><<span>BasicTableViewItemGroup</span>> tableItems = <span>new</span> <span>List</span><<span>BasicTableViewItemGroup</span>> ();
<span>BasicTableViewItemGroup</span> tGroup;
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Birds"</span>, Footer = <span>"Birds have wings, and sometimes use them."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Crow"</span>, SubHeading = <span>"AKA, Raven."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Chicken"</span>, SubHeading = <span>"Males are called roosters."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Turkey"</span>, SubHeading = <span>"Eaten at thanksgiving."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Fish"</span>, Footer = <span>"Fish live in water. Mostly."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Trout"</span>, SubHeading = <span>"Rainbow is a popular kind."</span>, ImageName = <span>"TargetIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Salmon"</span>, SubHeading = <span>"Good sushi."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Cod"</span>, SubHeading = <span>"Flat fish."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Mammals"</span>, Footer = <span>"Mammals nurse their young."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Deer"</span>, SubHeading = <span>"Bambi."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Bats"</span>, SubHeading = <span>"Fly at night."</span>, ImageName = <span>"TargetIcon.png"</span> });
tableItems.Add (tGroup);
<span>this</span>._tableViewSource = <span>new</span> <span>BasicTableViewSource</span>(tableItems);
}
}
}

让我们来运行下应用程序,看看什么样子。要运行应用程序,按住苹果键,并按”Enter”按钮,或者从 Run 菜单中选择 Debug。

当我们运行它之后,就可以看到下图所示效果:

脚标看上去稍显怪异。让我们来把表格的类型改为Grouped 后重新运行一下。要这样做,需要在 Interface Builder 中打开 MainWindow.xib ,然后在 Document 窗口上点击 Table View 来编辑表格风格,接着在 Inspector 窗口,通过风格下拉列表来改变为“Grouped”:

保存文件,切换回MonoDevelop,再次运行应用程序,你就可以看到如下效果了:

现在脚标就看着顺眼多了。

让我们再来处理其他地方。在项目中要显示一些图片和子标题,那么下面就要着手编写。

停止应用程序,接着双击BasicTableViewSource.cs 类来编辑它。在 GetCell method 方法中把如下代码行:

复制代码
cell = <span>new</span> <span>UITableViewCell</span> (UITableViewCellStyle.Default, <span>this</span>._cellIdentifier);

改为:

复制代码
cell = <span>new</span> <span>UITableViewCell</span> (UITableViewCellStyle.Subtitle, <span>this</span>._cellIdentifier);

这样就把 UITableViewCell 的风格改变为 Subtitle ,其会在单元格中显示文本的第二行。

你能选用的只有很少的风格。然而要注意,只有在DefaultSubtitle 风格中才支持图片。

现在,在这行之后:

复制代码
cell.TextLabel.<span>Text</span> = item.Name;

增加:

复制代码
cell.Accessory = UITableViewCellAccessory.Checkmark;
cell.DetailTextLabel.<span>Text</span> = item.SubHeading;
<span>if</span>(!<span>string</span>.IsNullOrEmpty(item.ImageName))
{
cell.ImageView.<span>Image</span> = UIImage.FromFile(<span>"Images/"</span> + item.ImageName );
}

UITableViewCells 的原来样子就会被改变为如下所示:

UITableViewCell 控件原本带有两个主要部分,单元格主体区,其是单元格显示内容的全部区域;单元格内容区,其通常用来放置内容在里面。如果附加区未使用的话,单元格内容区就会扩展占满整个单元格主体区。类似地,如果图片区不使用的话,标签区会覆盖图片的地方。

让我们来看 一下如何在代码中使用这些区域。第一行代码告诉单元格,右边会有一个Checkmark 。接着我们添加SubHeading 文本到DetailTextLabel 上。最后,我们在单元格上设置一个图片。

很容易就可以通过文件来创建图片对象。简单地使用 UIImage.FromFile 构造函数,把图片文件的路径传递就去。由于之前把图片文件的编译类型设为Content ,只要项目中的文件夹结构就是文件系统的,那么就可以容易地访问它们。

现在运行应用程序,就得到下面的效果:

非常好。如果在 Interface Builder 中把表格风格改回 Plain(双击打开 MainWindow.xib ,选择表格,在下拉列表中选择风格为“Plain”,并保存),那么再次运行,就得到:

还是很漂亮的。

在 iPhone OS 3.0 之后,我们也能定义一些其他属性,包括TextColor、SelectedTextColor、SelectedImage、BackgroundView、SelectedBackgroundView等等。这虽然让你有了更多的个性化设置,不过有时候,我们需要完全不同的显示效果。

要完成这个工作,必须创建自己的表格单元格。苹果允许我们通过继承UITableViewCell ,并创建其中我们所想的任何内容。

那么,让我们来创建自定义的表格样式。首先,在项目中创建一个名为“Controls”的新文件夹。在其中,我们打算用 View Controller 来创建新视图。在 Controls 文件夹上点右键,选择“Add”,接着“New File”,从左栏选择“iPhone”并在右边选择“View Interface Definition with Controller”,并命名为“CustomTableViewCell”:

在Interface Builder 打开CustomTableViewCell.xib 。就会显示空白视图:

我们不想使用标准视图,而要创建一个 UITableViewCell,所以我们会删除这个视图并用一个单元格对象来替换它。

按住苹果键(Apple),按退格键,或者从编辑窗口中选择“Delete”就可以从 Document 窗口中删除这个视图。

接着,从 Library 窗口中拖一个 Table View Cell 到你的 Document 窗口中:

如果你在新添加到表格单元格上双击,就会打开设计器,你会看到一个设计界面来让你添加控件:

那么,现在让我们来使用Interface Builder 来设计自定义单元格。拖动右下角的让单元格变高点。接着,从Library 窗口,拖一个UIImageView 控件和几个UILabels 控件:

你可以使用Attributes Inspector 窗口来改变UILabel 控件的大小,以及颜色等等属性。不用看上去完全和上图一样,只需相关的元素都在对应位置就行。

现在,我们完成了自定义的设计,就来为自定义单元格添加outlet,和File’s Owner 的内容,以便我们能访问它们。之前,我们把outlet 直接添加到AppDelegate 中,不过这里,要把他们添加到File’s Owner 中,以便可以在所具有的视图上创建类。

在Document 窗口中选择“File’s Owner”,接着在Library 窗口中在顶部选择“Classes”,在下拉列表中选择“Other Classes”。选择我们的 CustomTableViewCell 类,接着在窗口的下半部选择“Outlets”标签页。添加名为“cellMain”、“lblHeading”、“lblSubHeading”和“imgMain:”的 Outlets:

把Outlets 和我们之前添加的控件关联起来,就像对表格控件所做的那样。

确保Document 窗口的“File’s Owner“是被选中的,从Connection Inspector 中把圆点拖到Document 窗口的控件上:

最后一件我们必须在Interface Builder 中完成的事情,就是提供一个单元格标识。在Document 窗口上点击”Table View Cell“,接着在Attributes Inspector 上把 Identifier 属性设置为”MyCustomTableCellView“:

这让我们的自定义单元格模板可以被重用,正如之前我们在代码中实现的那样。

我们已经完成了Interface Builder 中事情了,那么保存文件,返回到MonoDevelop 中。双击CustomTableCellView.xib.cs 打开这个类文件。

我们要对这个类进行一点编辑。首先,我们来添加一些属性到类里面:

复制代码
<span>public</span> <span>UITableViewCell</span> Cell
{
<span>get</span> { <span>return</span> this.cellMain; }
}
<span>public</span> <span>string</span> Heading
{
<span>get</span> { <span>return</span> this.lblHeading.Text; }
<span>set</span> { <span>this</span>.lblHeading.<span>Text</span> = <span>value</span>; }
}
<span>public</span> <span>string</span> SubHeading
{
<span>get</span> { <span>return</span> this.lblSubHeading.Text; }
<span>set</span> { <span>this</span>.lblSubHeading.<span>Text</span> = <span>value</span>; }
}
<span>public</span> <span>UIImage</span> Image
{
<span>get</span> { <span>return</span> this.imgMain.Image; }
<span>set</span> { <span>this</span>.imgMain.<span>Image</span> = <span>value</span>; }
}

我们来看一下这些属性:

  • Cell – 这个属性让这个自定义单元格的调用者可以直接访问单元格本身,以便在UITableViewSourceGetCell 调用中,我们能直接从一个属性简单地返回,过一会我们会看到怎么弄。
  • Heading 和 SubHeading – 这个属性暴露了可以直接访问表格单元格中UILabel 控件的文本值。
  • Image – 这个属性直接暴露表格单元格中的UIImage 控件。

接下来,我们需要编辑其中一个构造器。文件在被 MonoDevelop 创建好后,文件中的最后一个构造器看起来如下:

复制代码
<span>public</span> CustomTableViewCell () : <span>base</span>(<span>"CustomTableViewCell"</span>, <span>null</span>)
{
Initialize ();
}

这是一个非常简单的构造器,如果你读过 Mono 文档关于基类构造说明的话,就会明白这里它会初始化这个类,并用我们传递进去的名称来加载 Nib(编译好的.xib 文件)文件。然而,文档中没有提到的是,基类构造器是异步的。如果你使用这种方式来调用,那么 Nib 好像未被立刻加载,在我们给自定义单元格设置诸如Heading属性的时候,我们会得到讨厌的空引用错误。

为了修正这个问题,我们必须确保 Nib 文件同步地加载,即直到加载完成,方法是不会返回。

那么,就需要编辑构造器,以便看起来如下:

复制代码
<span>public</span> CustomTableViewCell ()
{
MonoTouch.Foundation.NSBundle.MainBundle.LoadNib (<span>"CustomTableViewCell"</span>, <span>this</span>, <span>null</span>);
Initialize ();
}

在这里,我们首先要做到就是,把调用基类构造器的代码注释掉。我们会手动地进行来调用那个代码,所以在这里不用再调一次。接下来,我们添加一个对 LoadNib 方法的调用。它实际上和基类构造器所做的事情是一样的,除了不会强制进行异步处理。现在,我们确保了 Nib 以及任何东西都能初始化,以便我们能访问它。

完整的类看上去如下所示:

复制代码
<span>using</span> System;
<span>using</span> MonoTouch.Foundation;
<span>using</span> MonoTouch.UIKit;
<span>using</span> System;
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>partial</span> <span>class</span> <span>CustomTableViewCell</span> : <span>UIViewController</span>
{
<span>#region</span> Constructors
<span>public</span> CustomTableViewCell (<span>IntPtr</span> handle) : <span>base</span>(handle)
{
Initialize ();
}
[Export(<span>"initWithCoder:"</span>)]
<span>public</span> CustomTableViewCell (<span>NSCoder</span> coder) : <span>base</span>(coder)
{
Initialize ();
}
<span>public</span> CustomTableViewCell ()
{
MonoTouch.Foundation.NSBundle.MainBundle.LoadNib (<span>"CustomTableViewCell"</span>, <span>this</span>, <span>null</span>);
Initialize ();
}
<span>void</span> Initialize ()
{
}
<span>#endregion</span>
<span>public</span> <span>UITableViewCell</span> Cell
{
<span>get</span> { <span>return</span> this.cellMain; }
}
<span>public</span> <span>string</span> Heading
{
<span>get</span> { <span>return</span> this.lblHeading.Text; }
<span>set</span> { <span>this</span>.lblHeading.<span>Text</span> = <span>value</span>; }
}
<span>public</span> <span>string</span> SubHeading
{
<span>get</span> { <span>return</span> this.lblSubHeading.Text; }
<span>set</span> { <span>this</span>.lblSubHeading.<span>Text</span> = <span>value</span>; }
}
<span>public</span> <span>UIImage</span> Image
{
<span>get</span> { <span>return</span> this.imgMain.Image; }
<span>set</span> { <span>this</span>.imgMain.<span>Image</span> = <span>value</span>; }
}
}
}

好,现在构建好自定义单元格类了,接着往下走,来创建自定义UITableViewSource 类。

在”Code“文件夹中,创建一个新的类,复制如下代码进去:

复制代码
<span>using</span> System;
<span>using</span> System.Collections.Generic;
<span>using</span> MonoTouch.UIKit;
<span>namespace</span> Example_CustomUITableViewCells
{
<span>public</span> <span>class</span> <span>CustomTableViewSource</span> : <span>UITableViewSource</span>
{
<span>protected</span> <span>List</span><<span>BasicTableViewItemGroup</span>> _tableItems;
<span>protected</span> <span>string</span> _customCellIdentifier = <span>"MyCustomTableCellView"</span>;
<span>protected</span> <span>Dictionary</span><<span>int</span>, <span>CustomTableViewCell</span>> _cellControllers =
<span>new</span> <span>Dictionary</span><<span>int</span>, <span>CustomTableViewCell</span>>();
<span>public</span> CustomTableViewSource (<span>List</span><<span>BasicTableViewItemGroup</span>> items)
{
<span>this</span>._tableItems = items;
}
<span>public</span> <span>override</span> <span>int</span> NumberOfSections (<span>UITableView</span> tableView)
{
<span>return</span> this._tableItems.Count;
}
<span>public</span> <span>override</span> <span>int</span> RowsInSection (<span>UITableView</span> tableview, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Items.Count;
}
<span>public</span> <span>override</span> <span>string</span> TitleForHeader (<span>UITableView</span> tableView, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Name;
}
<span>public</span> <span>override</span> <span>string</span> TitleForFooter (<span>UITableView</span> tableView, <span>int</span> section)
{
<span>return</span> this._tableItems[section].Footer;
}
<span>public</span> <span>override</span> <span>float</span> GetHeightForRow (<span>UITableView</span> tableView, MonoTouch.Foundation<span>NSIndexPath</span> indexPath)
{
<span>return</span> 109f;
}
<span>public</span> <span>override</span> <span>UITableViewCell</span> GetCell (<span>UITableView</span> tableView, MonoTouch.Foundation<span>NSIndexPath</span> indexPath)
{
<span>UITableViewCell</span> cell = tableView.DequeueReusableCell (<span>this</span>._customCellIdentifier);
<span>CustomTableViewCell</span> customCellController = <span>null</span>;
<span>if</span> (cell == <span>null</span>)
{
customCellController = <span>new</span> <span>CustomTableViewCell</span> ();
cell = customCellController.Cell;
cell.Tag = <span>Environment</span>.TickCount;
<span>this</span>._cellControllers.Add (cell.Tag, customCellController);
}
<span>else</span>
{
customCellController = <span>this</span>._cellControllers[cell.Tag];
}
<span>BasicTableViewItem</span> item = <span>this</span>._tableItems[indexPath.<span>Section</span>].Items[indexPath.<span>Row</span>];
customCellController.Heading = item.Name;
customCellController.SubHeading = item.SubHeading;
<span>if</span> (!<span>string</span>.IsNullOrEmpty (item.ImageName))
{
customCellController.<span>Image</span> = UIImage.FromFile (<span>"Images/"</span> + item.ImageName);
}
<span>return</span> cell;
}
}
}

如果你浏览一遍这个代码,就看到它几乎和BasicTableViewSource 一样,只有少许改变。

要注意到第一件事,是声明部分:

复制代码
<span>protected</span> <span>List</span><<span>BasicTableViewItemGroup</span>> _tableItems;
<span>protected</span> <span>string</span> _customCellIdentifier = <span>"MyCustomTableCellView"</span>;
<span>protected</span> <span>Dictionary</span><<span>int</span>, <span>CustomTableViewCell</span>> _cellControllers =
<span>new</span> <span>Dictionary</span><<span>int</span>, <span>CustomTableViewCell</span>>();

我们添加了一个名为_cellControllers新变量。我们将在后面看到如何使用它,它会保存着自定义单元格的集合。

下一个增加的东西是GetHeightForRow 方法:

复制代码
<span>public</span> <span>override</span> <span>float</span> GetHeightForRow (<span>UITableView</span> tableView, MonoTouch.Foundation<span>NSIndexPath</span> indexPath)
{
<span>return</span> 109f;
}

当我们创建自定义单元格控制器之后,我们让它比通常的单元格要高。在数据绑定的过程中,CocoaTouch 需要知道要给这个单元格分配多少空间。这就是要调用GetHeightForRow 方法的缘由。如果我们不告知新的大小,那么系统会按照标准尺寸去调整布局,我们就会看到怪异的结果。

如果我们在 Interface Builder 中打开我们的单元格,并通过 Size Inspector 查看,会知道单元格的高度上多少。在我们的例子中,它是 109 像素,不过你的可能会有所不同。

最后改变的一块地方是,GetCell 方法:

复制代码
<span>public</span> <span>override</span> <span>UITableViewCell</span> GetCell (<span>UITableView</span> tableView, MonoTouch.Foundation<span>NSIndexPath</span> indexPath)
{
<span>UITableViewCell</span> cell = tableView.DequeueReusableCell (<span>this</span>._customCellIdentifier);
<span>CustomTableViewCell</span> customCellController = <span>null</span>;
<span>if</span> (cell == <span>null</span>)
{
customCellController = <span>new</span> <span>CustomTableViewCell</span> ();
cell = customCellController.Cell;
cell.Tag = <span>Environment</span>.TickCount;
<span>this</span>._cellControllers.Add (cell.Tag, customCellController);
}
<span>else</span>
{
customCellController = <span>this</span>._cellControllers[cell.Tag];
}
<span>BasicTableViewItem</span> item = <span>this</span>._tableItems[indexPath.<span>Section</span>].Items[indexPath.<span>Row</span>];
customCellController.Heading = item.Name;
customCellController.SubHeading = item.SubHeading;
<span>if</span> (!<span>string</span>.IsNullOrEmpty (item.ImageName))
{
customCellController.<span>Image</span> = UIImage.FromFile (<span>"Images/"</span> + item.ImageName);
}
<span>return</span> cell;
}

这个方法开始非常类似于BasicTableViewSource 类,我们尝试通过DequeueReusableCell 的调用来从缓存池中重用的一个单元格。从这之后,就变得复杂了。如果我们不能在重用的时候得到单元格,那么我们必须去创建一个新的。它就是由我们的CustomTableViewCell 类所创建的,它实际上是一个包含着自定义单元格的控制器。

一旦我们创建了控制器,我们就把 cell 变量赋值控制器的 Cell 属性。

我们接着为单元格添加了一个唯一标识符,以便我们在后面能把它和正确的CustomTableViewCell 控制器关联在一起。我们使用Environment.TickCount ,因为它提供了比较稳妥的唯一性值。如果我们想,也可以使用Guid.NewGuid,不过调用它稍微有点昂贵。

接下来,我们使用相同的 tag 标识,来创建存储在CustomTableViewCell 的字典对象。

这也就是我们下面这行代码显而易见的原因:

复制代码
<span>else</span>
{
customCellController = <span>this</span>._cellControllers[cell.Tag];
}

如果我们找到一个可重用的单元格,那么就需要通过控制器来设置相关属性。为了这样,必须使用之前我们使用过的标识符从 Dictionary 中提取出来。

到这里,已经涉及了对这个类的大部分改变。接下来的几行,为了改变控制器类中的属性,进行了相对于单元格本身较简单的修改。

好了!我们快要完成了。打开 Main.cs 文件,我们对 AppDelegate 类进行两个地方的改变,让其使用我们的CustomTableViewSource 而非BasicTableViewSource。

首先,注释掉这行:

<span>BasicTableViewSource</span> _tableViewSource;并紧接着后添加这行:

<span>CustomTableViewSource</span> _tableViewSource;接着,在CreateTableItems 方法中,注释掉这行:

<span>this</span>._tableViewSource = <span>new</span> <span>BasicTableViewSource</span>(tableItems);并紧接着后面添加这行:

<span>this</span>._tableViewSource = <span>new</span> <span>CustomTableViewSource</span>(tableItems);我们修改后的AppDelegate 类看起来如下:

复制代码
<span>public</span> <span>partial</span> <span>class</span> <span>AppDelegate</span> : <span>UIApplicationDelegate</span>
{
<span>CustomTableViewSource</span> _tableViewSource;
<span>public</span> <span>override</span> <span>bool</span> FinishedLaunching (<span>UIApplication</span> app, <span>NSDictionary</span> options)
{
<span>this</span>.CreateTableItems ();
<span>this</span>.tblMain.Source = <span>this</span>._tableViewSource;
window.MakeKeyAndVisible ();
<span>return</span> <span>true</span>;
}
<span>public</span> <span>override</span> <span>void</span> OnActivated (<span>UIApplication</span> application)
{
}
<span>protected</span> <span>void</span> CreateTableItems ()
{
<span>List</span><<span>BasicTableViewItemGroup</span>> tableItems = <span>new</span> <span>List</span><<span>BasicTableViewItemGroup</span>> ();
<span>BasicTableViewItemGroup</span> tGroup;
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Birds"</span>, Footer = <span>"Birds have wings, and sometimes use them."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Crow"</span>, SubHeading = <span>"AKA, Raven."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Chicken"</span>, SubHeading = <span>"Males are called roosters."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Turkey"</span>, SubHeading = <span>"Eaten at thanksgiving."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Fish"</span>, Footer = <span>"Fish live in water. Mostly."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Trout"</span>, SubHeading = <span>"Rainbow is a popular kind."</span>, ImageName = <span>"TargetIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Salmon"</span>, SubHeading = <span>"Good sushi."</span>, ImageName = <span>"PawIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Cod"</span>, SubHeading = <span>"Flat fish."</span>, ImageName = <span>"LightBulbIcon.png"</span> });
tableItems.Add (tGroup);
tGroup = <span>new</span> <span>BasicTableViewItemGroup</span>() { Name = <span>"Mammals"</span>, Footer = <span>"Mammals nurse their young."</span> };
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Deer"</span>, SubHeading = <span>"Bambi."</span>, ImageName = <span>"PushPinIcon.png"</span> });
tGroup.Items.Add (<span>new</span> <span>BasicTableViewItem</span>() { Name = <span>"Bats"</span>, SubHeading = <span>"Fly at night."</span>, ImageName = <span>"TargetIcon.png"</span> });
tableItems.Add (tGroup);
<span>this</span>._tableViewSource = <span>new</span> <span>CustomTableViewSource</span>(tableItems);
}
}

运行这个应用程序,就能看到下面的效果:

如果你在 Interface Builder 中把表格风格改为 Grouped,保存改变,再次运行程序,你就会看到下图:

恭喜!做了这么多的工作,终于完成了,不过你也学到了如何在 MonoTouch 中自定义表格。现在,我们已经让其运行起来了,来看一下在进行自定义单元格的时候,要考虑到一些事项。

性能考虑

如果没有正确地处理自定义单元格的开发,在使用UITableView 的时候会造成严重的性能问题。特别在你有很多行的时候更是如此。iPhone,虽然令人印象深刻,不过却只有性能有限的处理器,而为了获得用户所期望的较好响应能力,你应该考虑几个性能优化方式。

另外,尽早部署到设备上经常性地评估自定义表格的性能,是一种可取的做法。模拟器就是一个模拟器而已,并非真正的设备。它比设备运行的要快得多,如果你依赖于模拟器来测量应用程序的性能,那么你很可能会在部署到设备上的时候大失所望。

而且,在你测试自定义表格的时候,如果用户能添加自己的行,那么你应该测试应用程序有很多行的性能。如果它依然“活蹦乱跳”,具有很好的响应,那么你就能确信你已经开发正确,而你的用户应该不会对性能失望。

  • 单元格重用 - 单元格重用是获得具有良好响应表格的第一个技术。特别在具有大量的行的时候,更是需要。如果你不重用单元格,那么 iPhone OS 不得不在每个单元格显示的时候去创建一个新的实例。这马上就会变成一个严重的问题。为了确定你的单元格是否正被重用,建议在单元格重用代码中使用Console.WriteLine 把重用的过程打印到应用程序控制台。确保在第一次显示界面的时候,添加了足够多的行,以便在滚动屏幕的时候,OS 有机会重用老的已经被滚动出屏幕的单元格。本文章的例子就展示了如何正确地重用单元格,如果你的单元格没有重用,研究一下代码示例,确保所有实现都是正确的。
  • 缓存行高 - 表格会经常性地获取行的高度。实际上,它会在每次单元格创建的时候都获取一遍,在某些时候甚至会更频繁。如果你基于单元格内容来计算行高,那么确保要缓存这个值,以便你不必总是计算它。实际上,你可能会在单元格控制器中创建一个属性,计算,然后缓存之。
  • 缓存图片 - 如果你正使用图片,考虑缓存它们,以便不必每次显示的时候都从文件中载入。如果单元格之间会共享图片,可以考虑把他们放到一个字典对象中,并从里面来获取图片实例。要小心不要加载太多图片。用到可供你支配的 iPhone 内存的一半是不太正常的,因此如果你有大量的图片,你就要做出选择,是从文件中加载它们,还是在缓存字典中只保留一定数量的图片,没有的时候再从文件中加载。
  • 远离透明效果 - 在 iPhone 上执行的一个非常昂贵的操作就是渲染透明效果。在 iPhone 上有两个绘图系统,CoreGraphics 和 CoreAnimation。CoreGraphics 利用 GPU,而 CoreAnimation 根据要处理内容的快慢也利用主处理器,不过通常主要使用 GPU。iPhone 的 GPU 的问题在于它没有针对颜色混合进行优化。因此,你应该尽可能尝试避免透明效果。如果你确实不能避免,那么你应该通过重写DrawRect 方法来自己完成混合过程,并直接处理绘图计算,关于这点我们将在下一条中详细讲到。
  • 手动在 DrawRect 中绘制单元格 - 要解决上面提到的性能问题的最后努力,就是重写DrawRect 方法,自己执行绘图。然而这也有缺点。首先,在技术上相对复杂,第二,它会占用大量内存。就这点而言,它更适合那些没有大量行的表格,不过这样也会占用大量的处理器资源。对这个技术更多的信息,可以参考苹果示例程序的第 15 个例子——TableViewSuite,下面会给出注释。
  • 避免复杂的图形计算 - 正如透明效果问题一下,尽量远离那些需要计算的图形元素,比如在显示的时候把图片处理为渐变效果。
  • 编程创建单元格 - 如果你已经想方设法进行优化了,依然还是需要进一步提升性能,那么你可以不用 Interface Builder 来创建自定义UITableViewCell ,而是编程手写这个控件。关于编程构建视图的更多信息,可访问 Craig Dunn 的博客帖子。要学习如何不使用 Interface Builder 来创建 MonoTouch 应用程序,可以阅读 MonoTouch.info 里面列出的文章,在这里: http://monotouch.info/Tags/NoIB

展示进行性能优化的更多例子,可以访问苹果的 TableViewSuite 示例应用程序

它虽然是用 Objective-C 编写的,不过概念是相同的。


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011 年 1 月 13 日 00:002524
用户头像

发布了 254 篇内容, 共 37.7 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论

架构师训练营第一周命题作业

whiter

极客大学架构师训练营

食堂就餐卡系统设计

互金从业者X

第一周作业

东哥

极客大学架构师训练营

如何成为一个架构师?

逍遥乐天

极客大学架构师训练营

第一周学习总结

AspYc

食堂就餐卡系统架构设计文档

小叶

架构设计

第一周命题作业

AspYc

被迫重构代码,这次我干掉了 if-else

程序员内点事

食堂就餐卡系统设计

魔曦

极客大学架构师训练营

架构师训练营第01周——总结

李伟

极客大学架构师训练营

4天如何完爆Kafka源码核心流程!

古月木易

kafka

第一周学习总结

小海豚

学习

架构师训练营第一周学习总结

whiter

极客大学架构师训练营

While语句

拾贝

食堂就餐卡系统设计

小海豚

学习 食堂就餐卡系统设计

你还在为 TCP 重传、滑动窗口、流量控制、拥塞控制发愁吗?看完图解就不愁了

小林coding

TCP 计算机网络 网络协议

免费P7架构师直播课!技术人员如何提升职场技能?

奈学教育

架构师

免费P7架构师直播课!技术人员如何提升职场技能?

古月木易

架构师

架构师训练营-第一课作业-20200610-食堂就餐卡系统

👑👑merlan

架构 作业

01-kubernetes安装部署(手动)

绿星雪碧

Kubernetes etcd flannel

UML 体验(就餐卡系统设计)

陈皮

【架构师训练营】第1周-作业-食堂就餐卡系统

芥末

极客大学架构师训练营

写作的几点建议:面对卡文,写别人的题目,栩栩如生的写作

七镜花园-董一凡

写作

【总结】第一周架构师如何做架构

chengjing

一味的坚持,或许只是徒劳

这小胖猫

逻辑思维 职业成长 工作体会

食堂就餐卡系统设计(作业版)

Jerry Tse

极客大学架构师训练营 作业

4天如何完爆Kafka源码核心流程!

奈学教育

kafka

架构师训练营-作业-1】食堂就餐卡系统设计

superman

学习 极客大学架构师训练营

架构师培训-01食堂就餐卡系统设计文档

刘敏

架构师是什么?

芥末

极客大学架构师训练营

作业1

annie

极客大学架构师训练营

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

在MonoTouch中自定义表格-InfoQ